Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix(29118): tsconfig.extends as array #50403

Merged
merged 15 commits into from
Dec 13, 2022
Merged
1 change: 1 addition & 0 deletions src/compiler/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ namespace ts {

function convertToReusableCompilerOptionValue(option: CommandLineOption | undefined, value: CompilerOptionsValue, relativeToBuildInfo: (path: string) => string) {
if (option) {
Debug.assert(option.type !== "listOrElement");
if (option.type === "list") {
const values = value as readonly (string | number)[];
if (option.element.isFilePath && values.length) {
Expand Down
212 changes: 155 additions & 57 deletions src/compiler/commandLineParser.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6598,7 +6598,7 @@ namespace ts {
/* @internal */
export interface CommandLineOptionBase {
name: string;
type: "string" | "number" | "boolean" | "object" | "list" | ESMap<string, number | string>; // a value of a primitive type, or an object literal mapping named values to actual values
type: "string" | "number" | "boolean" | "object" | "list" | "listOrElement" | ESMap<string, number | string> ; // a value of a primitive type, or an object literal mapping named values to actual values
isFilePath?: boolean; // True if option value is a path or fileName
shortName?: string; // A short mnemonic for convenience - for instance, 'h' can be used in place of 'help'
description?: DiagnosticMessage; // The message describing what the command line switch does.
Expand Down Expand Up @@ -6669,7 +6669,7 @@ namespace ts {

/* @internal */
export interface CommandLineOptionOfListType extends CommandLineOptionBase {
type: "list";
type: "list" | "listOrElement";
element: CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption;
listPreserveFalsyValues?: boolean;
}
Expand Down
6 changes: 4 additions & 2 deletions src/executeCommandLine/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ namespace ts {
const defaultValueDescription =
typeof option.defaultValueDescription === "object"
? getDiagnosticText(option.defaultValueDescription)
: formatDefaultValue(
: Debug.assert(option.type !== "listOrElement");
formatDefaultValue(
option.defaultValueDescription,
option.type === "list" ? option.element.type : option.type
);
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -276,6 +277,7 @@ namespace ts {
};

function getValueType(option: CommandLineOption) {
Debug.assert(option.type !== "listOrElement");
switch (option.type) {
case "string":
case "number":
Expand All @@ -297,7 +299,7 @@ namespace ts {
possibleValues = option.type;
break;
case "list":
// TODO: check infinite loop
case "listOrElement":
possibleValues = getPossibleValues(option.element);
break;
case "object":
Expand Down
1 change: 1 addition & 0 deletions src/services/getEditsForFileRename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ namespace ts {
case "compilerOptions":
forEachProperty(property.initializer, (property, propertyName) => {
const option = getOptionFromName(propertyName);
Debug.assert(option?.type !== "listOrElement");
if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) {
updatePaths(property);
}
Expand Down
80 changes: 77 additions & 3 deletions src/testRunner/unittests/config/configurationExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,47 @@ namespace ts {
"dev/tests/unit/spec.ts": "",
"dev/tests/utils.ts": "",
"dev/tests/scenarios/first.json": "",
"dev/tests/baselines/first/output.ts": ""
"dev/tests/baselines/first/output.ts": "",
"dev/configs/extendsArrayFirst.json": JSON.stringify({
compilerOptions: {
allowJs: true,
noImplicitAny: true,
strictNullChecks: true
}
}),
"dev/configs/extendsArraySecond.json": JSON.stringify({
compilerOptions: {
module: "amd"
},
include: ["../supplemental.*"]
}),
"dev/configs/extendsArrayThird.json": JSON.stringify({
compilerOptions: {
module: null, // eslint-disable-line no-null/no-null
noImplicitAny: false
},
extends: "./extendsArrayFirst",
include: ["../supplemental.*"]
}),
"dev/configs/extendsArrayFourth.json": JSON.stringify({
compilerOptions: {
module: "system",
strictNullChecks: false
},
include: null, // eslint-disable-line no-null/no-null
files: ["../main.ts"]
}),
"dev/configs/extendsArrayFifth.json": JSON.stringify({
extends: ["./extendsArrayFirst", "./extendsArraySecond", "./extendsArrayThird", "./extendsArrayFourth"],
files: [],
}),
"dev/extendsArrayFails.json": JSON.stringify({
extends: ["./missingFile"],
compilerOptions: {
types: []
}
}),
"dev/extendsArrayFails2.json": JSON.stringify({ extends: [42] }),
}
}
});
Expand Down Expand Up @@ -292,9 +332,9 @@ namespace ts {
messageText: `Unknown option 'excludes'. Did you mean 'exclude'?`
}]);

testFailure("can error when 'extends' is not a string", "extends.json", [{
testFailure("can error when 'extends' is not a string or Array", "extends.json", [{
code: 5024,
messageText: `Compiler option 'extends' requires a value of type string.`
messageText: `Compiler option 'extends' requires a value of type string or Array.`
}]);

testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", {
Expand Down Expand Up @@ -349,6 +389,40 @@ namespace ts {
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
});
});

describe(testName, () => {
it("adds extendedSourceFiles from an array only once", () => {
const sourceFile = readJsonConfigFile("configs/extendsArrayFifth.json", (path) => host.readFile(path));
const dir = combinePaths(basePath, "configs");
const expected = [
combinePaths(dir, "extendsArrayFirst.json"),
combinePaths(dir, "extendsArraySecond.json"),
combinePaths(dir, "extendsArrayThird.json"),
combinePaths(dir, "extendsArrayFourth.json"),
];
parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "extendsArrayFifth.json");
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "extendsArrayFifth.json");
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
});

testSuccess("can overwrite top-level compilerOptions", "configs/extendsArrayFifth.json", {
allowJs: true,
noImplicitAny: false,
strictNullChecks: false,
module: ModuleKind.System
}, []);

testFailure("can report missing configurations", "extendsArrayFails.json", [{
code: 6053,
messageText: `File './missingFile' not found.`
}]);

testFailure("can error when 'extends' is not a string or Array2", "extendsArrayFails2.json", [{
code: 5024,
messageText: `Compiler option 'extends' requires a value of type string.`
}]);
});
});
});
}
4 changes: 4 additions & 0 deletions src/testRunner/unittests/config/showConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ namespace ts {
}
break;
}
case "listOrElement": {
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
Debug.fail();
break;
}
case "string": {
if (option.isTSConfigOnly) {
args = ["-p", "tsconfig.json"];
Expand Down
57 changes: 55 additions & 2 deletions src/testRunner/unittests/tsbuildWatch/programUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export function someFn() { }`),
verifyTscWatch({
scenario: "programUpdates",
subScenario: "works with extended source files",
commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json"],
commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json", "project3.tsconfig.json"],
sys: () => {
const alphaExtendedConfigFile: File = {
path: "/a/b/alpha.tsconfig.json",
Expand Down Expand Up @@ -602,10 +602,49 @@ export function someFn() { }`),
files: [otherFile.path]
})
};
const otherFile2: File = {
path: "/a/b/other2.ts",
content: "let k = 0;",
};
const extendsConfigFile1: File = {
path: "/a/b/extendsConfig1.tsconfig.json",
content: JSON.stringify({
compilerOptions: {
composite: true,
}
})
};
const extendsConfigFile2: File = {
path: "/a/b/extendsConfig2.tsconfig.json",
content: JSON.stringify({
compilerOptions: {
strictNullChecks: false,
}
})
};
const extendsConfigFile3: File = {
path: "/a/b/extendsConfig3.tsconfig.json",
content: JSON.stringify({
compilerOptions: {
noImplicitAny: true,
}
})
};
const project3Config: File = {
path: "/a/b/project3.tsconfig.json",
content: JSON.stringify({
extends: ["./extendsConfig1.tsconfig.json", "./extendsConfig2.tsconfig.json", "./extendsConfig3.tsconfig.json"],
compilerOptions: {
composite: false,
},
files: [otherFile.path]
})
};
return createWatchedSystem([
libFile,
alphaExtendedConfigFile, project1Config, commonFile1, commonFile2,
bravoExtendedConfigFile, project2Config, otherFile
bravoExtendedConfigFile, project2Config, otherFile, otherFile2,
extendsConfigFile1, extendsConfigFile2, extendsConfigFile3, project3Config
], { currentDirectory: "/a/b" });
},
changes: [
Expand Down Expand Up @@ -646,6 +685,20 @@ export function someFn() { }`),
change: noop,
timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2
},
{
caption: "Modify extendsConfigFile2",
change: sys => sys.writeFile("/a/b/extendsConfig2.tsconfig.json", JSON.stringify({
compilerOptions: { strictNullChecks: true }
})),
timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project1
},
{
caption: "Modify project 3",
change: sys => sys.writeFile("/a/b/project3.tsconfig.json", JSON.stringify({
extends: ["./extendsConfig1.tsconfig.json", "./extendsConfig2.tsconfig.json"],
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
})),
timeouts: checkSingleTimeoutQueueLengthAndRun // Build project1
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
},
]
});

Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4949,7 +4949,7 @@ declare namespace ts {
/**
* Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet
*/
extendedConfigPath?: string;
extendedConfigPath?: string | string[];
}
export interface ExtendedConfigCacheEntry {
extendedResult: TsConfigSourceFile;
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4949,7 +4949,7 @@ declare namespace ts {
/**
* Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet
*/
extendedConfigPath?: string;
extendedConfigPath?: string | string[];
}
export interface ExtendedConfigCacheEntry {
extendedResult: TsConfigSourceFile;
Expand Down
Loading