Skip to content

Commit 7267fca

Browse files
authored
Fix(29118): tsconfig.extends as array (#50403)
* tsconfig.extends as array * Updated baselines * Changes for pr * Changes for pr comments * Fixed formatting and edited a test * Resolved errors after a merge conflict * Added "string | list" type implentation * Removed string | list type implementation * Fixed formatting * Added compiler test * Resolving programUpdate errors * Fixing commandLineParser error
1 parent a3802c1 commit 7267fca

17 files changed

+767
-104
lines changed

src/compiler/builder.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde
11611161

11621162
function convertToReusableCompilerOptionValue(option: CommandLineOption | undefined, value: CompilerOptionsValue, relativeToBuildInfo: (path: string) => string) {
11631163
if (option) {
1164+
Debug.assert(option.type !== "listOrElement");
11641165
if (option.type === "list") {
11651166
const values = value as readonly (string | number)[];
11661167
if (option.element.isFilePath && values.length) {

src/compiler/commandLineParser.ts

+158-58
Large diffs are not rendered by default.

src/compiler/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6977,7 +6977,7 @@ export interface CreateProgramOptions {
69776977
/** @internal */
69786978
export interface CommandLineOptionBase {
69796979
name: string;
6980-
type: "string" | "number" | "boolean" | "object" | "list" | Map<string, number | string>; // a value of a primitive type, or an object literal mapping named values to actual values
6980+
type: "string" | "number" | "boolean" | "object" | "list" | "listOrElement" | Map<string, number | string>; // a value of a primitive type, or an object literal mapping named values to actual values
69816981
isFilePath?: boolean; // True if option value is a path or fileName
69826982
shortName?: string; // A short mnemonic for convenience - for instance, 'h' can be used in place of 'help'
69836983
description?: DiagnosticMessage; // The message describing what the command line switch does.
@@ -7047,7 +7047,7 @@ export interface TsConfigOnlyOption extends CommandLineOptionBase {
70477047

70487048
/** @internal */
70497049
export interface CommandLineOptionOfListType extends CommandLineOptionBase {
7050-
type: "list";
7050+
type: "list" | "listOrElement";
70517051
element: CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption;
70527052
listPreserveFalsyValues?: boolean;
70537053
}

src/executeCommandLine/executeCommandLine.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign
253253
typeof option.defaultValueDescription === "object"
254254
? getDiagnosticText(option.defaultValueDescription)
255255
: formatDefaultValue(
256-
option.defaultValueDescription,
257-
option.type === "list" ? option.element.type : option.type
258-
);
256+
option.defaultValueDescription,
257+
option.type === "list" || option.type === "listOrElement" ? option.element.type : option.type
258+
);
259259
const terminalWidth = sys.getWidthOfTerminal?.() ?? 0;
260260

261261
// Note: child_process might return `terminalWidth` as undefined.
@@ -365,6 +365,7 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign
365365
};
366366

367367
function getValueType(option: CommandLineOption) {
368+
Debug.assert(option.type !== "listOrElement");
368369
switch (option.type) {
369370
case "string":
370371
case "number":
@@ -386,7 +387,7 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign
386387
possibleValues = option.type;
387388
break;
388389
case "list":
389-
// TODO: check infinite loop
390+
case "listOrElement":
390391
possibleValues = getPossibleValues(option.element);
391392
break;
392393
case "object":

src/harness/harnessIO.ts

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ export namespace Compiler {
380380
}
381381
// If not a primitive, the possible types are specified in what is effectively a map of options.
382382
case "list":
383+
case "listOrElement":
383384
return ts.parseListTypeOption(option, value, errors);
384385
default:
385386
return ts.parseCustomTypeOption(option as ts.CommandLineOptionOfCustomType, value, errors);

src/services/getEditsForFileRename.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change
120120
if (foundExactMatch || propertyName !== "include" || !isArrayLiteralExpression(property.initializer)) return;
121121
const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined);
122122
if (includes.length === 0) return;
123-
const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory);
123+
const matchers = getFileMatcherPatterns(configDir, /*excludes*/[], includes, useCaseSensitiveFileNames, currentDirectory);
124124
// If there isn't some include for this, add a new one.
125125
if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) &&
126126
!getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) {
@@ -131,6 +131,7 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change
131131
case "compilerOptions":
132132
forEachProperty(property.initializer, (property, propertyName) => {
133133
const option = getOptionFromName(propertyName);
134+
Debug.assert(option?.type !== "listOrElement");
134135
if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) {
135136
updatePaths(property);
136137
}

src/testRunner/unittests/config/configurationExtension.ts

+78-3
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,47 @@ function createFileSystem(ignoreCase: boolean, cwd: string, root: string) {
186186
"dev/tests/unit/spec.ts": "",
187187
"dev/tests/utils.ts": "",
188188
"dev/tests/scenarios/first.json": "",
189-
"dev/tests/baselines/first/output.ts": ""
189+
"dev/tests/baselines/first/output.ts": "",
190+
"dev/configs/extendsArrayFirst.json": JSON.stringify({
191+
compilerOptions: {
192+
allowJs: true,
193+
noImplicitAny: true,
194+
strictNullChecks: true
195+
}
196+
}),
197+
"dev/configs/extendsArraySecond.json": JSON.stringify({
198+
compilerOptions: {
199+
module: "amd"
200+
},
201+
include: ["../supplemental.*"]
202+
}),
203+
"dev/configs/extendsArrayThird.json": JSON.stringify({
204+
compilerOptions: {
205+
module: null, // eslint-disable-line no-null/no-null
206+
noImplicitAny: false
207+
},
208+
extends: "./extendsArrayFirst",
209+
include: ["../supplemental.*"]
210+
}),
211+
"dev/configs/extendsArrayFourth.json": JSON.stringify({
212+
compilerOptions: {
213+
module: "system",
214+
strictNullChecks: false
215+
},
216+
include: null, // eslint-disable-line no-null/no-null
217+
files: ["../main.ts"]
218+
}),
219+
"dev/configs/extendsArrayFifth.json": JSON.stringify({
220+
extends: ["./extendsArrayFirst", "./extendsArraySecond", "./extendsArrayThird", "./extendsArrayFourth"],
221+
files: [],
222+
}),
223+
"dev/extendsArrayFails.json": JSON.stringify({
224+
extends: ["./missingFile"],
225+
compilerOptions: {
226+
types: []
227+
}
228+
}),
229+
"dev/extendsArrayFails2.json": JSON.stringify({ extends: [42] }),
190230
}
191231
}
192232
});
@@ -295,9 +335,9 @@ describe("unittests:: config:: configurationExtension", () => {
295335
messageText: `Unknown option 'excludes'. Did you mean 'exclude'?`
296336
}]);
297337

298-
testFailure("can error when 'extends' is not a string", "extends.json", [{
338+
testFailure("can error when 'extends' is not a string or Array", "extends.json", [{
299339
code: 5024,
300-
messageText: `Compiler option 'extends' requires a value of type string.`
340+
messageText: `Compiler option 'extends' requires a value of type string or Array.`
301341
}]);
302342

303343
testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", {
@@ -352,5 +392,40 @@ describe("unittests:: config:: configurationExtension", () => {
352392
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
353393
});
354394
});
395+
396+
describe(testName, () => {
397+
it("adds extendedSourceFiles from an array only once", () => {
398+
const sourceFile = ts.readJsonConfigFile("configs/extendsArrayFifth.json", (path) => host.readFile(path));
399+
const dir = ts.combinePaths(basePath, "configs");
400+
const expected = [
401+
ts.combinePaths(dir, "extendsArrayFirst.json"),
402+
ts.combinePaths(dir, "extendsArraySecond.json"),
403+
ts.combinePaths(dir, "extendsArrayThird.json"),
404+
ts.combinePaths(dir, "extendsArrayFourth.json"),
405+
];
406+
ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "extendsArrayFifth.json");
407+
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
408+
ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "extendsArrayFifth.json");
409+
assert.deepEqual(sourceFile.extendedSourceFiles, expected);
410+
});
411+
412+
testSuccess("can overwrite top-level compilerOptions", "configs/extendsArrayFifth.json", {
413+
allowJs: true,
414+
noImplicitAny: false,
415+
strictNullChecks: false,
416+
module: ts.ModuleKind.System
417+
}, []);
418+
419+
testFailure("can report missing configurations", "extendsArrayFails.json", [{
420+
code: 6053,
421+
messageText: `File './missingFile' not found.`
422+
}]);
423+
424+
testFailure("can error when 'extends' is not a string or Array2", "extendsArrayFails2.json", [{
425+
code: 5024,
426+
messageText: `Compiler option 'extends' requires a value of type string.`
427+
}]);
428+
});
355429
});
356430
});
431+

src/testRunner/unittests/config/showConfig.ts

+4
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ describe("unittests:: config:: showConfig", () => {
147147
}
148148
break;
149149
}
150+
case "listOrElement": {
151+
ts.Debug.fail();
152+
break;
153+
}
150154
case "string": {
151155
if (option.isTSConfigOnly) {
152156
args = ["-p", "tsconfig.json"];

src/testRunner/unittests/tsbuildWatch/programUpdates.ts

+72-3
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ export function someFn() { }`),
597597
verifyTscWatch({
598598
scenario: "programUpdates",
599599
subScenario: "works with extended source files",
600-
commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json"],
600+
commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json", "project3.tsconfig.json"],
601601
sys: () => {
602602
const alphaExtendedConfigFile: File = {
603603
path: "/a/b/alpha.tsconfig.json",
@@ -633,10 +633,49 @@ export function someFn() { }`),
633633
files: [otherFile.path]
634634
})
635635
};
636+
const otherFile2: File = {
637+
path: "/a/b/other2.ts",
638+
content: "let k = 0;",
639+
};
640+
const extendsConfigFile1: File = {
641+
path: "/a/b/extendsConfig1.tsconfig.json",
642+
content: JSON.stringify({
643+
compilerOptions: {
644+
composite: true,
645+
}
646+
})
647+
};
648+
const extendsConfigFile2: File = {
649+
path: "/a/b/extendsConfig2.tsconfig.json",
650+
content: JSON.stringify({
651+
compilerOptions: {
652+
strictNullChecks: false,
653+
}
654+
})
655+
};
656+
const extendsConfigFile3: File = {
657+
path: "/a/b/extendsConfig3.tsconfig.json",
658+
content: JSON.stringify({
659+
compilerOptions: {
660+
noImplicitAny: true,
661+
}
662+
})
663+
};
664+
const project3Config: File = {
665+
path: "/a/b/project3.tsconfig.json",
666+
content: JSON.stringify({
667+
extends: ["./extendsConfig1.tsconfig.json", "./extendsConfig2.tsconfig.json", "./extendsConfig3.tsconfig.json"],
668+
compilerOptions: {
669+
composite: false,
670+
},
671+
files: [otherFile2.path]
672+
})
673+
};
636674
return createWatchedSystem([
637675
libFile,
638676
alphaExtendedConfigFile, project1Config, commonFile1, commonFile2,
639-
bravoExtendedConfigFile, project2Config, otherFile
677+
bravoExtendedConfigFile, project2Config, otherFile, otherFile2,
678+
extendsConfigFile1, extendsConfigFile2, extendsConfigFile3, project3Config
640679
], { currentDirectory: "/a/b" });
641680
},
642681
edits: [
@@ -689,7 +728,37 @@ export function someFn() { }`),
689728
sys.checkTimeoutQueueLength(0);
690729
},
691730
},
692-
]
731+
{
732+
caption: "Modify extendsConfigFile2",
733+
edit: sys => sys.writeFile("/a/b/extendsConfig2.tsconfig.json", JSON.stringify({
734+
compilerOptions: { strictNullChecks: true }
735+
})),
736+
timeouts: sys => { // Build project3
737+
sys.checkTimeoutQueueLengthAndRun(1);
738+
sys.checkTimeoutQueueLength(0);
739+
},
740+
},
741+
{
742+
caption: "Modify project 3",
743+
edit: sys => sys.writeFile("/a/b/project3.tsconfig.json", JSON.stringify({
744+
extends: ["./extendsConfig1.tsconfig.json", "./extendsConfig2.tsconfig.json"],
745+
compilerOptions: { composite: false },
746+
files: ["/a/b/other2.ts"]
747+
})),
748+
timeouts: sys => { // Build project3
749+
sys.checkTimeoutQueueLengthAndRun(1);
750+
sys.checkTimeoutQueueLength(0);
751+
},
752+
},
753+
{
754+
caption: "Delete extendedConfigFile2 and report error",
755+
edit: sys => sys.deleteFile("./extendsConfig2.tsconfig.json"),
756+
timeouts: sys => { // Build project3
757+
sys.checkTimeoutQueueLengthAndRun(1);
758+
sys.checkTimeoutQueueLength(0);
759+
},
760+
}
761+
],
693762
});
694763

695764
verifyTscWatch({

tests/baselines/reference/api/tsserverlibrary.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9170,7 +9170,7 @@ declare namespace ts {
91709170
/**
91719171
* Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet
91729172
*/
9173-
extendedConfigPath?: string;
9173+
extendedConfigPath?: string | string[];
91749174
}
91759175
interface ExtendedConfigCacheEntry {
91769176
extendedResult: TsConfigSourceFile;

tests/baselines/reference/api/typescript.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5236,7 +5236,7 @@ declare namespace ts {
52365236
/**
52375237
* Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet
52385238
*/
5239-
extendedConfigPath?: string;
5239+
extendedConfigPath?: string | string[];
52405240
}
52415241
interface ExtendedConfigCacheEntry {
52425242
extendedResult: TsConfigSourceFile;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/index.ts(1,12): error TS7006: Parameter 'x' implicitly has an 'any' type.
2+
/index.ts(3,1): error TS2454: Variable 'y' is used before being assigned.
3+
4+
5+
==== /tsconfig.json (0 errors) ====
6+
{
7+
"extends": ["./tsconfig1.json", "./tsconfig2.json"]
8+
}
9+
10+
==== /tsconfig1.json (0 errors) ====
11+
{
12+
"compilerOptions": {
13+
"strictNullChecks": true
14+
}
15+
}
16+
17+
==== /tsconfig2.json (0 errors) ====
18+
{
19+
"compilerOptions": {
20+
"noImplicitAny": true
21+
}
22+
}
23+
24+
==== /index.ts (2 errors) ====
25+
function f(x) { } // noImplicitAny error
26+
~
27+
!!! error TS7006: Parameter 'x' implicitly has an 'any' type.
28+
let y: string;
29+
y.toLowerCase(); // strictNullChecks error
30+
~
31+
!!! error TS2454: Variable 'y' is used before being assigned.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [tests/cases/compiler/configFileExtendsAsList.ts] ////
2+
3+
//// [tsconfig1.json]
4+
{
5+
"compilerOptions": {
6+
"strictNullChecks": true
7+
}
8+
}
9+
10+
//// [tsconfig2.json]
11+
{
12+
"compilerOptions": {
13+
"noImplicitAny": true
14+
}
15+
}
16+
17+
//// [index.ts]
18+
function f(x) { } // noImplicitAny error
19+
let y: string;
20+
y.toLowerCase(); // strictNullChecks error
21+
22+
//// [index.js]
23+
function f(x) { } // noImplicitAny error
24+
var y;
25+
y.toLowerCase(); // strictNullChecks error
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
=== /index.ts ===
2+
function f(x) { } // noImplicitAny error
3+
>f : Symbol(f, Decl(index.ts, 0, 0))
4+
>x : Symbol(x, Decl(index.ts, 0, 11))
5+
6+
let y: string;
7+
>y : Symbol(y, Decl(index.ts, 1, 3))
8+
9+
y.toLowerCase(); // strictNullChecks error
10+
>y.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
11+
>y : Symbol(y, Decl(index.ts, 1, 3))
12+
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
=== /index.ts ===
2+
function f(x) { } // noImplicitAny error
3+
>f : (x: any) => void
4+
>x : any
5+
6+
let y: string;
7+
>y : string
8+
9+
y.toLowerCase(); // strictNullChecks error
10+
>y.toLowerCase() : string
11+
>y.toLowerCase : () => string
12+
>y : string
13+
>toLowerCase : () => string
14+

0 commit comments

Comments
 (0)