From 7a8238d88d64938972d46359a057305a8a2f2c16 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Mar 2023 12:00:08 -0700 Subject: [PATCH] Refactor config file parsing such that json and jsonSourceFile api use same paths (#53331) --- src/compiler/commandLineParser.ts | 513 ++++++++---------- src/compiler/parser.ts | 4 +- src/compiler/types.ts | 1 + ...ent in tsconfig with jsonSourceFile api.js | 14 +- 4 files changed, 234 insertions(+), 298 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index ab3909d8e23..556be8387a9 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -97,6 +97,7 @@ import { PollingWatchKind, PrefixUnaryExpression, ProjectReference, + PropertyAssignment, PropertyName, removeTrailingDirectorySeparator, returnTrue, @@ -1746,17 +1747,18 @@ function getOptionName(option: CommandLineOption) { function createUnknownOptionError( unknownOption: string, diagnostics: DidYouMeanOptionsDiagnostics, - createDiagnostics: (message: DiagnosticMessage, arg0: string, arg1?: string) => Diagnostic, - unknownOptionErrorText?: string + unknownOptionErrorText?: string, + node?: PropertyName, + sourceFile?: TsConfigSourceFile, ) { if (diagnostics.alternateMode?.getOptionsNameMap().optionsNameMap.has(unknownOption.toLowerCase())) { - return createDiagnostics(diagnostics.alternateMode.diagnostic, unknownOption); + return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.alternateMode.diagnostic, unknownOption); } const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); return possibleOption ? - createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : - createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); + createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : + createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); } /** @internal */ @@ -1797,7 +1799,7 @@ export function parseCommandLineWorker( i = parseOptionValue(args, i, watchOptionsDidYouMeanDiagnostics, watchOpt, watchOptions || (watchOptions = {}), errors); } else { - errors.push(createUnknownOptionError(inputOptionName, diagnostics, createCompilerDiagnostic, s)); + errors.push(createUnknownOptionError(inputOptionName, diagnostics, s)); } } } @@ -2083,7 +2085,7 @@ export function readConfigFile(fileName: string, readFile: (path: string) => str export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { const jsonSourceFile = parseJsonText(fileName, jsonText); return { - config: convertConfigFileToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics, /*reportOptionsErrors*/ false, /*optionsIterator*/ undefined), + config: convertConfigFileToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics, /*jsonConversionNotifier*/ undefined), error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : undefined }; } @@ -2152,6 +2154,25 @@ const extendsOptionDeclaration: CommandLineOptionOfListType = { type: "string" }, category: Diagnostics.File_Management, + disallowNullOrUndefined: true, +}; +const compilerOptionsDeclaration: TsConfigOnlyOption = { + name: "compilerOptions", + type: "object", + elementOptions: getCommandLineCompilerOptionsMap(), + extraKeyDiagnostics: compilerOptionsDidYouMeanDiagnostics, +}; +const watchOptionsDeclaration: TsConfigOnlyOption = { + name: "watchOptions", + type: "object", + elementOptions: getCommandLineWatchOptionsMap(), + extraKeyDiagnostics: watchOptionsDidYouMeanDiagnostics, +}; +const typeAcquisitionDeclaration: TsConfigOnlyOption = { + name: "typeAcquisition", + type: "object", + elementOptions: getCommandLineTypeAcquisitionMap(), + extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics }; let _tsconfigRootOptions: TsConfigOnlyOption; function getTsconfigRootOptionsMap() { @@ -2160,24 +2181,9 @@ function getTsconfigRootOptionsMap() { name: undefined!, // should never be needed since this is root type: "object", elementOptions: commandLineOptionsToMap([ - { - name: "compilerOptions", - type: "object", - elementOptions: getCommandLineCompilerOptionsMap(), - extraKeyDiagnostics: compilerOptionsDidYouMeanDiagnostics, - }, - { - name: "watchOptions", - type: "object", - elementOptions: getCommandLineWatchOptionsMap(), - extraKeyDiagnostics: watchOptionsDidYouMeanDiagnostics, - }, - { - name: "typeAcquisition", - type: "object", - elementOptions: getCommandLineTypeAcquisitionMap(), - extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics - }, + compilerOptionsDeclaration, + watchOptionsDeclaration, + typeAcquisitionDeclaration, extendsOptionDeclaration, { name: "references", @@ -2226,36 +2232,22 @@ function getTsconfigRootOptionsMap() { /** @internal */ export interface JsonConversionNotifier { - /** - * Notifies parent option object is being set with the optionKey and a valid optionValue - * Currently it notifies only if there is element with type object (parentOption) and - * has element's option declarations map associated with it - * @param parentOption parent option name in which the option and value are being set - * @param option option declaration which is being set with the value - * @param value value of the option - */ - onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue): void; - /** - * Notify when valid root key value option is being set - * @param key option key - * @param keyNode node corresponding to node in the source file - * @param value computed value of the key - * @param ValueNode node corresponding to value in the source file - */ - onSetValidOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; - /** - * Notify when unknown root key value option is being set - * @param key option key - * @param keyNode node corresponding to node in the source file - * @param value computed value of the key - * @param ValueNode node corresponding to value in the source file - */ - onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; + rootOptions: TsConfigOnlyOption; + onPropertySet( + keyText: string, + value: any, + propertyAssignment: PropertyAssignment, + parentOption: TsConfigOnlyOption | undefined, + option: CommandLineOption | undefined, + ): void; } -function convertConfigFileToObject(sourceFile: JsonSourceFile, errors: Diagnostic[], reportOptionsErrors: boolean, optionsIterator: JsonConversionNotifier | undefined): any { +function convertConfigFileToObject( + sourceFile: JsonSourceFile, + errors: Diagnostic[], + jsonConversionNotifier: JsonConversionNotifier | undefined, +): any { const rootExpression: Expression | undefined = sourceFile.statements[0]?.expression; - const knownRootOptions = reportOptionsErrors ? getTsconfigRootOptionsMap() : undefined; if (rootExpression && rootExpression.kind !== SyntaxKind.ObjectLiteralExpression) { errors.push(createDiagnosticForNodeInSourceFile( sourceFile, @@ -2269,19 +2261,19 @@ function convertConfigFileToObject(sourceFile: JsonSourceFile, errors: Diagnosti if (isArrayLiteralExpression(rootExpression)) { const firstObject = find(rootExpression.elements, isObjectLiteralExpression); if (firstObject) { - return convertToObjectWorker(sourceFile, firstObject, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); + return convertToJson(sourceFile, firstObject, errors, /*returnValue*/ true, jsonConversionNotifier); } } return {}; } - return convertToObjectWorker(sourceFile, rootExpression, errors, /*returnValue*/ true, knownRootOptions, optionsIterator); + return convertToJson(sourceFile, rootExpression, errors, /*returnValue*/ true, jsonConversionNotifier); } /** * Convert the json syntax tree into the json value */ export function convertToObject(sourceFile: JsonSourceFile, errors: Diagnostic[]): any { - return convertToObjectWorker(sourceFile, sourceFile.statements[0]?.expression, errors, /*returnValue*/ true, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); + return convertToJson(sourceFile, sourceFile.statements[0]?.expression, errors, /*returnValue*/ true, /*jsonConversionNotifier*/ undefined); } /** @@ -2291,28 +2283,22 @@ export function convertToObject(sourceFile: JsonSourceFile, errors: Diagnostic[] * * @internal */ -export function convertToObjectWorker( +export function convertToJson( sourceFile: JsonSourceFile, rootExpression: Expression | undefined, errors: Diagnostic[], returnValue: boolean, - knownRootOptions: CommandLineOption | undefined, - jsonConversionNotifier: JsonConversionNotifier | undefined): any { + jsonConversionNotifier: JsonConversionNotifier | undefined, +): any { if (!rootExpression) { return returnValue ? {} : undefined; } - return convertPropertyValueToJson(rootExpression, knownRootOptions); - - function isRootOptionMap(knownOptions: Map | undefined) { - return knownRootOptions && (knownRootOptions as TsConfigOnlyOption).elementOptions === knownOptions; - } + return convertPropertyValueToJson(rootExpression, jsonConversionNotifier?.rootOptions); function convertObjectLiteralExpressionToJson( node: ObjectLiteralExpression, - knownOptions: Map | undefined, - extraKeyDiagnostics: DidYouMeanOptionsDiagnostics | undefined, - parentOption: string | undefined + objectOption: TsConfigOnlyOption | undefined, ): any { const result: any = returnValue ? {} : undefined; for (const element of node.properties) { @@ -2330,46 +2316,15 @@ export function convertToObjectWorker( const textOfKey = isComputedNonLiteralName(element.name) ? undefined : getTextOfPropertyName(element.name); const keyText = textOfKey && unescapeLeadingUnderscores(textOfKey); - const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; - if (keyText && extraKeyDiagnostics && !option) { - if (knownOptions) { - errors.push(createUnknownOptionError( - keyText, - extraKeyDiagnostics, - (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1) - )); - } - else { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); - } - } + const option = keyText ? objectOption?.elementOptions?.get(keyText) : undefined; const value = convertPropertyValueToJson(element.initializer, option); if (typeof keyText !== "undefined") { if (returnValue) { result[keyText] = value; } + // Notify key value set, if user asked for it - if (jsonConversionNotifier && - // Current callbacks are only on known parent option or if we are setting values in the root - (parentOption || isRootOptionMap(knownOptions))) { - const isValidOptionValue = isCompilerOptionsValue(option, value); - if (parentOption) { - if (isValidOptionValue) { - // Notify option set in the parent if its a valid option value - jsonConversionNotifier.onSetValidOptionKeyValueInParent(parentOption, option!, value); - } - } - else if (isRootOptionMap(knownOptions)) { - if (isValidOptionValue) { - // Notify about the valid root key value being set - jsonConversionNotifier.onSetValidOptionKeyValueInRoot(keyText, element.name, value, element.initializer); - } - else if (!option) { - // Notify about the unknown root key value being set - jsonConversionNotifier.onSetUnknownOptionKeyValueInRoot(keyText, element.name, value, element.initializer); - } - } - } + jsonConversionNotifier?.onPropertySet(keyText, value, element, objectOption, option); } } return result; @@ -2389,57 +2344,32 @@ export function convertToObjectWorker( } function convertPropertyValueToJson(valueExpression: Expression, option: CommandLineOption | undefined): any { - let invalidReported: boolean | undefined; switch (valueExpression.kind) { case SyntaxKind.TrueKeyword: - reportInvalidOptionValue(option && option.type !== "boolean" && (option.type !== "listOrElement" || option.element.type !== "boolean")); - return validateValue(/*value*/ true); + return true; case SyntaxKind.FalseKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"&& (option.type !== "listOrElement" || option.element.type !== "boolean")); - return validateValue(/*value*/ false); + return false; case SyntaxKind.NullKeyword: - reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for - return validateValue(/*value*/ null); // eslint-disable-line no-null/no-null + return null; // eslint-disable-line no-null/no-null case SyntaxKind.StringLiteral: if (!isDoubleQuotedString(valueExpression)) { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected)); } - 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 - if (!customOption.type.has(text.toLowerCase())) { - errors.push( - createDiagnosticForInvalidCustomType( - customOption, - (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, message, arg0, arg1) - ) - ); - invalidReported = true; - } - } - return validateValue(text); + return (valueExpression as StringLiteral).text; case SyntaxKind.NumericLiteral: - reportInvalidOptionValue(option && option.type !== "number" && (option.type !== "listOrElement" || option.element.type !== "number")); - return validateValue(Number((valueExpression as NumericLiteral).text)); + return 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" && (option.type !== "listOrElement" || option.element.type !== "number")); - return validateValue(-Number(((valueExpression as PrefixUnaryExpression).operand as NumericLiteral).text)); + return -Number(((valueExpression as PrefixUnaryExpression).operand as NumericLiteral).text); case SyntaxKind.ObjectLiteralExpression: - 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" @@ -2448,51 +2378,23 @@ export function convertToObjectWorker( // that satifies it and need it to modify options set in them (for normalizing file paths) // vs what we set in the json // If need arises, we can modify this interface and callbacks as needed - if (option) { - const { elementOptions, extraKeyDiagnostics, name: optionName } = option as TsConfigOnlyOption; - return validateValue(convertObjectLiteralExpressionToJson(objectLiteralExpression, - elementOptions, extraKeyDiagnostics, optionName)); - } - else { - return validateValue(convertObjectLiteralExpressionToJson( - objectLiteralExpression, /* knownOptions*/ undefined, - /*extraKeyDiagnosticMessage */ undefined, /*parentOption*/ undefined)); - } + return convertObjectLiteralExpressionToJson(objectLiteralExpression, option as TsConfigOnlyOption); case SyntaxKind.ArrayLiteralExpression: - reportInvalidOptionValue(option && option.type !== "list" && option.type !== "listOrElement"); - return validateValue(convertArrayLiteralExpressionToJson( + return convertArrayLiteralExpressionToJson( (valueExpression as ArrayLiteralExpression).elements, - option && (option as CommandLineOptionOfListType).element)); + option && (option as CommandLineOptionOfListType).element); } // Not in expected format if (option) { - reportInvalidOptionValue(/*isError*/ true); + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option.name, getCompilerOptionValueTypeString(option))); } else { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)); } return undefined; - - function validateValue(value: CompilerOptionsValue) { - if (!invalidReported) { - const diagnostic = option?.extraValidation?.(value); - if (diagnostic) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, ...diagnostic)); - return undefined; - } - } - return value; - } - - function reportInvalidOptionValue(isError: boolean | undefined) { - if (isError) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option!.name, getCompilerOptionValueTypeString(option!))); - invalidReported = true; - } - } } function isDoubleQuotedString(node: Node): boolean { @@ -2510,7 +2412,7 @@ function getCompilerOptionValueTypeString(option: CommandLineOption): string { function isCompilerOptionsValue(option: CommandLineOption | undefined, value: any): value is CompilerOptionsValue { if (option) { - if (isNullOrUndefined(value)) return true; // All options are undefinable/nullable + if (isNullOrUndefined(value)) return !option.disallowNullOrUndefined; // All options are undefinable/nullable if (option.type === "list") { return isArray(value); } @@ -2984,9 +2886,7 @@ function parseJsonConfigFileContentWorker( const fileName = configFileName || "tsconfig.json"; const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty; const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer); - const error = nodeValue - ? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName) - : createCompilerDiagnostic(diagnosticMessage, fileName); + const error = createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, nodeValue, diagnosticMessage, fileName); errors.push(error); } else { @@ -3257,31 +3157,56 @@ function parseOwnConfigOfJson( const typeAcquisition = convertTypeAcquisitionFromJsonWorker(json.typeAcquisition, basePath, errors, configFileName); const watchOptions = convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors); json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); - let extendedConfigPath: string | string[] | undefined; + const extendedConfigPath = json.extends || json.extends === "" ? + getExtendsConfigPathOrArray(json.extends, host, basePath, configFileName, errors) : + undefined; + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; +} - if (json.extends || json.extends === "") { - 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; - if (isString(json.extends)) { - extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); +function getExtendsConfigPathOrArray( + value: CompilerOptionsValue, + host: ParseConfigHost, + basePath: string, + configFileName: string | undefined, + errors: Diagnostic[], + valueExpression?: Expression, + sourceFile?: JsonSourceFile, +) { + let extendedConfigPath: string | string[] | undefined; + const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; + if (isString(value)) { + extendedConfigPath = getExtendsConfigPath( + value, + host, + newBase, + errors, + valueExpression, + sourceFile, + ); + } + else if (isArray(value)) { + 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, + (valueExpression as ArrayLiteralExpression | undefined)?.elements[index], + sourceFile, + )); } 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))); - } - } + convertJsonOption(extendsOptionDeclaration.element, value, basePath, errors, (valueExpression as ArrayLiteralExpression | undefined)?.elements[index], sourceFile); } } } - return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; + else { + convertJsonOption(extendsOptionDeclaration, value, basePath, errors, valueExpression, sourceFile); + } + return extendedConfigPath; } function parseOwnConfigOfJsonSourceFile( @@ -3297,68 +3222,12 @@ function parseOwnConfigOfJsonSourceFile( let extendedConfigPath: string | string[] | undefined; let rootCompilerOptions: PropertyName[] | undefined; - const optionsIterator: JsonConversionNotifier = { - onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue) { - let currentOption; - switch (parentOption) { - case "compilerOptions": - currentOption = options; - break; - case "watchOptions": - currentOption = (watchOptions || (watchOptions = {})); - break; - case "typeAcquisition": - currentOption = (typeAcquisition || (typeAcquisition = getDefaultTypeAcquisition(configFileName))); - break; - default: - Debug.fail("Unknown option"); - } - - currentOption[option.name] = normalizeOptionValue(option, basePath, value); - }, - onSetValidOptionKeyValueInRoot(key: string, _keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression) { - switch (key) { - case "extends": - const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; - if (isString(value)) { - extendedConfigPath = getExtendsConfigPath( - value, - host, - newBase, - errors, - (message, arg0) => - createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0) - ); - } - 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) { - if (key === "excludes") { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, keyNode, Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); - } - if (find(commandOptionsWithoutBuild, (opt) => opt.name === key)) { - rootCompilerOptions = append(rootCompilerOptions, keyNode); - } - } - }; - const json = convertConfigFileToObject(sourceFile, errors, /*reportOptionsErrors*/ true, optionsIterator); + const rootOptions = getTsconfigRootOptionsMap(); + const json = convertConfigFileToObject( + sourceFile, + errors, + { rootOptions, onPropertySet }, + ); if (!typeAcquisition) { typeAcquisition = getDefaultTypeAcquisition(configFileName); @@ -3369,6 +3238,54 @@ function parseOwnConfigOfJsonSourceFile( } return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; + + function onPropertySet( + keyText: string, + value: any, + propertyAssignment: PropertyAssignment, + parentOption: TsConfigOnlyOption | undefined, + option: CommandLineOption | undefined, + ) { + // Ensure value is verified except for extends which is handled in its own way for error reporting + if (option && option !== extendsOptionDeclaration) value = convertJsonOption(option, value, basePath, errors, propertyAssignment.initializer, sourceFile); + if (parentOption?.name) { + if (option) { + let currentOption; + if (parentOption === compilerOptionsDeclaration) currentOption = options; + else if (parentOption === watchOptionsDeclaration) currentOption = watchOptions ??= {}; + else if (parentOption === typeAcquisitionDeclaration) currentOption = typeAcquisition ??= getDefaultTypeAcquisition(configFileName); + else Debug.fail("Unknown option"); + currentOption[option.name] = value; + } + else if (keyText && parentOption?.extraKeyDiagnostics) { + if (parentOption.elementOptions) { + errors.push(createUnknownOptionError( + keyText, + parentOption.extraKeyDiagnostics, + /*unknownOptionErrorText*/ undefined, + propertyAssignment.name, + sourceFile, + )); + } + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, propertyAssignment.name, parentOption.extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); + } + } + } + else if (parentOption === rootOptions) { + if (option === extendsOptionDeclaration) { + extendedConfigPath = getExtendsConfigPathOrArray(value, host, basePath, configFileName, errors, propertyAssignment.initializer, sourceFile); + } + else if (!option) { + if (keyText === "excludes") { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, propertyAssignment.name, Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); + } + if (find(commandOptionsWithoutBuild, (opt) => opt.name === keyText)) { + rootCompilerOptions = append(rootCompilerOptions, propertyAssignment.name); + } + } + } + } } function getExtendsConfigPath( @@ -3376,14 +3293,16 @@ function getExtendsConfigPath( host: ParseConfigHost, basePath: string, errors: Diagnostic[], - createDiagnostic: (message: DiagnosticMessage, arg1?: string) => Diagnostic) { + valueExpression: Expression | undefined, + sourceFile: TsConfigSourceFile | undefined, +) { extendedConfig = normalizeSlashes(extendedConfig); if (isRootedDiskPath(extendedConfig) || startsWith(extendedConfig, "./") || startsWith(extendedConfig, "../")) { let extendedConfigPath = getNormalizedAbsolutePath(extendedConfig, basePath); if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { extendedConfigPath = `${extendedConfigPath}.json`; if (!host.fileExists(extendedConfigPath)) { - errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); + errors.push(createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, Diagnostics.File_0_not_found, extendedConfig)); return undefined; } } @@ -3395,10 +3314,10 @@ function getExtendsConfigPath( return resolved.resolvedModule.resolvedFileName; } if (extendedConfig === "") { - errors.push(createDiagnostic(Diagnostics.Compiler_option_0_cannot_be_given_an_empty_string, "extends")); + errors.push(createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, Diagnostics.Compiler_option_0_cannot_be_given_an_empty_string, "extends")); } else { - errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); + errors.push(createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, Diagnostics.File_0_not_found, extendedConfig)); } return undefined; } @@ -3520,51 +3439,48 @@ function convertOptionsFromJson(optionsNameMap: Map, (defaultOptions || (defaultOptions = {}))[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); } else { - errors.push(createUnknownOptionError(id, diagnostics, createCompilerDiagnostic)); + errors.push(createUnknownOptionError(id, diagnostics)); } } return defaultOptions; } +function createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile: TsConfigSourceFile | undefined, node: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number) { + return sourceFile && node ? + createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2, arg3) : + createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); +} + /** @internal */ -export function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Diagnostic[]): CompilerOptionsValue { +export function convertJsonOption( + opt: CommandLineOption, + value: any, + basePath: string, + errors: Diagnostic[], + valueExpression?: Expression, + sourceFile?: TsConfigSourceFile, +): CompilerOptionsValue { if (isCompilerOptionsValue(opt, value)) { const optType = opt.type; if ((optType === "list") && isArray(value)) { - return convertJsonOptionOfListType(opt, value, basePath, errors); + return convertJsonOptionOfListType(opt, value, basePath, errors, valueExpression as ArrayLiteralExpression | undefined, sourceFile); } else if (optType === "listOrElement") { return isArray(value) ? - convertJsonOptionOfListType(opt, value, basePath, errors) : - convertJsonOption(opt.element, value, basePath, errors); + convertJsonOptionOfListType(opt, value, basePath, errors, valueExpression as ArrayLiteralExpression | undefined, sourceFile) : + convertJsonOption(opt.element, value, basePath, errors, valueExpression, sourceFile); } else if (!isString(opt.type)) { - return convertJsonOptionOfCustomType(opt as CommandLineOptionOfCustomType, value as string, errors); + return convertJsonOptionOfCustomType(opt as CommandLineOptionOfCustomType, value as string, errors, valueExpression, sourceFile); } - const validatedValue = validateJsonOptionValue(opt, value, errors); + const validatedValue = validateJsonOptionValue(opt, value, errors, valueExpression, sourceFile); return isNullOrUndefined(validatedValue) ? validatedValue : normalizeNonListOptionValue(opt, basePath, validatedValue); } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); + errors.push(createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); } } -function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { - if (isNullOrUndefined(value)) return undefined; - 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; - } - return value; - } - else if (!isString(option.type)) { - return option.type.get(isString(value) ? value.toLowerCase() : value); - } - return normalizeNonListOptionValue(option, basePath, value); -} - function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { if (option.isFilePath) { value = getNormalizedAbsolutePath(value, basePath); @@ -3575,28 +3491,48 @@ function normalizeNonListOptionValue(option: CommandLineOption, basePath: string return value; } -function validateJsonOptionValue(opt: CommandLineOption, value: T, errors: Diagnostic[]): T | undefined { +function validateJsonOptionValue( + opt: CommandLineOption, + value: T, + errors: Diagnostic[], + valueExpression?: Expression, + sourceFile?: TsConfigSourceFile, +): T | undefined { if (isNullOrUndefined(value)) return undefined; const d = opt.extraValidation?.(value); if (!d) return value; - errors.push(createCompilerDiagnostic(...d)); + errors.push(createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, ...d)); return undefined; } -function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Diagnostic[]) { +function convertJsonOptionOfCustomType( + opt: CommandLineOptionOfCustomType, + value: string, + errors: Diagnostic[], + valueExpression?: Expression, + sourceFile?: TsConfigSourceFile, +) { if (isNullOrUndefined(value)) return undefined; const key = value.toLowerCase(); const val = opt.type.get(key); if (val !== undefined) { - return validateJsonOptionValue(opt, val, errors); + return validateJsonOptionValue(opt, val, errors, valueExpression, sourceFile); } else { - errors.push(createCompilerDiagnosticForInvalidCustomType(opt)); + errors.push(createDiagnosticForInvalidCustomType(opt, (message, arg0, arg1) => + createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, valueExpression, message, arg0, arg1))); } } -function convertJsonOptionOfListType(option: CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: Diagnostic[]): any[] { - return filter(map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => option.listPreserveFalsyValues ? true : !!v); +function convertJsonOptionOfListType( + option: CommandLineOptionOfListType, + values: readonly any[], + basePath: string, + errors: Diagnostic[], + valueExpression: ArrayLiteralExpression | undefined, + sourceFile: TsConfigSourceFile | undefined, +): any[] { + return filter(map(values, (v, index) => convertJsonOption(option.element, v, basePath, errors, valueExpression?.elements[index], sourceFile)), v => option.listPreserveFalsyValues ? true : !!v); } /** @@ -3804,9 +3740,7 @@ function validateSpecs(specs: readonly string[], errors: Diagnostic[], disallowT function createDiagnostic(message: DiagnosticMessage, spec: string): Diagnostic { const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec); - return element ? - createDiagnosticForNodeInSourceFile(jsonSourceFile!, element, message, spec) : - createCompilerDiagnostic(message, spec); + return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element, message, spec); } } @@ -3971,7 +3905,8 @@ export function convertCompilerOptionsForTelemetry(opts: CompilerOptions): Compi return out; } -function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} { +function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} | undefined { + if (value === undefined) return value; switch (option.type) { case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "". return ""; @@ -3986,13 +3921,13 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): // fall through to list case "list": const elementType = option.element; - return isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; + return isArray(value) ? mapDefined(value, v => getOptionValueWithEmptyStrings(v, elementType)) : ""; default: return forEachEntry(option.type, (optionEnumValue, optionStringValue) => { if (optionEnumValue === value) { return optionStringValue; } - })!; // TODO: GH#18217 + }); } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 659fedbfe03..e21c2fa1ff9 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -52,7 +52,7 @@ import { ConstructSignatureDeclaration, containsParseError, ContinueStatement, - convertToObjectWorker, + convertToJson, createDetachedDiagnostic, createNodeFactory, createScanner, @@ -1570,7 +1570,7 @@ namespace Parser { scriptKind = ensureScriptKind(fileName, scriptKind); if (scriptKind === ScriptKind.JSON) { const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes); - convertToObjectWorker(result, result.statements[0]?.expression, result.parseDiagnostics, /*returnValue*/ false, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); + convertToJson(result, result.statements[0]?.expression, result.parseDiagnostics, /*returnValue*/ false, /*jsonConversionNotifier*/ undefined); result.referencedFiles = emptyArray; result.typeReferenceDirectives = emptyArray; result.libReferenceDirectives = emptyArray; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d1e42b19cd4..8ed61e4446b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -7356,6 +7356,7 @@ export interface CommandLineOptionBase { affectsBuildInfo?: true; // true if this options should be emitted in buildInfo transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid + disallowNullOrUndefined?: true; // If set option does not allow setting null } /** @internal */ diff --git a/tests/baselines/reference/config/tsconfigParsing/generates errors for when invalid comment type present in tsconfig with jsonSourceFile api.js b/tests/baselines/reference/config/tsconfigParsing/generates errors for when invalid comment type present in tsconfig with jsonSourceFile api.js index de2ce52a49a..4db167daa21 100644 --- a/tests/baselines/reference/config/tsconfigParsing/generates errors for when invalid comment type present in tsconfig with jsonSourceFile api.js +++ b/tests/baselines/reference/config/tsconfigParsing/generates errors for when invalid comment type present in tsconfig with jsonSourceFile api.js @@ -18,21 +18,17 @@ FileNames:: Errors:: /apath/tsconfig.json:3:17 - error TS1327: String literal with double quotes expected. -3 ## this comment does cause issues -   ~ -/apath/tsconfig.json:3:17 - error TS5023: Unknown compiler option '#'. - 3 ## this comment does cause issues    ~ /apath/tsconfig.json:3:18 - error TS1328: Property value can only be string literal, numeric literal, 'true', 'false', 'null', object literal or array literal. 3 ## this comment does cause issues    ~ -/apath/tsconfig.json:3:20 - error TS1327: String literal with double quotes expected. +/apath/tsconfig.json:3:17 - error TS5023: Unknown compiler option '#'. 3 ## this comment does cause issues -   ~~~~ -/apath/tsconfig.json:3:20 - error TS5023: Unknown compiler option 'this'. +   ~ +/apath/tsconfig.json:3:20 - error TS1327: String literal with double quotes expected. 3 ## this comment does cause issues    ~~~~ @@ -40,6 +36,10 @@ Errors:: 3 ## this comment does cause issues    ~~~~~~~ +/apath/tsconfig.json:3:20 - error TS5023: Unknown compiler option 'this'. + +3 ## this comment does cause issues +   ~~~~ /apath/tsconfig.json:3:33 - error TS1136: Property assignment expected. 3 ## this comment does cause issues