Skip to content

Commit 33af9ea

Browse files
authored
Support arrays in CLI arguments (#1300)
* Support arrays in CLI arguments * Support arrays of objects in CLI via arrays of JSON * Do not split by comma if JSON is involved * Refine some tests, improve readability * Use an enum OptionSource
1 parent 4d49354 commit 33af9ea

File tree

3 files changed

+64
-12
lines changed

3 files changed

+64
-12
lines changed

src/cli/diagnostics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export const compilerOptionRequiresAValueOfType = createCommandLineError(
3333
(name: string, type: string) => `Compiler option '${name}' requires a value of type ${type}.`
3434
);
3535

36+
export const compilerOptionCouldNotParseJson = createCommandLineError(
37+
5025,
38+
(name: string, error: string) => `Compiler option '${name}' failed to parse the given JSON value: '${error}'.`
39+
);
40+
3641
export const optionProjectCannotBeMixedWithSourceFilesOnACommandLine = createCommandLineError(
3742
5042,
3843
() => "Option 'project' cannot be mixed with source files on a command line."

src/cli/parse.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface CommandLineOptionOfEnum extends CommandLineOptionBase {
1818
}
1919

2020
interface CommandLineOptionOfPrimitive extends CommandLineOptionBase {
21-
type: "boolean" | "string" | "object" | "array";
21+
type: "boolean" | "string" | "json-array-of-objects" | "array";
2222
}
2323

2424
type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive;
@@ -82,7 +82,7 @@ export const optionDeclarations: CommandLineOption[] = [
8282
{
8383
name: "luaPlugins",
8484
description: "List of TypeScriptToLua plugins.",
85-
type: "object",
85+
type: "json-array-of-objects",
8686
},
8787
{
8888
name: "tstlVerbose",
@@ -124,7 +124,7 @@ export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine):
124124
continue;
125125
}
126126

127-
const { error, value } = readValue(option, rawValue);
127+
const { error, value } = readValue(option, rawValue, OptionSource.TsConfig);
128128
if (error) parsedConfigFile.errors.push(error);
129129
if (parsedConfigFile.options[name] === undefined) parsedConfigFile.options[name] = value;
130130
}
@@ -164,9 +164,9 @@ function updateParsedCommandLine(parsedCommandLine: ts.ParsedCommandLine, args:
164164
if (error) parsedCommandLine.errors.push(error);
165165
parsedCommandLine.options[option.name] = value;
166166
if (consumed) {
167-
i += 1;
168167
// Values of custom options are parsed as a file name, exclude them
169-
parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(f => f !== value);
168+
parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(f => f !== args[i + 1]);
169+
i += 1;
170170
}
171171
}
172172
}
@@ -196,21 +196,25 @@ function readCommandLineArgument(option: CommandLineOption, value: any): Command
196196
};
197197
}
198198

199-
return { ...readValue(option, value), consumed: true };
199+
return { ...readValue(option, value, OptionSource.CommandLine), consumed: true };
200+
}
201+
202+
enum OptionSource {
203+
CommandLine,
204+
TsConfig,
200205
}
201206

202207
interface ReadValueResult {
203208
error?: ts.Diagnostic;
204209
value: any;
205210
}
206211

207-
function readValue(option: CommandLineOption, value: unknown): ReadValueResult {
212+
function readValue(option: CommandLineOption, value: unknown, source: OptionSource): ReadValueResult {
208213
if (value === null) return { value };
209214

210215
switch (option.type) {
211216
case "boolean":
212-
case "string":
213-
case "object": {
217+
case "string": {
214218
if (typeof value !== option.type) {
215219
return {
216220
value: undefined,
@@ -220,15 +224,44 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult {
220224

221225
return { value };
222226
}
223-
case "array": {
224-
if (!Array.isArray(value)) {
227+
case "array":
228+
case "json-array-of-objects": {
229+
const isInvalidNonCliValue = source === OptionSource.TsConfig && !Array.isArray(value);
230+
const isInvalidCliValue = source === OptionSource.CommandLine && typeof value !== "string";
231+
232+
if (isInvalidNonCliValue || isInvalidCliValue) {
225233
return {
226234
value: undefined,
227235
error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, option.type),
228236
};
229237
}
230238

231-
return { value };
239+
const shouldParseValue = source === OptionSource.CommandLine && typeof value === "string";
240+
if (!shouldParseValue) return { value };
241+
242+
if (option.type === "array") {
243+
const array = value.split(",");
244+
return { value: array };
245+
}
246+
247+
try {
248+
const objects = JSON.parse(value);
249+
if (!Array.isArray(objects)) {
250+
return {
251+
value: undefined,
252+
error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, option.type),
253+
};
254+
}
255+
256+
return { value: objects };
257+
} catch (e) {
258+
if (!(e instanceof SyntaxError)) throw e;
259+
260+
return {
261+
value: undefined,
262+
error: cliDiagnostics.compilerOptionCouldNotParseJson(option.name, e.message),
263+
};
264+
}
232265
}
233266
case "enum": {
234267
if (typeof value !== "string") {

test/cli/parse.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ describe("command line", () => {
5959
});
6060
});
6161

62+
describe("array-of-objects options", () => {
63+
test.each([["{"], ["{}"], ["0"], ["''"]])("should error on invalid value (%s)", value => {
64+
const result = tstl.parseCommandLine(["--luaPlugins", value]);
65+
66+
expect(result.errors).toHaveDiagnostics();
67+
});
68+
});
69+
6270
describe("boolean options", () => {
6371
test.each([true, false])("should parse booleans (%p)", value => {
6472
const result = tstl.parseCommandLine(["--noHeader", value.toString()]);
@@ -125,6 +133,12 @@ describe("command line", () => {
125133

126134
["extension", ".lua", { extension: ".lua" }],
127135
["extension", "scar", { extension: "scar" }],
136+
137+
["luaPlugins", '[{ "name": "a" }]', { luaPlugins: [{ name: "a" }] }],
138+
["luaPlugins", '[{ "name": "a" },{ "name": "b" }]', { luaPlugins: [{ name: "a" }, { name: "b" }] }],
139+
140+
["noResolvePaths", "path1", { noResolvePaths: ["path1"] }],
141+
["noResolvePaths", "path1,path2", { noResolvePaths: ["path1", "path2"] }],
128142
])("--%s %s", (optionName, value, expected) => {
129143
const result = tstl.parseCommandLine([`--${optionName}`, value]);
130144

0 commit comments

Comments
 (0)