diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 26877de43c4..d273f59411f 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -681,13 +681,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; error?: Diagnostic } { + export function readConfigFile(fileName: string, readFile: (path: string) => string): { config?: any; errors: Diagnostic[] } { let text = ""; try { text = readFile(fileName); } catch (e) { - return { error: createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; + return { errors: [createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message)] }; } return parseConfigFileTextToJson(fileName, text); } @@ -697,13 +697,102 @@ 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, stripComments = true): { config?: any; error?: Diagnostic } { - try { - const jsonTextToParse = stripComments ? removeComments(jsonText) : jsonText; - return { config: /\S/.test(jsonTextToParse) ? JSON.parse(jsonTextToParse) : {} }; + export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config: any; errors: Diagnostic[] } { + const { node, errors } = parseJsonText(fileName, jsonText); + return { + config: convertToJson(node, errors), + errors + }; + } + + /** + * Convert the json syntax tree into the json value + * @param jsonNode + * @param errors + */ + function convertToJson(jsonNode: JsonNode, errors: Diagnostic[]): any { + if (!jsonNode) { + return undefined; } - catch (e) { - return { error: createCompilerDiagnostic(Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; + + if (jsonNode.kind === SyntaxKind.EndOfFileToken) { + return {}; + } + + const sourceFile = jsonNode.parent; + return convertObjectLiteralExpressionToJson(jsonNode); + + function convertObjectLiteralExpressionToJson(node: ObjectLiteralExpression): 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; + + case SyntaxKind.PropertyAssignment: + 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 value = parseValue(element.initializer); + if (typeof keyText !== undefined && typeof value !== undefined) { + result[keyText] = value; + } + } + } + return result; + } + + function convertArrayLiteralExpressionToJson(node: ArrayLiteralExpression): any[] { + const result: any[] = []; + for (const element of node.elements) { + result.push(parseValue(element)); + } + return result; + } + + function parseValue(node: Expression): any { + switch (node.kind) { + case SyntaxKind.TrueKeyword: + return true; + + case SyntaxKind.FalseKeyword: + return false; + + case SyntaxKind.NullKeyword: + 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; + + case SyntaxKind.NumericLiteral: + return Number((node).text); + + case SyntaxKind.ObjectLiteralExpression: + return convertObjectLiteralExpressionToJson(node); + + case SyntaxKind.ArrayLiteralExpression: + return convertArrayLiteralExpressionToJson(node); + } + + // Not in expected format + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_number_object_array_true_false_or_null_expected)); + return undefined; + } + + function isDoubleQuotedString(node: Node) { + return node.kind === SyntaxKind.StringLiteral && getSourceTextOfNodeFromSourceFile(sourceFile, node).charCodeAt(0) === CharacterCodes.doubleQuote; } } @@ -795,31 +884,6 @@ namespace ts { } } - /** - * Remove the comments from a json like text. - * Comments can be single line comments (starting with # or //) or multiline comments using / * * / - * - * This method replace comment content by whitespace rather than completely remove them to keep positions in json parsing error reporting accurate. - */ - function removeComments(jsonText: string): string { - let output = ""; - const scanner = createScanner(ScriptTarget.ES5, /* skipTrivia */ false, LanguageVariant.Standard, jsonText); - let token: SyntaxKind; - while ((token = scanner.scan()) !== SyntaxKind.EndOfFileToken) { - switch (token) { - case SyntaxKind.SingleLineCommentTrivia: - case SyntaxKind.MultiLineCommentTrivia: - // replace comments with whitespace to preserve original character positions - output += scanner.getTokenText().replace(/\S/g, " "); - break; - default: - output += scanner.getTokenText(); - break; - } - } - return output; - } - /** * Parse the contents of a config file (tsconfig.json). * @param json The contents of the config file to parse @@ -896,8 +960,8 @@ namespace ts { } } const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path)); - if (extendedResult.error) { - errors.push(extendedResult.error); + if (extendedResult.errors.length) { + errors.push(...extendedResult.errors); return; } const extendedDirname = getDirectoryPath(extendedConfigPath); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2dd4e76f8f4..19232130ad8 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -851,6 +851,14 @@ "category": "Error", "code": 1317 }, + "String literal with double quotes expected.": { + "category": "Error", + "code": 1318 + }, + "String, number, object, array, true, false or null expected.": { + "category": "Error", + "code": 1319 + }, "Duplicate identifier '{0}'.": { "category": "Error", "code": 2300 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index b08f75b0f5b..c2cf2d5bfbb 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -449,6 +449,17 @@ namespace ts { return Parser.parseIsolatedEntityName(text, languageVersion); } + export type ParsedNodeResults = { node: T; errors: Diagnostic[] }; + + /** + * Parse json text into SyntaxTree and return node and parse errors if any + * @param fileName + * @param sourceText + */ + export function parseJsonText(fileName: string, sourceText: string): ParsedNodeResults { + return Parser.parseJsonText(fileName, sourceText); + } + export function isExternalModule(file: SourceFile): boolean { return file.externalModuleIndicator !== undefined; } @@ -610,6 +621,33 @@ namespace ts { return isInvalid ? entityName : undefined; } + export function parseJsonText(fileName: string, sourceText: string): ParsedNodeResults { + initializeState(sourceText, ScriptTarget.ES2015, /*syntaxCursor*/ undefined, ScriptKind.JS); + // Set source file so that errors will be reported with this file name + sourceFile = { kind: SyntaxKind.SourceFile, text: sourceText, fileName }; + let node: JsonNode; + // Prime the scanner. + nextToken(); + if (token() === SyntaxKind.EndOfFileToken) { + node = parseTokenNode(); + } + else if (token() === SyntaxKind.OpenBraceToken || + lookAhead(() => token() === SyntaxKind.StringLiteral)) { + node = parseObjectLiteralExpression(); + parseExpected(SyntaxKind.EndOfFileToken, Diagnostics.Unexpected_token); + } + else { + parseExpected(SyntaxKind.OpenBraceToken); + } + + if (node) { + node.parent = sourceFile; + } + const errors = parseDiagnostics; + clearState(); + return { node, errors }; + } + function getLanguageVariant(scriptKind: ScriptKind) { // .tsx and .jsx files are treated as jsx language variant. return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS ? LanguageVariant.JSX : LanguageVariant.Standard; diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 10d4a0c1d36..d435b596953 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -367,9 +367,9 @@ namespace ts { } const result = parseConfigFileTextToJson(configFileName, cachedConfigFileText); + reportDiagnostics(result.errors, /* compilerHost */ undefined); const configObject = result.config; if (!configObject) { - reportDiagnostics([result.error], /* compilerHost */ undefined); sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b5e3eefbb2d..04377709f3b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -525,6 +525,8 @@ namespace ts { export type AtToken = Token; export type ReadonlyToken = Token; + export type JsonNode = ObjectLiteralExpression | EndOfFileToken; + export type Modifier = Token | Token diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index f7541dc9bfe..973a7b5dac1 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -208,12 +208,13 @@ class ProjectRunner extends RunnerBase { configFileName = ts.findConfigFile("", fileExists); } + let errors: ts.Diagnostic[]; if (configFileName) { const result = ts.readConfigFile(configFileName, getSourceFileText); - if (result.error) { + if (!result.config) { return { moduleKind, - errors: [result.error] + errors: result.errors }; } @@ -228,11 +229,12 @@ class ProjectRunner extends RunnerBase { if (configParseResult.errors.length > 0) { return { moduleKind, - errors: configParseResult.errors + errors: result.errors.concat(configParseResult.errors) }; } inputFiles = configParseResult.fileNames; compilerOptions = configParseResult.options; + errors = result.errors; } const projectCompilerResult = compileProjectFiles(moduleKind, () => inputFiles, getSourceFileText, writeFile, compilerOptions); @@ -242,7 +244,7 @@ class ProjectRunner extends RunnerBase { compilerOptions, sourceMapData: projectCompilerResult.sourceMapData, outputFiles, - errors: projectCompilerResult.errors, + errors: errors ? errors.concat(projectCompilerResult.errors) : projectCompilerResult.errors, }; function createCompilerOptions() { diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts index 8e845925eb2..010d03d6811 100644 --- a/src/harness/unittests/configurationExtension.ts +++ b/src/harness/unittests/configurationExtension.ts @@ -112,8 +112,8 @@ namespace ts { ], ([testName, basePath, host]) => { function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) { it(name, () => { - const {config, error} = ts.readConfigFile(entry, name => host.readFile(name)); - assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); + const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name)); + assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].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; @@ -124,8 +124,8 @@ namespace ts { function testFailure(name: string, entry: string, expectedDiagnostics: {code: number, category: DiagnosticCategory, messageText: string}[]) { it(name, () => { - const {config, error} = ts.readConfigFile(entry, name => host.readFile(name)); - assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); + const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name)); + assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n")); const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry); verifyDiagnostics(parsed.errors, expectedDiagnostics); }); diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 3db3675dbd6..4548e063601 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -123,10 +123,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, [ - "')' expected.", - "Declaration or statement expected.", - "Declaration or statement expected.", - "Failed to parse file '/a/b/tsconfig.json'" + "'{' expected." ]); } // fix config and trigger watcher @@ -175,10 +172,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, [ - "')' expected.", - "Declaration or statement expected.", - "Declaration or statement expected.", - "Failed to parse file '/a/b/tsconfig.json'" + "'{' expected." ]); } }); diff --git a/src/harness/unittests/tsconfigParsing.ts b/src/harness/unittests/tsconfigParsing.ts index 7b980cdd2d8..9a7ed315d7f 100644 --- a/src/harness/unittests/tsconfigParsing.ts +++ b/src/harness/unittests/tsconfigParsing.ts @@ -3,15 +3,18 @@ namespace ts { describe("parseConfigFileTextToJson", () => { - function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic }) { + function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; errors?: 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(undefined !== parsed.error); + assert.isTrue(!!parsed.errors.length); } function assertParseErrorWithExcludesKeyword(jsonText: string) { @@ -199,7 +202,7 @@ namespace ts { } "files": ["file1.ts"] }`; - const { configJsonObject, diagnostics } = sanitizeConfigFile("config.json", content); + const { config: configJsonObject, errors: diagnostics } = parseConfigFileTextToJson("config.json", content); const expectedResult = { compilerOptions: { allowJs: true, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f69b5c11547..f6e05ac6788 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -786,18 +786,8 @@ namespace ts.server { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); - let errors: Diagnostic[]; - - const result = parseConfigFileTextToJson(configFilename, configFileContent); - let config = result.config; - - if (result.error) { - // try to reparse config file - const { configJsonObject: sanitizedConfig, diagnostics } = sanitizeConfigFile(configFilename, configFileContent); - config = sanitizedConfig; - errors = diagnostics.length ? diagnostics : [result.error]; - } + const { config = {}, errors } = parseConfigFileTextToJson(configFilename, configFileContent); const parsedCommandLine = parseJsonConfigFileContent( config, this.host, @@ -806,13 +796,13 @@ namespace ts.server { configFilename); if (parsedCommandLine.errors.length) { - errors = concatenate(errors, parsedCommandLine.errors); + errors.push(...parsedCommandLine.errors); } Debug.assert(!!parsedCommandLine.fileNames); if (parsedCommandLine.fileNames.length === 0) { - (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); + errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); return { success: false, configFileErrors: errors }; } diff --git a/src/services/shims.ts b/src/services/shims.ts index 1c8132793a3..c210aa29257 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -1135,13 +1135,13 @@ namespace ts { const result = parseConfigFileTextToJson(fileName, text); - if (result.error) { + if (!result.config) { return { options: {}, typingOptions: {}, files: [], raw: {}, - errors: [realizeDiagnostic(result.error, "\r\n")] + errors: realizeDiagnostics(result.errors, "\r\n") }; } @@ -1153,7 +1153,7 @@ namespace ts { typingOptions: configFile.typingOptions, files: configFile.fileNames, raw: configFile.raw, - errors: realizeDiagnostics(configFile.errors, "\r\n") + errors: realizeDiagnostics(result.errors.concat(configFile.errors), "\r\n") }; }); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 82b13f82327..5cd9853c4be 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1334,28 +1334,4 @@ namespace ts { } return ensureScriptKind(fileName, scriptKind); } - - export function sanitizeConfigFile(configFileName: string, content: string) { - const options: TranspileOptions = { - fileName: "config.js", - compilerOptions: { - target: ScriptTarget.ES2015, - removeComments: true - }, - reportDiagnostics: true - }; - const { outputText, diagnostics } = ts.transpileModule("(" + content + ")", options); - // Becasue the content was wrapped in "()", the start position of diagnostics needs to be subtract by 1 - // also, the emitted result will have "(" in the beginning and ");" in the end. We need to strip these - // as well - const trimmedOutput = outputText.trim(); - for (const diagnostic of diagnostics) { - diagnostic.start = diagnostic.start - 1; - } - const {config, error} = parseConfigFileTextToJson(configFileName, trimmedOutput.substring(1, trimmedOutput.length - 2), /*stripComments*/ false); - return { - configJsonObject: config || {}, - diagnostics: error ? concatenate(diagnostics, [error]) : diagnostics - }; - } }