diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 43a1e804e1d..2cc2545b3c4 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -462,7 +462,7 @@ namespace ts { ]; /* @internal */ - export let typingOptionDeclarations: CommandLineOption[] = [ + export const typingOptionDeclarations: CommandLineOption[] = [ { name: "enableAutoDiscovery", type: "boolean", @@ -522,8 +522,12 @@ namespace ts { /* @internal */ export function createCompilerDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType): Diagnostic { + return createDiagnosticForInvalidCustomType(opt, createCompilerDiagnostic); + } + + function createDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType, createDiagnostic: (message: DiagnosticMessage, arg0: string, arg1: string) => Diagnostic): Diagnostic { const namesOfType = Object.keys(opt.type).map(key => `'${key}'`).join(", "); - return createCompilerDiagnostic(Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); + return createDiagnostic(Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); } /* @internal */ @@ -681,13 +685,13 @@ namespace ts { * Read tsconfig.json file * @param fileName The path to the config file */ - export function readConfigFile(fileName: string, readFile: (path: string) => string): { config?: any; errors: Diagnostic[] } { + export function readConfigFile(fileName: string, readFile: (path: string) => string): { config?: any; error?: Diagnostic } { let text = ""; try { text = readFile(fileName); } catch (e) { - return { errors: [createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message)] }; + return { error: createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } return parseConfigFileTextToJson(fileName, text); } @@ -697,20 +701,107 @@ namespace ts { * @param fileName The path to the config file * @param jsonText The text of the config file */ - export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config: any; errors: Diagnostic[] } { + export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { const { node, errors } = parseJsonText(fileName, jsonText); return { config: convertToJson(node, errors), - errors + error: errors.length ? errors[0] : undefined }; } + /** + * Read tsconfig.json file + * @param fileName The path to the config file + */ + export function readConfigFileToJsonNode(fileName: string, readFile: (path: string) => string): ParsedNodeResults { + let text = ""; + try { + text = readFile(fileName); + } + catch (e) { + return { errors: [createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message)] }; + } + return parseJsonText(fileName, text); + } + + const tsconfigRootOptions: CommandLineOption[] = [ + { + name: "compilerOptions", + type: "object", + optionDeclarations: optionDeclarations, + extraKeyDiagnosticMessage: Diagnostics.Unknown_compiler_option_0 + }, + { + name: "typingOptions", + type: "object", + optionDeclarations: typingOptionDeclarations, + extraKeyDiagnosticMessage: Diagnostics.Unknown_typing_option_0 + }, + { + name: "extends", + type: "string" + }, + { + name: "files", + type: "list", + element: { + name: "files", + type: "string" + } + }, + { + name: "include", + type: "list", + element: { + name: "include", + type: "string" + } + }, + { + name: "exclude", + type: "list", + element: { + name: "exclude", + type: "string" + } + }, + compileOnSaveCommandLineOption + ]; + + function commandLineOptionsToMap(options: CommandLineOption[]) { + return arrayToMap(options, option => option.name); + } + + let _tsconfigRootOptionsMap: Map; + function getTsconfigRootOptionsMap() { + if (_tsconfigRootOptionsMap === undefined) { + _tsconfigRootOptionsMap = commandLineOptionsToMap(tsconfigRootOptions); + } + return _tsconfigRootOptionsMap; + } + + interface JsonConversionNotifier { + /** Notifies options object is being set with the optionKey and optionValue is being set */ + onSetOptionKeyValue(optionsObject: string, option: CommandLineOption, value: CompilerOptionsValue): void; + /** Notify when root key value is being set */ + onRootKeyValue(key: string, propertyName: PropertyName, value: CompilerOptionsValue, node: Expression): void; + } + /** * Convert the json syntax tree into the json value * @param jsonNode * @param errors */ - function convertToJson(jsonNode: JsonNode, errors: Diagnostic[]): any { + export function convertToJson(jsonNode: JsonNode, errors: Diagnostic[]): any { + return convertToJsonWorker(jsonNode, errors); + } + + /** + * Convert the json syntax tree into the json value + * @param jsonNode + * @param errors + */ + function convertToJsonWorker(jsonNode: JsonNode, errors: Diagnostic[], knownRootOptions?: Map, optionsIterator?: JsonConversionNotifier): any { if (!jsonNode) { return undefined; } @@ -720,75 +811,125 @@ namespace ts { } const sourceFile = jsonNode.parent; - return convertObjectLiteralExpressionToJson(jsonNode); + return convertObjectLiteralExpressionToJson(jsonNode, knownRootOptions); - function convertObjectLiteralExpressionToJson(node: ObjectLiteralExpression): any { + function convertObjectLiteralExpressionToJson(node: ObjectLiteralExpression, options?: Map, extraKeyDiagnosticMessage?: DiagnosticMessage, optionsObject?: string): any { const result: any = {}; for (const element of node.properties) { - switch (element.kind) { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.SpreadAssignment: - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element, Diagnostics.Property_assignment_expected)); - break; + if (element.kind !== SyntaxKind.PropertyAssignment) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element, Diagnostics.Property_assignment_expected)); + continue; + } - case SyntaxKind.PropertyAssignment: - if (element.questionToken) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, Diagnostics._0_can_only_be_used_in_a_ts_file, "?")); + if (element.questionToken) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, Diagnostics._0_can_only_be_used_in_a_ts_file, "?")); + } + if (!isDoubleQuotedString(element.name)) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, Diagnostics.String_literal_with_double_quotes_expected)); + } + + const keyText = getTextOfPropertyName(element.name); + const option = options ? options[keyText] : undefined; + if (extraKeyDiagnosticMessage && !option) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnosticMessage, keyText)); + } + const value = parseValue(element.initializer, option); + if (typeof keyText !== undefined && typeof value !== undefined) { + result[keyText] = value; + // Notify key value set, if user asked for it + if (optionsIterator && + (optionsObject || options === knownRootOptions)) { + const isValidOptionValue = isCompilerOptionsValue(option, value); + if (optionsObject && isValidOptionValue) { + optionsIterator.onSetOptionKeyValue(optionsObject, option, value); } - if (!isDoubleQuotedString(element.name)) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, Diagnostics.String_literal_with_double_quotes_expected)); - } - const keyText = getTextOfPropertyName(element.name); - const value = parseValue(element.initializer); - if (typeof keyText !== undefined && typeof value !== undefined) { - result[keyText] = value; + if (options === knownRootOptions && (isValidOptionValue || !option)) { + optionsIterator.onRootKeyValue(keyText, element.name, value, element.initializer); } + } + } } return result; } - function convertArrayLiteralExpressionToJson(node: ArrayLiteralExpression): any[] { + function convertArrayLiteralExpressionToJson(elements: NodeArray, option?: CommandLineOption): any[] { const result: any[] = []; - for (const element of node.elements) { - result.push(parseValue(element)); + for (const element of elements) { + result.push(parseValue(element, option)); } return result; } - function parseValue(node: Expression): any { + function parseValue(node: Expression, option: CommandLineOption): any { switch (node.kind) { case SyntaxKind.TrueKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); return true; case SyntaxKind.FalseKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); return false; case SyntaxKind.NullKeyword: + reportInvalidOptionValue(!!option); return null; // tslint:disable-line:no-null-keyword case SyntaxKind.StringLiteral: if (!isDoubleQuotedString(node)) { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_literal_with_double_quotes_expected)); } - return (node).text; + reportInvalidOptionValue(option && (typeof option.type === "string" && option.type !== "string")); + const text = (node).text; + if (option && typeof option.type !== "string") { + const customOption = option; + // Validate custom option type + if (!(text in customOption.type)) { + errors.push( + createDiagnosticForInvalidCustomType( + customOption, + (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1) + ) + ); + } + } + return text; case SyntaxKind.NumericLiteral: + reportInvalidOptionValue(option && option.type !== "number"); return Number((node).text); case SyntaxKind.ObjectLiteralExpression: - return convertObjectLiteralExpressionToJson(node); + reportInvalidOptionValue(option && option.type !== "object"); + const objectOption = option; + const optionDeclarations = option && objectOption.optionDeclarations ? commandLineOptionsToMap(objectOption.optionDeclarations) : undefined; + return convertObjectLiteralExpressionToJson( + node, + optionDeclarations, + option && objectOption.extraKeyDiagnosticMessage, + optionDeclarations && option.name + ); case SyntaxKind.ArrayLiteralExpression: - return convertArrayLiteralExpressionToJson(node); + reportInvalidOptionValue(option && option.type !== "list"); + return convertArrayLiteralExpressionToJson((node).elements, option && (option).element); } // Not in expected format - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_number_object_array_true_false_or_null_expected)); + if (option) { + reportInvalidOptionValue(!!option); + } + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_number_object_array_true_false_or_null_expected)); + } + return undefined; + + function reportInvalidOptionValue(isError: boolean) { + if (isError) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option.name, getCompilerOptionValueTypeString(option))); + } + } } function isDoubleQuotedString(node: Node) { @@ -796,6 +937,22 @@ namespace ts { } } + function getCompilerOptionValueTypeString(option: CommandLineOption) { + return option.type === "list" ? + "Array" : + typeof option.type === "string" ? option.type : "string"; + } + + function isCompilerOptionsValue(option: CommandLineOption, value: any): value is CompilerOptionsValue { + if (option) { + if (option.type === "list") { + return isArray(value); + } + const expectedType = typeof option.type === "string" ? option.type : "string"; + return typeof value === expectedType; + } + } + /** * Generate tsconfig configuration when running command line "--init" * @param options commandlineOptions to be generated into tsconfig.json @@ -891,7 +1048,30 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[]): ParsedCommandLine { + return parseJsonConfigFileContentWorker(json, /*jsonNode*/ undefined, host, basePath, existingOptions, configFileName, resolutionStack); + } + + /** + * Parse the contents of a config file (tsconfig.json). + * @param jsonNode The contents of the config file to parse + * @param host Instance of ParseConfigHost used to enumerate files in folder. + * @param basePath A root directory to resolve relative path entries in the config + * file to. e.g. outDir + */ + export function parseJsonNodeConfigFileContent(jsonNode: JsonNode, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[]): ParsedCommandLine { + return parseJsonConfigFileContentWorker(/*json*/ undefined, jsonNode, host, basePath, existingOptions, configFileName, resolutionStack); + } + + /** + * Parse the contents of a config file (tsconfig.json). + * @param json The contents of the config file to parse + * @param host Instance of ParseConfigHost used to enumerate files in folder. + * @param basePath A root directory to resolve relative path entries in the config + * file to. e.g. outDir + */ + function parseJsonConfigFileContentWorker(json: any, jsonNode: JsonNode, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine { + Debug.assert((json === undefined && jsonNode !== undefined) || (json !== undefined && jsonNode === undefined)); const errors: Diagnostic[] = []; const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); @@ -900,22 +1080,67 @@ namespace ts { options: {}, fileNames: [], typingOptions: {}, - raw: json, - errors: [createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))], + raw: json || convertToJson(jsonNode, errors), + errors: errors.concat(createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))), wildcardDirectories: {} }; } - let options: CompilerOptions = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName); - const typingOptions: TypingOptions = convertTypingOptionsFromJsonWorker(json["typingOptions"], basePath, errors, configFileName); + let options: CompilerOptions; + let typingOptions: TypingOptions; + let compileOnSave: boolean; + let hasExtendsError: boolean, extendedConfigPath: Path; + if (json) { + options = convertCompilerOptionsFromJsonWorker(json["compilerOptions"], basePath, errors, configFileName); + typingOptions = convertTypingOptionsFromJsonWorker(json["typingOptions"], basePath, errors, configFileName); + compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); + } + else { + options = getDefaultCompilerOptions(configFileName); + typingOptions = getDefaultTypingOptions(configFileName); + const optionsIterator: JsonConversionNotifier = { + onSetOptionKeyValue(optionsObject: string, option: CommandLineOption, value: CompilerOptionsValue) { + Debug.assert(optionsObject === "compilerOptions" || optionsObject === "typingOptions"); + const currentOption = optionsObject === "compilerOptions" ? options : typingOptions; + currentOption[option.name] = normalizeOptionValue(option, basePath, value); + }, + onRootKeyValue(key: string, propertyName: PropertyName, value: CompilerOptionsValue, node: Expression) { + switch (key) { + case "extends": + const extendsDiagnostic = getExtendsConfigPath(value, (message, arg0) => + createDiagnosticForNodeInSourceFile(jsonNode.parent, node, message, arg0)); + if ((extendsDiagnostic).messageText) { + errors.push(extendsDiagnostic); + hasExtendsError = true; + } + else { + extendedConfigPath = extendsDiagnostic; + } + return; + case "excludes": + errors.push(createDiagnosticForNodeInSourceFile(jsonNode.parent, propertyName, Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); + return; + case "files": + if ((value).length === 0) { + errors.push(createDiagnosticForNodeInSourceFile(jsonNode.parent, node, Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json")); + } + return; + case "compileOnSave": + compileOnSave = value; + return; + } + } + }; + json = convertToJsonWorker(jsonNode, errors, getTsconfigRootOptionsMap(), optionsIterator); + } if (json["extends"]) { let [include, exclude, files, baseOptions]: [string[], string[], string[], CompilerOptions] = [undefined, undefined, undefined, {}]; - if (typeof json["extends"] === "string") { - [include, exclude, files, baseOptions] = (tryExtendsName(json["extends"]) || [include, exclude, files, baseOptions]); + if (!hasExtendsError && typeof json["extends"] === "string") { + [include, exclude, files, baseOptions] = (tryExtendsName(json["extends"], extendedConfigPath) || [include, exclude, files, baseOptions]); } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string")); + createCompilerDiagnosticForJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"); } if (include && !json["include"]) { json["include"] = include; @@ -933,7 +1158,6 @@ namespace ts { options.configFilePath = configFileName; const { fileNames, wildcardDirectories } = getFileNames(errors); - const compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); return { options, @@ -945,21 +1169,35 @@ namespace ts { compileOnSave }; - function tryExtendsName(extendedConfig: string): [string[], string[], string[], CompilerOptions] { + function getExtendsConfigPath(extendedConfig: string, createDiagnostic: (message: DiagnosticMessage, arg1?: string) => T): T | Path { // If the path isn't a rooted or relative path, don't try to resolve it (we reserve the right to special case module-id like paths in the future) if (!(isRootedDiskPath(extendedConfig) || startsWith(normalizeSlashes(extendedConfig), "./") || startsWith(normalizeSlashes(extendedConfig), "../"))) { - errors.push(createCompilerDiagnostic(Diagnostics.A_path_in_an_extends_option_must_be_relative_or_rooted_but_0_is_not, extendedConfig)); - return; + return createDiagnostic(Diagnostics.A_path_in_an_extends_option_must_be_relative_or_rooted_but_0_is_not, extendedConfig); } let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName); if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) { extendedConfigPath = `${extendedConfigPath}.json` as Path; if (!host.fileExists(extendedConfigPath)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig)); - return; + return createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig); } } - const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path)); + return extendedConfigPath; + } + + function tryExtendsName(extendedConfig: string, extendedConfigPath?: Path): [string[], string[], string[], CompilerOptions] { + if (!extendedConfigPath) { + const result = getExtendsConfigPath(extendedConfig, (message, arg1) => (createCompilerDiagnosticForJson(message, arg1), true)); + if (result === true) { + return; + } + extendedConfigPath = result; + } + + if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) { + extendedConfigPath = `${extendedConfigPath}.json` as Path; + } + + const extendedResult = readConfigFileToJsonNode(extendedConfigPath, path => host.readFile(path)); if (extendedResult.errors.length) { errors.push(...extendedResult.errors); return; @@ -968,11 +1206,11 @@ namespace ts { const relativeDifference = convertToRelativePath(extendedDirname, basePath, getCanonicalFileName); const updatePath: (path: string) => string = path => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path); // Merge configs (copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios) - const result = parseJsonConfigFileContent(extendedResult.config, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath])); + const result = parseJsonNodeConfigFileContent(extendedResult.node, host, extendedDirname, /*existingOptions*/undefined, getBaseFileName(extendedConfigPath), resolutionStack.concat([resolvedPath])); errors.push(...result.errors); const [include, exclude, files] = map(["include", "exclude", "files"], key => { - if (!json[key] && extendedResult.config[key]) { - return map(extendedResult.config[key], updatePath); + if (!json[key] && result.raw[key]) { + return map(result.raw[key], updatePath); } }); return [include, exclude, files, result.options]; @@ -984,11 +1222,11 @@ namespace ts { if (isArray(json["files"])) { fileNames = json["files"]; if (fileNames.length === 0) { - errors.push(createCompilerDiagnostic(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json")); + createCompilerDiagnosticForJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); } } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array")); + createCompilerDiagnosticForJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array"); } } @@ -998,7 +1236,7 @@ namespace ts { includeSpecs = json["include"]; } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array")); + createCompilerDiagnosticForJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array"); } } @@ -1008,11 +1246,11 @@ namespace ts { excludeSpecs = json["exclude"]; } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array")); + createCompilerDiagnosticForJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"); } } else if (hasProperty(json, "excludes")) { - errors.push(createCompilerDiagnostic(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); + createCompilerDiagnosticForJson(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude); } else { // By default, exclude common package folders and the outDir @@ -1028,7 +1266,7 @@ namespace ts { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors); + const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, jsonNode); if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) { errors.push( @@ -1041,6 +1279,12 @@ namespace ts { return result; } + + function createCompilerDiagnosticForJson(message: DiagnosticMessage, arg0?: string, arg1?: string) { + if (!jsonNode) { + errors.push(createCompilerDiagnostic(message, arg0, arg1)); + } + } } export function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Diagnostic[]): boolean { @@ -1066,20 +1310,30 @@ namespace ts { return { options, errors }; } - function convertCompilerOptionsFromJsonWorker(jsonOptions: any, - basePath: string, errors: Diagnostic[], configFileName?: string): CompilerOptions { - + function getDefaultCompilerOptions(configFileName?: string) { const options: CompilerOptions = getBaseFileName(configFileName) === "jsconfig.json" ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true } : {}; + return options; + } + + function convertCompilerOptionsFromJsonWorker(jsonOptions: any, + basePath: string, errors: Diagnostic[], configFileName?: string): CompilerOptions { + + const options = getDefaultCompilerOptions(configFileName); convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_compiler_option_0, errors); return options; } + function getDefaultTypingOptions(configFileName?: string) { + const options: TypingOptions = { enableAutoDiscovery: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; + return options; + } + function convertTypingOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: Diagnostic[], configFileName?: string): TypingOptions { - const options: TypingOptions = { enableAutoDiscovery: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; + const options = getDefaultTypingOptions(configFileName); convertOptionsFromJson(typingOptionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_typing_option_0, errors); return options; } @@ -1091,7 +1345,7 @@ namespace ts { return; } - const optionNameMap = arrayToMap(optionDeclarations, opt => opt.name); + const optionNameMap = commandLineOptionsToMap(optionDeclarations); for (const id in jsonOptions) { if (id in optionNameMap) { @@ -1105,28 +1359,43 @@ namespace ts { } function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Diagnostic[]): CompilerOptionsValue { - const optType = opt.type; - const expectedType = typeof optType === "string" ? optType : "string"; - if (optType === "list" && isArray(value)) { - return convertJsonOptionOfListType(opt, value, basePath, errors); - } - else if (typeof value === expectedType) { - if (typeof optType !== "string") { - return convertJsonOptionOfCustomType(opt, value, errors); + if (isCompilerOptionsValue(opt, value)) { + const optType = opt.type; + if (optType === "list" && isArray(value)) { + return convertJsonOptionOfListType(opt, value, basePath, errors); } - else { - if (opt.isFilePath) { - value = normalizePath(combinePaths(basePath, value)); - if (value === "") { - value = "."; - } - } + else if (typeof optType !== "string") { + return convertJsonOptionOfCustomType(opt, value, errors); + } + return normalizeNonListOptionValue(opt, basePath, value); + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); + } + } + + function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { + if (option.type === "list") { + const listOption = option; + if (listOption.element.isFilePath || typeof listOption.element.type !== "string") { + return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v); } return value; } - else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, expectedType)); + else if (typeof option.type !== "string") { + return option.type[value]; } + return normalizeNonListOptionValue(option, basePath, value); + } + + function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { + if (option.isFilePath) { + value = normalizePath(combinePaths(basePath, value)); + if (value === "") { + value = "."; + } + } + return value; } function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Diagnostic[]) { @@ -1230,7 +1499,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): ExpandResult { + function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], jsonNode: JsonNode): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1249,11 +1518,11 @@ namespace ts { const wildcardFileMap = createMap(); if (include) { - include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false); + include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false, jsonNode, "include"); } if (exclude) { - exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true); + exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true, jsonNode, "exclude"); } // Wildcard directories (provided as part of a wildcard path) are stored in a @@ -1309,17 +1578,17 @@ namespace ts { }; } - function validateSpecs(specs: string[], errors: Diagnostic[], allowTrailingRecursion: boolean) { + function validateSpecs(specs: string[], errors: Diagnostic[], allowTrailingRecursion: boolean, jsonNode: JsonNode, specKey: string) { const validSpecs: string[] = []; for (const spec of specs) { if (!allowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec)); + errors.push(createDiagnostic(Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec)); } else if (invalidMultipleRecursionPatterns.test(spec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, spec)); + errors.push(createDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, spec)); } else if (invalidDotDotAfterRecursiveWildcardPattern.test(spec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec)); + errors.push(createDiagnostic(Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec)); } else { validSpecs.push(spec); @@ -1327,6 +1596,23 @@ namespace ts { } return validSpecs; + + function createDiagnostic(message: DiagnosticMessage, spec: string): Diagnostic { + if (jsonNode && jsonNode.kind === SyntaxKind.ObjectLiteralExpression) { + for (const property of jsonNode.properties) { + if (property.kind === SyntaxKind.PropertyAssignment && getTextOfPropertyName(property.name) === specKey) { + const specsNode = property.initializer; + for (const element of specsNode.elements) { + if (element.kind === SyntaxKind.StringLiteral && (element).text === spec) { + return createDiagnosticForNodeInSourceFile(jsonNode.parent, element, message, spec); + } + } + } + } + + } + return createCompilerDiagnostic(message, spec); + } } /** diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 11b42097926..03a6d0d7cb9 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -448,7 +448,7 @@ namespace ts { return Parser.parseIsolatedEntityName(text, languageVersion); } - export type ParsedNodeResults = { node: T; errors: Diagnostic[] }; + export type ParsedNodeResults = { node?: T; errors: Diagnostic[] }; /** * Parse json text into SyntaxTree and return node and parse errors if any diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index d435b596953..ee653d688ed 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -366,15 +366,14 @@ namespace ts { return; } - const result = parseConfigFileTextToJson(configFileName, cachedConfigFileText); + const result = parseJsonText(configFileName, cachedConfigFileText); reportDiagnostics(result.errors, /* compilerHost */ undefined); - const configObject = result.config; - if (!configObject) { + if (!result.node) { sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } const cwd = sys.getCurrentDirectory(); - const configParseResult = parseJsonConfigFileContent(configObject, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); + const configParseResult = parseJsonNodeConfigFileContent(result.node, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); if (configParseResult.errors.length > 0) { reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 04377709f3b..db8d31e13ef 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3322,6 +3322,8 @@ namespace ts { /* @internal */ export interface TsConfigOnlyOption extends CommandLineOptionBase { type: "object"; + optionDeclarations?: CommandLineOption[]; + extraKeyDiagnosticMessage?: DiagnosticMessage; } /* @internal */ diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 88346557ba7..6be7adcde5f 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1876,13 +1876,13 @@ namespace Harness { for (let i = 0; i < testUnitData.length; i++) { const data = testUnitData[i]; if (ts.getBaseFileName(data.name).toLowerCase() === "tsconfig.json") { - const configJson = ts.parseConfigFileTextToJson(data.name, data.content); - assert.isTrue(configJson.config !== undefined); + const configJson = ts.parseJsonText(data.name, data.content); + assert.isTrue(configJson.node !== undefined); let baseDir = ts.normalizePath(ts.getDirectoryPath(data.name)); if (rootDir) { baseDir = ts.getNormalizedAbsolutePath(baseDir, rootDir); } - tsConfig = ts.parseJsonConfigFileContent(configJson.config, parseConfigHost, baseDir); + tsConfig = ts.parseJsonNodeConfigFileContent(configJson.node, parseConfigHost, baseDir); tsConfig.options.configFilePath = data.name; // delete entry from the list diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 973a7b5dac1..de22ff314fa 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -210,22 +210,21 @@ class ProjectRunner extends RunnerBase { let errors: ts.Diagnostic[]; if (configFileName) { - const result = ts.readConfigFile(configFileName, getSourceFileText); - if (!result.config) { + const result = ts.readConfigFileToJsonNode(configFileName, getSourceFileText); + if (!result.node) { return { moduleKind, errors: result.errors }; } - const configObject = result.config; const configParseHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(), fileExists, readDirectory, readFile }; - const configParseResult = ts.parseJsonConfigFileContent(configObject, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions); + const configParseResult = ts.parseJsonNodeConfigFileContent(result.node, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions); if (configParseResult.errors.length > 0) { return { moduleKind, diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index 9f9bf8589f0..fe9bed3f5ab 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -74,14 +74,14 @@ namespace RWC { const tsconfigFile = ts.forEach(ioLog.filesRead, f => isTsConfigFile(f) ? f : undefined); if (tsconfigFile) { const tsconfigFileContents = getHarnessCompilerInputUnit(tsconfigFile.path); - const parsedTsconfigFileContents = ts.parseConfigFileTextToJson(tsconfigFile.path, tsconfigFileContents.content); + const parsedTsconfigFileContents = ts.parseJsonText(tsconfigFile.path, tsconfigFileContents.content); const configParseHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(), fileExists: Harness.IO.fileExists, readDirectory: Harness.IO.readDirectory, readFile: Harness.IO.readFile }; - const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, configParseHost, ts.getDirectoryPath(tsconfigFile.path)); + const configParseResult = ts.parseJsonNodeConfigFileContent(parsedTsconfigFileContents.node, configParseHost, ts.getDirectoryPath(tsconfigFile.path)); fileNames = configParseResult.fileNames; opts.options = ts.extend(opts.options, configParseResult.options); } diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts index 010d03d6811..4077f54ef3d 100644 --- a/src/harness/unittests/configurationExtension.ts +++ b/src/harness/unittests/configurationExtension.ts @@ -112,23 +112,40 @@ namespace ts { ], ([testName, basePath, host]) => { function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) { it(name, () => { - const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name)); - assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n")); + const {config, error} = ts.readConfigFile(entry, name => host.readFile(name)); + assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry); assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); expected.configFilePath = entry; assert.deepEqual(parsed.options, expected); assert.deepEqual(parsed.fileNames, expectedFiles); }); + + it(name, () => { + const {node, errors} = ts.readConfigFileToJsonNode(entry, name => host.readFile(name)); + assert(node && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n")); + const parsed = ts.parseJsonNodeConfigFileContent(node, host, basePath, {}, entry); + assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); + expected.configFilePath = entry; + assert.deepEqual(parsed.options, expected); + assert.deepEqual(parsed.fileNames, expectedFiles); + }); } - function testFailure(name: string, entry: string, expectedDiagnostics: {code: number, category: DiagnosticCategory, messageText: string}[]) { + function testFailure(name: string, entry: string, expectedDiagnostics: { code: number, category: DiagnosticCategory, messageText: string }[]) { it(name, () => { - const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name)); - assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n")); + const {config, error} = ts.readConfigFile(entry, name => host.readFile(name)); + assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry); verifyDiagnostics(parsed.errors, expectedDiagnostics); }); + + it(name, () => { + const {node, errors} = ts.readConfigFileToJsonNode(entry, name => host.readFile(name)); + assert(node && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n")); + const parsed = ts.parseJsonNodeConfigFileContent(node, host, basePath, {}, entry); + verifyDiagnostics(parsed.errors, expectedDiagnostics); + }); } describe(testName, () => { diff --git a/src/harness/unittests/convertCompilerOptionsFromJson.ts b/src/harness/unittests/convertCompilerOptionsFromJson.ts index f44dc259710..d191e2d2f3b 100644 --- a/src/harness/unittests/convertCompilerOptionsFromJson.ts +++ b/src/harness/unittests/convertCompilerOptionsFromJson.ts @@ -4,6 +4,11 @@ namespace ts { describe("convertCompilerOptionsFromJson", () => { function assertCompilerOptions(json: any, configFileName: string, expectedResult: { compilerOptions: CompilerOptions, errors: Diagnostic[] }) { + assertCompilerOptionsWithJson(json, configFileName, expectedResult); + assertCompilerOptionsWithJsonNode(json, configFileName, expectedResult); + } + + function assertCompilerOptionsWithJson(json: any, configFileName: string, expectedResult: { compilerOptions: CompilerOptions, errors: Diagnostic[] }) { const { options: actualCompilerOptions, errors: actualErrors} = convertCompilerOptionsFromJson(json["compilerOptions"], "/apath/", configFileName); const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); @@ -21,6 +26,33 @@ namespace ts { } } + function assertCompilerOptionsWithJsonNode(json: any, configFileName: string, expectedResult: { compilerOptions: CompilerOptions, errors: Diagnostic[] }) { + const fileText = JSON.stringify(json); + const { node, errors } = parseJsonText(configFileName, fileText); + assert(!errors.length); + assert(!!node); + const host: ParseConfigHost = new Utils.MockParseConfigHost("/apath/", true, []); + const { options: actualCompilerOptions, errors: actualParseErrors } = parseJsonNodeConfigFileContent(node, host, "/apath/", /*existingOptions*/ undefined, configFileName); + expectedResult.compilerOptions["configFilePath"] = configFileName; + + const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); + const expectedCompilerOptions = JSON.stringify(expectedResult.compilerOptions); + assert.equal(parsedCompilerOptions, expectedCompilerOptions); + + const actualErrors = filter(actualParseErrors, error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + const expectedErrors = expectedResult.errors; + assert.isTrue(expectedResult.errors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedResult.errors)}. Actual error: ${JSON.stringify(actualErrors)}.`); + for (let i = 0; i < actualErrors.length; i++) { + const actualError = actualErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); + assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); + assert(actualError.file); + assert(actualError.start); + assert(actualError.length); + } + } + // tsconfig.json tests it("Convert correctly format tsconfig.json to compiler-options ", () => { assertCompilerOptions( diff --git a/src/harness/unittests/convertTypingOptionsFromJson.ts b/src/harness/unittests/convertTypingOptionsFromJson.ts index 439409b24b7..d2924eedf03 100644 --- a/src/harness/unittests/convertTypingOptionsFromJson.ts +++ b/src/harness/unittests/convertTypingOptionsFromJson.ts @@ -4,6 +4,11 @@ namespace ts { describe("convertTypingOptionsFromJson", () => { function assertTypingOptions(json: any, configFileName: string, expectedResult: { typingOptions: TypingOptions, errors: Diagnostic[] }) { + assertTypingOptionsWithJson(json, configFileName, expectedResult); + assertTypingOptionsWithJsonNode(json, configFileName, expectedResult); + } + + function assertTypingOptionsWithJson(json: any, configFileName: string, expectedResult: { typingOptions: TypingOptions, errors: Diagnostic[] }) { const { options: actualTypingOptions, errors: actualErrors } = convertTypingOptionsFromJson(json["typingOptions"], "/apath/", configFileName); const parsedTypingOptions = JSON.stringify(actualTypingOptions); const expectedTypingOptions = JSON.stringify(expectedResult.typingOptions); @@ -19,6 +24,31 @@ namespace ts { } } + function assertTypingOptionsWithJsonNode(json: any, configFileName: string, expectedResult: { typingOptions: TypingOptions, errors: Diagnostic[] }) { + const fileText = JSON.stringify(json); + const { node, errors } = parseJsonText(configFileName, fileText); + assert(!errors.length); + assert(!!node); + const host: ParseConfigHost = new Utils.MockParseConfigHost("/apath/", true, []); + const { typingOptions: actualTypingOptions, errors: actualParseErrors } = parseJsonNodeConfigFileContent(node, host, "/apath/", /*existingOptions*/ undefined, configFileName); + const parsedTypingOptions = JSON.stringify(actualTypingOptions); + const expectedTypingOptions = JSON.stringify(expectedResult.typingOptions); + assert.equal(parsedTypingOptions, expectedTypingOptions); + + const actualErrors = filter(actualParseErrors, error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + const expectedErrors = expectedResult.errors; + assert.isTrue(expectedResult.errors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedResult.errors)}. Actual error: ${JSON.stringify(actualErrors)}.`); + for (let i = 0; i < actualErrors.length; i++) { + const actualError = actualErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); + assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); + assert(actualError.file); + assert(actualError.start); + assert(actualError.length); + } + } + // tsconfig.json it("Convert correctly format tsconfig.json to typing-options ", () => { assertTypingOptions( @@ -154,7 +184,7 @@ namespace ts { }, errors: [ { - category: Diagnostics.Unknown_compiler_option_0.category, + category: Diagnostics.Unknown_typing_option_0.category, code: Diagnostics.Unknown_typing_option_0.code, file: undefined, start: 0, diff --git a/src/harness/unittests/matchFiles.ts b/src/harness/unittests/matchFiles.ts index b514ac142c5..6681be4596c 100644 --- a/src/harness/unittests/matchFiles.ts +++ b/src/harness/unittests/matchFiles.ts @@ -97,6 +97,39 @@ namespace ts { assert.deepEqual(actual.errors, expected.errors); } + function validateMatches(expected: ts.ParsedCommandLine, json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[]) { + { + const jsonText = JSON.stringify(json); + const {node} = parseJsonText(caseInsensitiveTsconfigPath, jsonText); + const actual = ts.parseJsonNodeConfigFileContent(node, host, basePath, existingOptions, configFileName, resolutionStack); + assertParsed(actual, expected); + } + { + const actual = ts.parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack); + expected.errors = map(expected.errors, error => { + return { + category: error.category, + code: error.code, + file: undefined, + length: undefined, + messageText: error.messageText, + start: undefined, + }; + }); + assertParsed(actual, expected); + } + } + + function createDiagnosticForConfigFile(json: any, start: number, length: number, diagnosticMessage: DiagnosticMessage, arg0: string) { + const text = JSON.stringify(json); + const file = { + fileName: caseInsensitiveTsconfigPath, + kind: SyntaxKind.SourceFile, + text + }; + return ts.createFileDiagnostic(file, start, length, diagnosticMessage, arg0); + } + describe("matchFiles", () => { describe("with literal file list", () => { it("without exclusions", () => { @@ -115,8 +148,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("missing files are still present", () => { const json = { @@ -134,8 +166,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("are not removed due to excludes", () => { const json = { @@ -156,8 +187,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); }); @@ -178,8 +208,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with non .ts file extensions are excluded", () => { const json = { @@ -197,8 +226,7 @@ namespace ts { fileNames: [], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("with missing files are excluded", () => { const json = { @@ -216,8 +244,7 @@ namespace ts { fileNames: [], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("with literal excludes", () => { const json = { @@ -237,8 +264,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with wildcard excludes", () => { const json = { @@ -265,8 +291,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with recursive excludes", () => { const json = { @@ -292,8 +317,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with case sensitive exclude", () => { const json = { @@ -312,8 +336,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseSensitiveHost, caseSensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); }); it("with common package folders and no exclusions", () => { const json = { @@ -334,8 +357,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); it("with common package folders and exclusions", () => { const json = { @@ -361,8 +383,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); it("with common package folders and empty exclude", () => { const json = { @@ -387,8 +408,7 @@ namespace ts { ], wildcardDirectories: {}, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); }); @@ -411,8 +431,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.None }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("`*` matches only ts files", () => { const json = { @@ -432,8 +451,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.None }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("`?` matches only a single character", () => { const json = { @@ -452,8 +470,7 @@ namespace ts { "c:/dev/x": ts.WatchDirectoryFlags.None }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with recursive directory", () => { const json = { @@ -474,8 +491,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with multiple recursive directories", () => { const json = { @@ -498,8 +514,7 @@ namespace ts { "c:/dev/z": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("case sensitive", () => { const json = { @@ -517,8 +532,7 @@ namespace ts { "/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseSensitiveHost, caseSensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); }); it("with missing files are excluded", () => { const json = { @@ -537,8 +551,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("always include literal files", () => { const json = { @@ -562,8 +575,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("exclude folders", () => { const json = { @@ -587,8 +599,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with common package folders and no exclusions", () => { const json = { @@ -606,8 +617,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); it("with common package folders and exclusions", () => { const json = { @@ -630,8 +640,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); it("with common package folders and empty exclude", () => { const json = { @@ -653,8 +662,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive }, }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); it("exclude .js files when allowJs=false", () => { const json = { @@ -678,8 +686,7 @@ namespace ts { "c:/dev/js": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("include .js files when allowJs=true", () => { const json = { @@ -703,8 +710,7 @@ namespace ts { "c:/dev/js": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("include explicitly listed .min.js files when allowJs=true", () => { const json = { @@ -728,8 +734,7 @@ namespace ts { "c:/dev/js": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("include paths outside of the project", () => { const json = { @@ -752,8 +757,7 @@ namespace ts { "c:/ext": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("include paths outside of the project using relative paths", () => { const json = { @@ -775,8 +779,7 @@ namespace ts { "c:/ext": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("exclude paths outside of the project using relative paths", () => { const json = { @@ -796,8 +799,7 @@ namespace ts { fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("include files with .. in their name", () => { const json = { @@ -816,8 +818,7 @@ namespace ts { ], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("exclude files with .. in their name", () => { const json = { @@ -838,8 +839,7 @@ namespace ts { "c:/ext": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("with jsx=none, allowJs=false", () => { const json = { @@ -861,8 +861,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); }); it("with jsx=preserve, allowJs=false", () => { const json = { @@ -886,8 +885,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); }); it("with jsx=none, allowJs=true", () => { const json = { @@ -911,8 +909,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); }); it("with jsx=preserve, allowJs=true", () => { const json = { @@ -938,8 +935,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); }); it("exclude .min.js files using wildcards", () => { const json = { @@ -965,8 +961,7 @@ namespace ts { "c:/dev/js": ts.WatchDirectoryFlags.None } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); describe("with trailing recursive directory", () => { it("in includes", () => { @@ -978,15 +973,14 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), + createDiagnosticForConfigFile(json, 12, 4, ts.Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), ts.createCompilerDiagnostic(ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(defaultExcludes)) ], fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("in excludes", () => { const json = { @@ -1006,8 +1000,7 @@ namespace ts { fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); }); describe("with multiple recursive directory patterns", () => { @@ -1020,15 +1013,14 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, "**/x/**/*"), + createDiagnosticForConfigFile(json, 12, 11, ts.Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, "**/x/**/*"), ts.createCompilerDiagnostic(ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(defaultExcludes)) ], fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("in excludes", () => { const json = { @@ -1042,7 +1034,7 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, "**/x/**") + createDiagnosticForConfigFile(json, 34, 9, ts.Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, "**/x/**") ], fileNames: [ "c:/dev/a.ts", @@ -1054,8 +1046,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); }); @@ -1069,15 +1060,14 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), + createDiagnosticForConfigFile(json, 12, 9, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), ts.createCompilerDiagnostic(ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(defaultExcludes)) ], fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("in includes after a subdirectory", () => { @@ -1089,15 +1079,14 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), + createDiagnosticForConfigFile(json, 12, 11, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), ts.createCompilerDiagnostic(ts.Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(defaultExcludes)) ], fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); it("in excludes immediately after", () => { @@ -1112,7 +1101,7 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..") + createDiagnosticForConfigFile(json, 34, 7, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..") ], fileNames: [ "c:/dev/a.ts", @@ -1124,8 +1113,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); it("in excludes after a subdirectory", () => { @@ -1140,7 +1128,7 @@ namespace ts { const expected: ts.ParsedCommandLine = { options: {}, errors: [ - ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..") + createDiagnosticForConfigFile(json, 34, 9, ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..") ], fileNames: [ "c:/dev/a.ts", @@ -1152,8 +1140,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); }); @@ -1170,8 +1157,7 @@ namespace ts { "c:/dev/z": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); }); }); @@ -1195,8 +1181,7 @@ namespace ts { "c:/dev/w": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); describe("that are explicitly included", () => { it("without wildcards", () => { @@ -1215,8 +1200,7 @@ namespace ts { ], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); it("with recursive wildcards that match directories", () => { const json = { @@ -1237,8 +1221,7 @@ namespace ts { "c:/dev": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); it("with recursive wildcards that match nothing", () => { const json = { @@ -1259,8 +1242,7 @@ namespace ts { "c:/dev/x": ts.WatchDirectoryFlags.Recursive } }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); it("with wildcard excludes that implicitly exclude dotted files", () => { const json = { @@ -1280,8 +1262,7 @@ namespace ts { fileNames: [], wildcardDirectories: {} }; - const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); - assertParsed(actual, expected); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath, undefined, caseInsensitiveTsconfigPath); }); }); }); diff --git a/src/harness/unittests/tsconfigParsing.ts b/src/harness/unittests/tsconfigParsing.ts index 9a7ed315d7f..c31608ee2fe 100644 --- a/src/harness/unittests/tsconfigParsing.ts +++ b/src/harness/unittests/tsconfigParsing.ts @@ -3,40 +3,69 @@ namespace ts { describe("parseConfigFileTextToJson", () => { - function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; errors?: Diagnostic[] }) { + function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - if (!expectedConfigObject.errors) { - expectedConfigObject.errors = []; - } assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); } function assertParseError(jsonText: string) { const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); assert.isTrue(undefined === parsed.config); - assert.isTrue(!!parsed.errors.length); + assert.isTrue(undefined !== parsed.error); } function assertParseErrorWithExcludesKeyword(jsonText: string) { - const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "tests/cases/unittests"); - assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && - parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); + { + const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); + const parsedCommand = ts.parseJsonConfigFileContent(parsed.config, ts.sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); + } + { + const parsed = ts.parseJsonText("/apath/tsconfig.json", jsonText); + const parsedCommand = ts.parseJsonNodeConfigFileContent(parsed.node, ts.sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === ts.Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); + } + } + + function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = ts.parseConfigFileTextToJson(configFileName, jsonText); + const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList); + return ts.parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName); + } + + function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = ts.parseJsonText(configFileName, jsonText); + const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList); + return ts.parseJsonNodeConfigFileContent(parsed.node, host, basePath, /*existingOptions*/ undefined, configFileName); } function assertParseFileList(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedFileList: string[]) { - const json = JSON.parse(jsonText); - const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList); - const parsed = ts.parseJsonConfigFileContent(json, host, basePath, /*existingOptions*/ undefined, configFileName); - assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); + { + const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); + assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); + } + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); + } } - function assertParseFileDiagnostics(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedDiagnosticCode: number) { - const json = JSON.parse(jsonText); - const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList); - const parsed = ts.parseJsonConfigFileContent(json, host, basePath, /*existingOptions*/ undefined, configFileName); - assert.isTrue(parsed.errors.length >= 0); - assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); + function assertParseFileDiagnostics(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedDiagnosticCode: number, noLocation?: boolean) { + { + const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); + } + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); + if (!noLocation) { + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode && e.file && e.start && e.length).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)} with location information`); + } + } } it("returns empty config for file with only whitespaces", () => { @@ -202,7 +231,8 @@ namespace ts { } "files": ["file1.ts"] }`; - const { config: configJsonObject, errors: diagnostics } = parseConfigFileTextToJson("config.json", content); + const { node, errors: diagnostics } = parseJsonText("config.json", content); + const configJsonObject = convertToJson(node, diagnostics); const expectedResult = { compilerOptions: { allowJs: true, @@ -232,7 +262,8 @@ namespace ts { "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.js"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); }); it("generates errors for empty directory", () => { @@ -245,7 +276,8 @@ namespace ts { "/apath/tsconfig.json", "tests/cases/unittests", [], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); }); it("generates errors for empty include", () => { @@ -256,7 +288,8 @@ namespace ts { "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); }); it("generates errors for includes with outDir", () => { @@ -270,7 +303,8 @@ namespace ts { "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); }); }); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f6e05ac6788..19b5c178a72 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -782,14 +782,18 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } + private getDefaultParsedJsonNode(): EndOfFileToken { + return { kind: SyntaxKind.EndOfFileToken }; + } + private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); - const { config = {}, errors } = parseConfigFileTextToJson(configFilename, configFileContent); - const parsedCommandLine = parseJsonConfigFileContent( - config, + const { node = this.getDefaultParsedJsonNode(), errors } = parseJsonText(configFilename, configFileContent); + const parsedCommandLine = parseJsonNodeConfigFileContent( + node, this.host, getDirectoryPath(configFilename), /*existingOptions*/ {}, @@ -809,7 +813,7 @@ namespace ts.server { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, compilerOptions: parsedCommandLine.options, - configHasFilesProperty: config["files"] !== undefined, + configHasFilesProperty: parsedCommandLine.raw["files"] !== undefined, wildcardDirectories: createMap(parsedCommandLine.wildcardDirectories), typingOptions: parsedCommandLine.typingOptions, compileOnSave: parsedCommandLine.compileOnSave diff --git a/src/services/shims.ts b/src/services/shims.ts index c210aa29257..1ded848f700 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -1133,9 +1133,9 @@ namespace ts { () => { const text = sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()); - const result = parseConfigFileTextToJson(fileName, text); + const result = parseJsonText(fileName, text); - if (!result.config) { + if (!result.node) { return { options: {}, typingOptions: {}, @@ -1146,7 +1146,7 @@ namespace ts { } const normalizedFileName = normalizeSlashes(fileName); - const configFile = parseJsonConfigFileContent(result.config, this.host, getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName); + const configFile = parseJsonNodeConfigFileContent(result.node, this.host, getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName); return { options: configFile.options,