diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index d4f2dc7f31f23..550baef382984 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1161,6 +1161,7 @@ function getBuildInfo(state: BuilderProgramState, bundle: BundleBuildInfo | unde 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) { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index cd354820ad3f7..97a09f716345e 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -103,6 +103,7 @@ import { returnTrue, ScriptTarget, startsWith, + stringContains, StringLiteral, SyntaxKind, sys, @@ -1658,11 +1659,14 @@ export function parseCustomTypeOption(opt: CommandLineOptionOfCustomType, value: } /** @internal */ -export function parseListTypeOption(opt: CommandLineOptionOfListType, value = "", errors: Push): (string | number)[] | undefined { +export function parseListTypeOption(opt: CommandLineOptionOfListType, value = "", errors: Push): string | (string | number)[] | undefined { value = trimString(value); if (startsWith(value, "-")) { return undefined; } + if (opt.type === "listOrElement" && !stringContains(value, ",")) { + return validateJsonOptionValue(opt, value, errors); + } if (value === "") { return []; } @@ -1672,6 +1676,9 @@ export function parseListTypeOption(opt: CommandLineOptionOfListType, value = "" return mapDefined(values, v => validateJsonOptionValue(opt.element, parseInt(v), errors)); case "string": return mapDefined(values, v => validateJsonOptionValue(opt.element, v || "", errors)); + case "boolean": + case "object": + return Debug.fail(`List of ${opt.element.type} is not yet supported.`); default: return mapDefined(values, v => parseCustomTypeOption(opt.element as CommandLineOptionOfCustomType, v, errors)); } @@ -1850,6 +1857,9 @@ function parseOptionValue( i++; } break; + case "listOrElement": + Debug.fail("listOrElement not supported here"); + break; // If not a primitive, the possible types are specified in what is effectively a map of options. default: options[opt.name] = parseCustomTypeOption(opt as CommandLineOptionOfCustomType, args[i], errors); @@ -2090,6 +2100,15 @@ function getCommandLineTypeAcquisitionMap() { return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); } +const extendsOptionDeclaration: CommandLineOptionOfListType = { + name: "extends", + type: "listOrElement", + element: { + name: "extends", + type: "string" + }, + category: Diagnostics.File_Management, +}; let _tsconfigRootOptions: TsConfigOnlyOption; function getTsconfigRootOptionsMap() { if (_tsconfigRootOptions === undefined) { @@ -2121,11 +2140,7 @@ function getTsconfigRootOptionsMap() { elementOptions: getCommandLineTypeAcquisitionMap(), extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics }, - { - name: "extends", - type: "string", - category: Diagnostics.File_Management, - }, + extendsOptionDeclaration, { name: "references", type: "list", @@ -2339,11 +2354,11 @@ export function convertToObjectWorker( let invalidReported: boolean | undefined; switch (valueExpression.kind) { case SyntaxKind.TrueKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); + reportInvalidOptionValue(option && option.type !== "boolean" && (option.type !== "listOrElement" || option.element.type !== "boolean")); return validateValue(/*value*/ true); case SyntaxKind.FalseKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); + reportInvalidOptionValue(option && option.type !== "boolean"&& (option.type !== "listOrElement" || option.element.type !== "boolean")); return validateValue(/*value*/ false); case SyntaxKind.NullKeyword: @@ -2354,8 +2369,11 @@ export function convertToObjectWorker( if (!isDoubleQuotedString(valueExpression)) { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected)); } - reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string")); + reportInvalidOptionValue(option && isString(option.type) && option.type !== "string" && (option.type !== "listOrElement" || (isString(option.element.type) && option.element.type !== "string"))); const text = (valueExpression as StringLiteral).text; + if (option) { + Debug.assert(option.type !== "listOrElement" || option.element.type === "string", "Only string or array of string is handled for now"); + } if (option && !isString(option.type)) { const customOption = option as CommandLineOptionOfCustomType; // Validate custom option type @@ -2372,18 +2390,18 @@ export function convertToObjectWorker( return validateValue(text); case SyntaxKind.NumericLiteral: - reportInvalidOptionValue(option && option.type !== "number"); + reportInvalidOptionValue(option && option.type !== "number" && (option.type !== "listOrElement" || option.element.type !== "number")); return validateValue(Number((valueExpression as NumericLiteral).text)); case SyntaxKind.PrefixUnaryExpression: if ((valueExpression as PrefixUnaryExpression).operator !== SyntaxKind.MinusToken || (valueExpression as PrefixUnaryExpression).operand.kind !== SyntaxKind.NumericLiteral) { break; // not valid JSON syntax } - reportInvalidOptionValue(option && option.type !== "number"); + reportInvalidOptionValue(option && option.type !== "number" && (option.type !== "listOrElement" || option.element.type !== "number")); return validateValue(-Number(((valueExpression as PrefixUnaryExpression).operand as NumericLiteral).text)); case SyntaxKind.ObjectLiteralExpression: - reportInvalidOptionValue(option && option.type !== "object"); + reportInvalidOptionValue(option && option.type !== "object" && (option.type !== "listOrElement" || option.element.type !== "object")); const objectLiteralExpression = valueExpression as ObjectLiteralExpression; // Currently having element option declaration in the tsconfig with type "object" @@ -2404,7 +2422,7 @@ export function convertToObjectWorker( } case SyntaxKind.ArrayLiteralExpression: - reportInvalidOptionValue(option && option.type !== "list"); + reportInvalidOptionValue(option && option.type !== "list" && option.type !== "listOrElement"); return validateValue(convertArrayLiteralExpressionToJson( (valueExpression as ArrayLiteralExpression).elements, option && (option as CommandLineOptionOfListType).element)); @@ -2444,8 +2462,10 @@ export function convertToObjectWorker( } } -function getCompilerOptionValueTypeString(option: CommandLineOption) { - return option.type === "list" ? +function getCompilerOptionValueTypeString(option: CommandLineOption): string { + return (option.type === "listOrElement") ? + `${getCompilerOptionValueTypeString(option.element)} or Array`: + option.type === "list" ? "Array" : isString(option.type) ? option.type : "string"; } @@ -2456,6 +2476,9 @@ function isCompilerOptionsValue(option: CommandLineOption | undefined, value: an if (option.type === "list") { return isArray(value); } + if (option.type === "listOrElement") { + return isArray(value) || isCompilerOptionsValue(option.element, value); + } const expectedType = isString(option.type) ? option.type : "string"; return typeof value === expectedType; } @@ -2560,15 +2583,18 @@ function matchesSpecs(path: string, includeSpecs: readonly string[] | undefined, } function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption): Map | undefined { - if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean" || optionDefinition.type === "object") { - // this is of a type CommandLineOptionOfPrimitiveType - return undefined; - } - else if (optionDefinition.type === "list") { - return getCustomTypeMapOfCommandLineOption(optionDefinition.element); - } - else { - return optionDefinition.type; + switch (optionDefinition.type) { + case "string": + case "number": + case "boolean": + case "object": + // this is of a type CommandLineOptionOfPrimitiveType + return undefined; + case "list": + case "listOrElement": + return getCustomTypeMapOfCommandLineOption(optionDefinition.element); + default: + return optionDefinition.type; } } @@ -2612,6 +2638,7 @@ function serializeOptionBaseObject( const value = options[name] as CompilerOptionsValue; const optionDefinition = optionsNameMap.get(name.toLowerCase()); if (optionDefinition) { + Debug.assert(optionDefinition.type !== "listOrElement"); const customTypeMap = getCustomTypeMapOfCommandLineOption(optionDefinition); if (!customTypeMap) { // There is no map associated with this compiler option then use the value as-is @@ -2805,6 +2832,7 @@ function convertToOptionValueWithAbsolutePaths(option: CommandLineOption | undef else if (option.isFilePath) { return toAbsolutePath(value as string); } + Debug.assert(option.type !== "listOrElement"); } return value; } @@ -3076,13 +3104,22 @@ export interface ParsedTsconfig { /** * 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[]; } function isSuccessfulParsedTsconfig(value: ParsedTsconfig) { return !!value.options; } +interface ExtendsResult { + options: CompilerOptions; + watchOptions?: WatchOptions; + include?: string[]; + exclude?: string[]; + files?: string[]; + compileOnSave?: boolean; + extendedSourceFiles?: Set +} /** * This *just* extracts options/include/exclude/files out of a config file. * It does *not* resolve the included files. @@ -3119,34 +3156,52 @@ function parseConfig( if (ownConfig.extendedConfigPath) { // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. resolutionStack = resolutionStack.concat([resolvedPath]); - const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, resolutionStack, errors, extendedConfigCache); + const result: ExtendsResult = { options:{} }; + if (isString(ownConfig.extendedConfigPath)) { + applyExtendedConfig(result, ownConfig.extendedConfigPath); + } + else { + ownConfig.extendedConfigPath.forEach(extendedConfigPath => applyExtendedConfig(result, extendedConfigPath)); + } + if (!ownConfig.raw.include && result.include) ownConfig.raw.include = result.include; + if (!ownConfig.raw.exclude && result.exclude) ownConfig.raw.exclude = result.exclude; + if (!ownConfig.raw.files && result.files) ownConfig.raw.files = result.files; + if (ownConfig.raw.compileOnSave === undefined && result.compileOnSave) ownConfig.raw.compileOnSave = result.compileOnSave; + if (sourceFile && result.extendedSourceFiles) sourceFile.extendedSourceFiles = arrayFrom(result.extendedSourceFiles.keys()); + + ownConfig.options = assign(result.options, ownConfig.options); + ownConfig.watchOptions = ownConfig.watchOptions && result.watchOptions ? + assign(result.watchOptions, ownConfig.watchOptions) : + ownConfig.watchOptions || result.watchOptions; + } + return ownConfig; + + function applyExtendedConfig(result: ExtendsResult, extendedConfigPath: string){ + const extendedConfig = getExtendedConfig(sourceFile, extendedConfigPath, host, resolutionStack, errors, extendedConfigCache, result); if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { - const baseRaw = extendedConfig.raw; - const raw = ownConfig.raw; - let relativeDifference: string | undefined; - const setPropertyInRawIfNotUndefined = (propertyName: string) => { - if (!raw[propertyName] && baseRaw[propertyName]) { - raw[propertyName] = map(baseRaw[propertyName], (path: string) => isRootedDiskPath(path) ? path : combinePaths( - relativeDifference ||= convertToRelativePath(getDirectoryPath(ownConfig.extendedConfigPath!), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)), + const extendsRaw = extendedConfig.raw; + let relativeDifference: string | undefined ; + const setPropertyInResultIfNotUndefined = (propertyName: "include" | "exclude" | "files") => { + if (extendsRaw[propertyName]) { + result[propertyName] = map(extendsRaw[propertyName], (path: string) => isRootedDiskPath(path) ? path : combinePaths( + relativeDifference ||= convertToRelativePath(getDirectoryPath(extendedConfigPath), basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames)), path )); } }; - setPropertyInRawIfNotUndefined("include"); - setPropertyInRawIfNotUndefined("exclude"); - setPropertyInRawIfNotUndefined("files"); - if (raw.compileOnSave === undefined) { - raw.compileOnSave = baseRaw.compileOnSave; + setPropertyInResultIfNotUndefined("include"); + setPropertyInResultIfNotUndefined("exclude"); + setPropertyInResultIfNotUndefined("files"); + if (extendsRaw.compileOnSave !== undefined) { + result.compileOnSave = extendsRaw.compileOnSave; } - ownConfig.options = assign({}, extendedConfig.options, ownConfig.options); - ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? - assign({}, extendedConfig.watchOptions, ownConfig.watchOptions) : - ownConfig.watchOptions || extendedConfig.watchOptions; + assign(result.options, extendedConfig.options); + result.watchOptions = result.watchOptions && extendedConfig.watchOptions ? + assign({}, result.watchOptions, extendedConfig.watchOptions) : + result.watchOptions || extendedConfig.watchOptions; // TODO extend type typeAcquisition } } - - return ownConfig; } function parseOwnConfigOfJson( @@ -3166,15 +3221,28 @@ function parseOwnConfigOfJson( const typeAcquisition = convertTypeAcquisitionFromJsonWorker(json.typeAcquisition || json.typingOptions, basePath, errors, configFileName); const watchOptions = convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors); json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); - let extendedConfigPath: string | undefined; + let extendedConfigPath: string | string[] | undefined; if (json.extends) { - if (!isString(json.extends)) { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string")); + if (!isCompilerOptionsValue(extendsOptionDeclaration, json.extends)) { + errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", getCompilerOptionValueTypeString(extendsOptionDeclaration))); } else { const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; - extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); + if (isString(json.extends)) { + extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); + } + else { + extendedConfigPath = []; + for (const fileName of json.extends as unknown[]) { + if (isString(fileName)) { + extendedConfigPath = append(extendedConfigPath, getExtendsConfigPath(fileName, host, newBase, errors, createCompilerDiagnostic)); + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", getCompilerOptionValueTypeString(extendsOptionDeclaration.element))); + } + } + } } } return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; @@ -3190,7 +3258,7 @@ function parseOwnConfigOfJsonSourceFile( const options = getDefaultCompilerOptions(configFileName); let typeAcquisition: TypeAcquisition | undefined, typingOptionstypeAcquisition: TypeAcquisition | undefined; let watchOptions: WatchOptions | undefined; - let extendedConfigPath: string | undefined; + let extendedConfigPath: string | string[] | undefined; let rootCompilerOptions: PropertyName[] | undefined; const optionsIterator: JsonConversionNotifier = { @@ -3219,15 +3287,33 @@ function parseOwnConfigOfJsonSourceFile( switch (key) { case "extends": const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; + if (isString(value)) { extendedConfigPath = getExtendsConfigPath( - value as string, + value, host, newBase, errors, (message, arg0) => createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0) ); - return; + } + else { + extendedConfigPath = []; + for (let index = 0; index < (value as unknown[]).length; index++) { + const fileName = (value as unknown[])[index]; + if (isString(fileName)) { + extendedConfigPath = append(extendedConfigPath, getExtendsConfigPath( + fileName, + host, + newBase, + errors, + (message, arg0) => + createDiagnosticForNodeInSourceFile(sourceFile, (valueNode as ArrayLiteralExpression).elements[index], message, arg0) + )); + } + } + } + return; } }, onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, _value: CompilerOptionsValue, _valueNode: Expression) { @@ -3301,7 +3387,8 @@ function getExtendedConfig( host: ParseConfigHost, resolutionStack: string[], errors: Push, - extendedConfigCache?: Map + extendedConfigCache: Map | undefined, + result: ExtendsResult ): ParsedTsconfig | undefined { const path = host.useCaseSensitiveFileNames ? extendedConfigPath : toFileNameLowerCase(extendedConfigPath); let value: ExtendedConfigCacheEntry | undefined; @@ -3321,9 +3408,11 @@ function getExtendedConfig( } } if (sourceFile) { - sourceFile.extendedSourceFiles = [extendedResult.fileName]; + (result.extendedSourceFiles ??= new Set()).add(extendedResult.fileName); if (extendedResult.extendedSourceFiles) { - sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); + for (const extenedSourceFile of extendedResult.extendedSourceFiles) { + result.extendedSourceFiles.add(extenedSourceFile); + } } } if (extendedResult.parseDiagnostics.length) { @@ -3416,10 +3505,15 @@ function convertOptionsFromJson(optionsNameMap: Map, export function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { if (isCompilerOptionsValue(opt, value)) { const optType = opt.type; - if (optType === "list" && isArray(value)) { + if ((optType === "list") && isArray(value)) { return convertJsonOptionOfListType(opt, value, basePath, errors); } - else if (!isString(optType)) { + else if (optType === "listOrElement") { + return isArray(value) ? + convertJsonOptionOfListType(opt, value, basePath, errors) : + convertJsonOption(opt.element, value, basePath, errors); + } + else if (!isString(opt.type)) { return convertJsonOptionOfCustomType(opt as CommandLineOptionOfCustomType, value as string, errors); } const validatedValue = validateJsonOptionValue(opt, value, errors); @@ -3432,7 +3526,8 @@ export function convertJsonOption(opt: CommandLineOption, value: any, basePath: function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { if (isNullOrUndefined(value)) return undefined; - if (option.type === "list") { + if (option.type === "listOrElement" && !isArray(value)) return normalizeOptionValue(option.element, basePath, value); + else if (option.type === "list" || option.type === "listOrElement") { const listOption = option; if (listOption.element.isFilePath || !isString(listOption.element.type)) { return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => listOption.listPreserveFalsyValues ? true : !!v) as CompilerOptionsValue; @@ -3860,6 +3955,9 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): return typeof value === "number" ? value : ""; case "boolean": return typeof value === "boolean" ? value : ""; + case "listOrElement": + if (!isArray(value)) return getOptionValueWithEmptyStrings(value, option.element); + // fall through to list case "list": const elementType = option.element; return isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; @@ -3873,7 +3971,7 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): } -function getDefaultValueForOption(option: CommandLineOption) { +function getDefaultValueForOption(option: CommandLineOption): {} { switch (option.type) { case "number": return 1; @@ -3884,6 +3982,8 @@ function getDefaultValueForOption(option: CommandLineOption) { return option.isFilePath ? `./${defaultValue && typeof defaultValue === "string" ? defaultValue : ""}` : ""; case "list": return []; + case "listOrElement": + return getDefaultValueForOption(option.element); case "object": return {}; default: diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fcfb940152086..12bf22b8af668 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6977,7 +6977,7 @@ export interface CreateProgramOptions { /** @internal */ export interface CommandLineOptionBase { name: string; - type: "string" | "number" | "boolean" | "object" | "list" | Map; // a value of a primitive type, or an object literal mapping named values to actual values + type: "string" | "number" | "boolean" | "object" | "list" | "listOrElement" | Map; // 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. @@ -7047,7 +7047,7 @@ export interface TsConfigOnlyOption extends CommandLineOptionBase { /** @internal */ export interface CommandLineOptionOfListType extends CommandLineOptionBase { - type: "list"; + type: "list" | "listOrElement"; element: CommandLineOptionOfCustomType | CommandLineOptionOfStringType | CommandLineOptionOfNumberType | CommandLineOptionOfBooleanType | TsConfigOnlyOption; listPreserveFalsyValues?: boolean; } diff --git a/src/executeCommandLine/executeCommandLine.ts b/src/executeCommandLine/executeCommandLine.ts index 58bd58cb02150..0853f9c6916f4 100644 --- a/src/executeCommandLine/executeCommandLine.ts +++ b/src/executeCommandLine/executeCommandLine.ts @@ -253,9 +253,9 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign typeof option.defaultValueDescription === "object" ? getDiagnosticText(option.defaultValueDescription) : formatDefaultValue( - option.defaultValueDescription, - option.type === "list" ? option.element.type : option.type - ); + option.defaultValueDescription, + option.type === "list" || option.type === "listOrElement" ? option.element.type : option.type + ); const terminalWidth = sys.getWidthOfTerminal?.() ?? 0; // Note: child_process might return `terminalWidth` as undefined. @@ -365,6 +365,7 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign }; function getValueType(option: CommandLineOption) { + Debug.assert(option.type !== "listOrElement"); switch (option.type) { case "string": case "number": @@ -386,7 +387,7 @@ function generateOptionOutput(sys: System, option: CommandLineOption, rightAlign possibleValues = option.type; break; case "list": - // TODO: check infinite loop + case "listOrElement": possibleValues = getPossibleValues(option.element); break; case "object": diff --git a/src/harness/harnessIO.ts b/src/harness/harnessIO.ts index 642e4990385b3..4dcfcbca99faa 100644 --- a/src/harness/harnessIO.ts +++ b/src/harness/harnessIO.ts @@ -380,6 +380,7 @@ export namespace Compiler { } // If not a primitive, the possible types are specified in what is effectively a map of options. case "list": + case "listOrElement": return ts.parseListTypeOption(option, value, errors); default: return ts.parseCustomTypeOption(option as ts.CommandLineOptionOfCustomType, value, errors); diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 4d87384d5a4e4..ef3a1107fca0b 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -120,7 +120,7 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change if (foundExactMatch || propertyName !== "include" || !isArrayLiteralExpression(property.initializer)) return; const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); if (includes.length === 0) return; - const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + const matchers = getFileMatcherPatterns(configDir, /*excludes*/[], includes, useCaseSensitiveFileNames, currentDirectory); // If there isn't some include for this, add a new one. if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { @@ -131,6 +131,7 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change 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); } diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index 8434eb350cf60..8677621a6e514 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -186,7 +186,47 @@ function createFileSystem(ignoreCase: boolean, cwd: string, root: string) { "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] }), } } }); @@ -295,9 +335,9 @@ describe("unittests:: config:: configurationExtension", () => { 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", { @@ -352,5 +392,40 @@ describe("unittests:: config:: configurationExtension", () => { assert.deepEqual(sourceFile.extendedSourceFiles, expected); }); }); + + describe(testName, () => { + it("adds extendedSourceFiles from an array only once", () => { + const sourceFile = ts.readJsonConfigFile("configs/extendsArrayFifth.json", (path) => host.readFile(path)); + const dir = ts.combinePaths(basePath, "configs"); + const expected = [ + ts.combinePaths(dir, "extendsArrayFirst.json"), + ts.combinePaths(dir, "extendsArraySecond.json"), + ts.combinePaths(dir, "extendsArrayThird.json"), + ts.combinePaths(dir, "extendsArrayFourth.json"), + ]; + ts.parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "extendsArrayFifth.json"); + assert.deepEqual(sourceFile.extendedSourceFiles, expected); + ts.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: ts.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.` + }]); + }); }); }); + diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index 438bee2f111ee..a815ec7d7750c 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -147,6 +147,10 @@ describe("unittests:: config:: showConfig", () => { } break; } + case "listOrElement": { + ts.Debug.fail(); + break; + } case "string": { if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; diff --git a/src/testRunner/unittests/tsbuildWatch/programUpdates.ts b/src/testRunner/unittests/tsbuildWatch/programUpdates.ts index aa385da701dfa..b4498d1f7e5f9 100644 --- a/src/testRunner/unittests/tsbuildWatch/programUpdates.ts +++ b/src/testRunner/unittests/tsbuildWatch/programUpdates.ts @@ -597,7 +597,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", @@ -633,10 +633,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: [otherFile2.path] + }) + }; return createWatchedSystem([ libFile, alphaExtendedConfigFile, project1Config, commonFile1, commonFile2, - bravoExtendedConfigFile, project2Config, otherFile + bravoExtendedConfigFile, project2Config, otherFile, otherFile2, + extendsConfigFile1, extendsConfigFile2, extendsConfigFile3, project3Config ], { currentDirectory: "/a/b" }); }, edits: [ @@ -689,7 +728,37 @@ export function someFn() { }`), sys.checkTimeoutQueueLength(0); }, }, - ] + { + caption: "Modify extendsConfigFile2", + edit: sys => sys.writeFile("/a/b/extendsConfig2.tsconfig.json", JSON.stringify({ + compilerOptions: { strictNullChecks: true } + })), + timeouts: sys => { // Build project3 + sys.checkTimeoutQueueLengthAndRun(1); + sys.checkTimeoutQueueLength(0); + }, + }, + { + caption: "Modify project 3", + edit: sys => sys.writeFile("/a/b/project3.tsconfig.json", JSON.stringify({ + extends: ["./extendsConfig1.tsconfig.json", "./extendsConfig2.tsconfig.json"], + compilerOptions: { composite: false }, + files: ["/a/b/other2.ts"] + })), + timeouts: sys => { // Build project3 + sys.checkTimeoutQueueLengthAndRun(1); + sys.checkTimeoutQueueLength(0); + }, + }, + { + caption: "Delete extendedConfigFile2 and report error", + edit: sys => sys.deleteFile("./extendsConfig2.tsconfig.json"), + timeouts: sys => { // Build project3 + sys.checkTimeoutQueueLengthAndRun(1); + sys.checkTimeoutQueueLength(0); + }, + } + ], }); verifyTscWatch({ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index be4bf4cb7a4d7..ed1562bcdeac1 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9170,7 +9170,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[]; } interface ExtendedConfigCacheEntry { extendedResult: TsConfigSourceFile; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 8ddf62571c5a9..853c3294bead9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5236,7 +5236,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[]; } interface ExtendedConfigCacheEntry { extendedResult: TsConfigSourceFile; diff --git a/tests/baselines/reference/configFileExtendsAsList.errors.txt b/tests/baselines/reference/configFileExtendsAsList.errors.txt new file mode 100644 index 0000000000000..1accde5ea2632 --- /dev/null +++ b/tests/baselines/reference/configFileExtendsAsList.errors.txt @@ -0,0 +1,31 @@ +/index.ts(1,12): error TS7006: Parameter 'x' implicitly has an 'any' type. +/index.ts(3,1): error TS2454: Variable 'y' is used before being assigned. + + +==== /tsconfig.json (0 errors) ==== + { + "extends": ["./tsconfig1.json", "./tsconfig2.json"] + } + +==== /tsconfig1.json (0 errors) ==== + { + "compilerOptions": { + "strictNullChecks": true + } + } + +==== /tsconfig2.json (0 errors) ==== + { + "compilerOptions": { + "noImplicitAny": true + } + } + +==== /index.ts (2 errors) ==== + function f(x) { } // noImplicitAny error + ~ +!!! error TS7006: Parameter 'x' implicitly has an 'any' type. + let y: string; + y.toLowerCase(); // strictNullChecks error + ~ +!!! error TS2454: Variable 'y' is used before being assigned. \ No newline at end of file diff --git a/tests/baselines/reference/configFileExtendsAsList.js b/tests/baselines/reference/configFileExtendsAsList.js new file mode 100644 index 0000000000000..e805b7a293656 --- /dev/null +++ b/tests/baselines/reference/configFileExtendsAsList.js @@ -0,0 +1,25 @@ +//// [tests/cases/compiler/configFileExtendsAsList.ts] //// + +//// [tsconfig1.json] +{ + "compilerOptions": { + "strictNullChecks": true + } +} + +//// [tsconfig2.json] +{ + "compilerOptions": { + "noImplicitAny": true + } +} + +//// [index.ts] +function f(x) { } // noImplicitAny error +let y: string; +y.toLowerCase(); // strictNullChecks error + +//// [index.js] +function f(x) { } // noImplicitAny error +var y; +y.toLowerCase(); // strictNullChecks error diff --git a/tests/baselines/reference/configFileExtendsAsList.symbols b/tests/baselines/reference/configFileExtendsAsList.symbols new file mode 100644 index 0000000000000..97b24a733694e --- /dev/null +++ b/tests/baselines/reference/configFileExtendsAsList.symbols @@ -0,0 +1,13 @@ +=== /index.ts === +function f(x) { } // noImplicitAny error +>f : Symbol(f, Decl(index.ts, 0, 0)) +>x : Symbol(x, Decl(index.ts, 0, 11)) + +let y: string; +>y : Symbol(y, Decl(index.ts, 1, 3)) + +y.toLowerCase(); // strictNullChecks error +>y.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) +>y : Symbol(y, Decl(index.ts, 1, 3)) +>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) + diff --git a/tests/baselines/reference/configFileExtendsAsList.types b/tests/baselines/reference/configFileExtendsAsList.types new file mode 100644 index 0000000000000..dfa1eadf113be --- /dev/null +++ b/tests/baselines/reference/configFileExtendsAsList.types @@ -0,0 +1,14 @@ +=== /index.ts === +function f(x) { } // noImplicitAny error +>f : (x: any) => void +>x : any + +let y: string; +>y : string + +y.toLowerCase(); // strictNullChecks error +>y.toLowerCase() : string +>y.toLowerCase : () => string +>y : string +>toLowerCase : () => string + diff --git a/tests/baselines/reference/tsbuildWatch/programUpdates/works-with-extended-source-files.js b/tests/baselines/reference/tsbuildWatch/programUpdates/works-with-extended-source-files.js index 14f5b0f8e6420..38aa01cf7af96 100644 --- a/tests/baselines/reference/tsbuildWatch/programUpdates/works-with-extended-source-files.js +++ b/tests/baselines/reference/tsbuildWatch/programUpdates/works-with-extended-source-files.js @@ -33,25 +33,45 @@ let y = 1 //// [/a/b/other.ts] let z = 0; +//// [/a/b/other2.ts] +let k = 0; -/a/lib/tsc.js -b -w -v project1.tsconfig.json project2.tsconfig.json +//// [/a/b/extendsConfig1.tsconfig.json] +{"compilerOptions":{"composite":true}} + +//// [/a/b/extendsConfig2.tsconfig.json] +{"compilerOptions":{"strictNullChecks":false}} + +//// [/a/b/extendsConfig3.tsconfig.json] +{"compilerOptions":{"noImplicitAny":true}} + +//// [/a/b/project3.tsconfig.json] +{"extends":["./extendsConfig1.tsconfig.json","./extendsConfig2.tsconfig.json","./extendsConfig3.tsconfig.json"],"compilerOptions":{"composite":false},"files":["/a/b/other2.ts"]} + + +/a/lib/tsc.js -b -w -v project1.tsconfig.json project2.tsconfig.json project3.tsconfig.json Output:: >> Screen clear -[12:00:25 AM] Starting compilation in watch mode... +[12:00:35 AM] Starting compilation in watch mode... -[12:00:26 AM] Projects in this build: +[12:00:36 AM] Projects in this build: * project1.tsconfig.json - * project2.tsconfig.json + * project2.tsconfig.json + * project3.tsconfig.json + +[12:00:37 AM] Project 'project1.tsconfig.json' is out of date because output file 'project1.tsconfig.tsbuildinfo' does not exist + +[12:00:38 AM] Building project '/a/b/project1.tsconfig.json'... -[12:00:27 AM] Project 'project1.tsconfig.json' is out of date because output file 'project1.tsconfig.tsbuildinfo' does not exist +[12:00:52 AM] Project 'project2.tsconfig.json' is out of date because output file 'project2.tsconfig.tsbuildinfo' does not exist -[12:00:28 AM] Building project '/a/b/project1.tsconfig.json'... +[12:00:53 AM] Building project '/a/b/project2.tsconfig.json'... -[12:00:42 AM] Project 'project2.tsconfig.json' is out of date because output file 'project2.tsconfig.tsbuildinfo' does not exist +[12:01:03 AM] Project 'project3.tsconfig.json' is out of date because output file 'other2.js' does not exist -[12:00:43 AM] Building project '/a/b/project2.tsconfig.json'... +[12:01:04 AM] Building project '/a/b/project3.tsconfig.json'... -[12:00:53 AM] Found 0 errors. Watching for file changes. +[12:01:08 AM] Found 0 errors. Watching for file changes. @@ -88,6 +108,21 @@ Shape signatures in builder refreshed for:: /a/lib/lib.d.ts (used version) /a/b/other.ts (computed .d.ts during emit) +Program root files: ["/a/b/other2.ts"] +Program options: {"composite":false,"strictNullChecks":false,"noImplicitAny":true,"watch":true,"configFilePath":"/a/b/project3.tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/a/b/other2.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/a/b/other2.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/a/b/other2.ts (used version) + PolledWatches:: FsWatches:: @@ -105,6 +140,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: @@ -238,6 +283,10 @@ declare let z: number; "size": 755 } +//// [/a/b/other2.js] +var k = 0; + + Change:: Modify alpha config @@ -248,11 +297,11 @@ Input:: Output:: >> Screen clear -[12:00:57 AM] File change detected. Starting incremental compilation... +[12:01:12 AM] File change detected. Starting incremental compilation... -[12:00:58 AM] Project 'project1.tsconfig.json' is out of date because output 'project1.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' +[12:01:13 AM] Project 'project1.tsconfig.json' is out of date because output 'project1.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' -[12:00:59 AM] Building project '/a/b/project1.tsconfig.json'... +[12:01:14 AM] Building project '/a/b/project1.tsconfig.json'... @@ -288,6 +337,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: @@ -368,11 +427,11 @@ Change:: Build project 2 Input:: Output:: -[12:01:13 AM] Project 'project2.tsconfig.json' is out of date because output 'project2.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' +[12:01:28 AM] Project 'project2.tsconfig.json' is out of date because output 'project2.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' -[12:01:14 AM] Building project '/a/b/project2.tsconfig.json'... +[12:01:29 AM] Building project '/a/b/project2.tsconfig.json'... -[12:01:25 AM] Found 0 errors. Watching for file changes. +[12:01:40 AM] Found 0 errors. Watching for file changes. @@ -406,6 +465,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: @@ -473,13 +542,13 @@ Input:: Output:: >> Screen clear -[12:01:29 AM] File change detected. Starting incremental compilation... +[12:01:44 AM] File change detected. Starting incremental compilation... -[12:01:30 AM] Project 'project2.tsconfig.json' is out of date because output 'project2.tsconfig.tsbuildinfo' is older than input 'bravo.tsconfig.json' +[12:01:45 AM] Project 'project2.tsconfig.json' is out of date because output 'project2.tsconfig.tsbuildinfo' is older than input 'bravo.tsconfig.json' -[12:01:31 AM] Building project '/a/b/project2.tsconfig.json'... +[12:01:46 AM] Building project '/a/b/project2.tsconfig.json'... -[12:01:42 AM] Found 0 errors. Watching for file changes. +[12:01:57 AM] Found 0 errors. Watching for file changes. @@ -513,6 +582,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: @@ -579,17 +658,17 @@ Input:: Output:: >> Screen clear -[12:01:46 AM] File change detected. Starting incremental compilation... +[12:02:01 AM] File change detected. Starting incremental compilation... -[12:01:47 AM] Project 'project2.tsconfig.json' is out of date because output 'commonFile1.js' is older than input 'project2.tsconfig.json' +[12:02:02 AM] Project 'project2.tsconfig.json' is out of date because output 'other2.js' is older than input 'project2.tsconfig.json' -[12:01:48 AM] Building project '/a/b/project2.tsconfig.json'... +[12:02:03 AM] Building project '/a/b/project2.tsconfig.json'... -[12:01:59 AM] Found 0 errors. Watching for file changes. +[12:02:17 AM] Found 0 errors. Watching for file changes. -Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts","/a/b/other.ts"] +Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts","/a/b/other.ts","/a/b/other2.ts"] Program options: {"strict":true,"watch":true,"configFilePath":"/a/b/project2.tsconfig.json"} Program structureReused: Not Program files:: @@ -597,17 +676,20 @@ Program files:: /a/b/commonFile1.ts /a/b/commonFile2.ts /a/b/other.ts +/a/b/other2.ts Semantic diagnostics in builder refreshed for:: /a/lib/lib.d.ts /a/b/commonFile1.ts /a/b/commonFile2.ts /a/b/other.ts +/a/b/other2.ts Shape signatures in builder refreshed for:: /a/b/commonfile1.ts (computed .d.ts) /a/b/commonfile2.ts (computed .d.ts) /a/b/other.ts (computed .d.ts) +/a/b/other2.ts (computed .d.ts) PolledWatches:: @@ -624,6 +706,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: /a/b: @@ -638,6 +730,11 @@ exitCode:: ExitStatus.undefined var z = 0; +//// [/a/b/other2.js] +"use strict"; +var k = 0; + + Change:: update aplha config @@ -648,11 +745,11 @@ Input:: Output:: >> Screen clear -[12:02:04 AM] File change detected. Starting incremental compilation... +[12:02:22 AM] File change detected. Starting incremental compilation... -[12:02:05 AM] Project 'project1.tsconfig.json' is out of date because output 'project1.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' +[12:02:23 AM] Project 'project1.tsconfig.json' is out of date because output 'project1.tsconfig.tsbuildinfo' is older than input 'alpha.tsconfig.json' -[12:02:06 AM] Building project '/a/b/project1.tsconfig.json'... +[12:02:24 AM] Building project '/a/b/project1.tsconfig.json'... @@ -686,6 +783,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: /a/b: @@ -765,15 +872,15 @@ Change:: Build project 2 Input:: Output:: -[12:02:20 AM] Project 'project2.tsconfig.json' is out of date because output 'commonFile1.js' is older than input 'alpha.tsconfig.json' +[12:02:38 AM] Project 'project2.tsconfig.json' is out of date because output 'commonFile1.js' is older than input 'alpha.tsconfig.json' -[12:02:21 AM] Building project '/a/b/project2.tsconfig.json'... +[12:02:39 AM] Building project '/a/b/project2.tsconfig.json'... -[12:02:32 AM] Found 0 errors. Watching for file changes. +[12:02:53 AM] Found 0 errors. Watching for file changes. -Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts","/a/b/other.ts"] +Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts","/a/b/other.ts","/a/b/other2.ts"] Program options: {"watch":true,"configFilePath":"/a/b/project2.tsconfig.json"} Program structureReused: Not Program files:: @@ -781,12 +888,14 @@ Program files:: /a/b/commonFile1.ts /a/b/commonFile2.ts /a/b/other.ts +/a/b/other2.ts Semantic diagnostics in builder refreshed for:: /a/lib/lib.d.ts /a/b/commonFile1.ts /a/b/commonFile2.ts /a/b/other.ts +/a/b/other2.ts No shapes updated in the builder:: @@ -805,6 +914,16 @@ FsWatches:: {} /a/b/other.ts: {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} FsWatchesRecursive:: /a/b: @@ -818,3 +937,189 @@ exitCode:: ExitStatus.undefined var z = 0; +//// [/a/b/other2.js] +var k = 0; + + + +Change:: Modify extendsConfigFile2 + +Input:: +//// [/a/b/extendsConfig2.tsconfig.json] +{"compilerOptions":{"strictNullChecks":true}} + + +Output:: +>> Screen clear +[12:02:57 AM] File change detected. Starting incremental compilation... + +[12:02:58 AM] Project 'project3.tsconfig.json' is out of date because output 'other2.js' is older than input 'extendsConfig2.tsconfig.json' + +[12:02:59 AM] Building project '/a/b/project3.tsconfig.json'... + +[12:03:00 AM] Updating unchanged output timestamps of project '/a/b/project3.tsconfig.json'... + +[12:03:02 AM] Found 0 errors. Watching for file changes. + + + +Program root files: ["/a/b/other2.ts"] +Program options: {"composite":false,"strictNullChecks":true,"noImplicitAny":true,"watch":true,"configFilePath":"/a/b/project3.tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/a/b/other2.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/a/b/other2.ts + +No shapes updated in the builder:: + +PolledWatches:: + +FsWatches:: +/a/b/project1.tsconfig.json: + {} +/a/b/alpha.tsconfig.json: + {} +/a/b/commonfile1.ts: + {} +/a/b/commonfile2.ts: + {} +/a/b/project2.tsconfig.json: + {} +/a/b/other.ts: + {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/extendsconfig3.tsconfig.json: + {} +/a/b/other2.ts: + {} + +FsWatchesRecursive:: +/a/b: + {} + +exitCode:: ExitStatus.undefined + +//// [/a/b/other2.js] file changed its modified time + +Change:: Modify project 3 + +Input:: +//// [/a/b/project3.tsconfig.json] +{"extends":["./extendsConfig1.tsconfig.json","./extendsConfig2.tsconfig.json"],"compilerOptions":{"composite":false},"files":["/a/b/other2.ts"]} + + +Output:: +>> Screen clear +[12:03:06 AM] File change detected. Starting incremental compilation... + +[12:03:07 AM] Project 'project3.tsconfig.json' is out of date because output 'other2.js' is older than input 'project3.tsconfig.json' + +[12:03:08 AM] Building project '/a/b/project3.tsconfig.json'... + +[12:03:09 AM] Updating unchanged output timestamps of project '/a/b/project3.tsconfig.json'... + +[12:03:11 AM] Found 0 errors. Watching for file changes. + + + +Program root files: ["/a/b/other2.ts"] +Program options: {"composite":false,"strictNullChecks":true,"watch":true,"configFilePath":"/a/b/project3.tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/a/b/other2.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/a/b/other2.ts + +No shapes updated in the builder:: + +PolledWatches:: + +FsWatches:: +/a/b/project1.tsconfig.json: + {} +/a/b/alpha.tsconfig.json: + {} +/a/b/commonfile1.ts: + {} +/a/b/commonfile2.ts: + {} +/a/b/project2.tsconfig.json: + {} +/a/b/other.ts: + {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/other2.ts: + {} + +FsWatchesRecursive:: +/a/b: + {} + +exitCode:: ExitStatus.undefined + +//// [/a/b/other2.js] file changed its modified time + +Change:: Delete extendedConfigFile2 and report error + +Input:: +//// [/a/b/extendsConfig2.tsconfig.json] deleted + +Output:: +>> Screen clear +[12:03:13 AM] File change detected. Starting incremental compilation... + +[12:03:14 AM] Project 'project3.tsconfig.json' is up to date because newest input 'other2.ts' is older than output 'other2.js' + +error TS5083: Cannot read file '/a/b/extendsConfig2.tsconfig.json'. + +[12:03:15 AM] Found 1 error. Watching for file changes. + + + +PolledWatches:: + +FsWatches:: +/a/b/project1.tsconfig.json: + {} +/a/b/alpha.tsconfig.json: + {} +/a/b/commonfile1.ts: + {} +/a/b/commonfile2.ts: + {} +/a/b/project2.tsconfig.json: + {} +/a/b/other.ts: + {} +/a/b/project3.tsconfig.json: + {} +/a/b/extendsconfig1.tsconfig.json: + {} +/a/b/extendsconfig2.tsconfig.json: + {} +/a/b/other2.ts: + {} + +FsWatchesRecursive:: +/a/b: + {} + +exitCode:: ExitStatus.undefined + diff --git a/tests/cases/compiler/configFileExtendsAsList.ts b/tests/cases/compiler/configFileExtendsAsList.ts new file mode 100644 index 0000000000000..8b6e1cf971544 --- /dev/null +++ b/tests/cases/compiler/configFileExtendsAsList.ts @@ -0,0 +1,23 @@ +// @Filename: /tsconfig1.json +{ + "compilerOptions": { + "strictNullChecks": true + } +} + +// @Filename: /tsconfig2.json +{ + "compilerOptions": { + "noImplicitAny": true + } +} + +// @Filename: /tsconfig.json +{ + "extends": ["./tsconfig1.json", "./tsconfig2.json"] +} + +// @Filename: /index.ts +function f(x) { } // noImplicitAny error +let y: string; +y.toLowerCase(); // strictNullChecks error \ No newline at end of file