Refactor config file parsing such that json and jsonSourceFile api use same paths (#53331)

This commit is contained in:
Sheetal Nandi
2023-03-20 12:00:08 -07:00
committed by GitHub
parent 3fab5fff34
commit 7a8238d88d
4 changed files with 234 additions and 298 deletions

View File

@@ -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<string, CommandLineOption> | undefined) {
return knownRootOptions && (knownRootOptions as TsConfigOnlyOption).elementOptions === knownOptions;
}
return convertPropertyValueToJson(rootExpression, jsonConversionNotifier?.rootOptions);
function convertObjectLiteralExpressionToJson(
node: ObjectLiteralExpression,
knownOptions: Map<string, CommandLineOption> | 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<string, CommandLineOption>,
(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<T extends CompilerOptionsValue>(opt: CommandLineOption, value: T, errors: Diagnostic[]): T | undefined {
function validateJsonOptionValue<T extends CompilerOptionsValue>(
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
});
}
}

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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