Skip to content

Commit cca98c3

Browse files
committed
Parse json using our own parser
1 parent c90a40c commit cca98c3

12 files changed

+172
-95
lines changed

src/compiler/commandLineParser.ts

Lines changed: 99 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -681,13 +681,13 @@ namespace ts {
681681
* Read tsconfig.json file
682682
* @param fileName The path to the config file
683683
*/
684-
export function readConfigFile(fileName: string, readFile: (path: string) => string): { config?: any; error?: Diagnostic } {
684+
export function readConfigFile(fileName: string, readFile: (path: string) => string): { config?: any; errors: Diagnostic[] } {
685685
let text = "";
686686
try {
687687
text = readFile(fileName);
688688
}
689689
catch (e) {
690-
return { error: createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) };
690+
return { errors: [createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message)] };
691691
}
692692
return parseConfigFileTextToJson(fileName, text);
693693
}
@@ -697,13 +697,102 @@ namespace ts {
697697
* @param fileName The path to the config file
698698
* @param jsonText The text of the config file
699699
*/
700-
export function parseConfigFileTextToJson(fileName: string, jsonText: string, stripComments = true): { config?: any; error?: Diagnostic } {
701-
try {
702-
const jsonTextToParse = stripComments ? removeComments(jsonText) : jsonText;
703-
return { config: /\S/.test(jsonTextToParse) ? JSON.parse(jsonTextToParse) : {} };
700+
export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config: any; errors: Diagnostic[] } {
701+
const { node, errors } = parseJsonText(fileName, jsonText);
702+
return {
703+
config: convertToJson(node, errors),
704+
errors
705+
};
706+
}
707+
708+
/**
709+
* Convert the json syntax tree into the json value
710+
* @param jsonNode
711+
* @param errors
712+
*/
713+
function convertToJson(jsonNode: JsonNode, errors: Diagnostic[]): any {
714+
if (!jsonNode) {
715+
return undefined;
704716
}
705-
catch (e) {
706-
return { error: createCompilerDiagnostic(Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) };
717+
718+
if (jsonNode.kind === SyntaxKind.EndOfFileToken) {
719+
return {};
720+
}
721+
722+
const sourceFile = <SourceFile>jsonNode.parent;
723+
return convertObjectLiteralExpressionToJson(jsonNode);
724+
725+
function convertObjectLiteralExpressionToJson(node: ObjectLiteralExpression): any {
726+
const result: any = {};
727+
for (const element of node.properties) {
728+
switch (element.kind) {
729+
case SyntaxKind.MethodDeclaration:
730+
case SyntaxKind.GetAccessor:
731+
case SyntaxKind.SetAccessor:
732+
case SyntaxKind.ShorthandPropertyAssignment:
733+
case SyntaxKind.SpreadAssignment:
734+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element, Diagnostics.Property_assignment_expected));
735+
break;
736+
737+
case SyntaxKind.PropertyAssignment:
738+
if (element.questionToken) {
739+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, Diagnostics._0_can_only_be_used_in_a_ts_file, "?"));
740+
}
741+
if (!isDoubleQuotedString(element.name)) {
742+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, Diagnostics.String_literal_with_double_quotes_expected));
743+
}
744+
const keyText = getTextOfPropertyName(element.name);
745+
const value = parseValue(element.initializer);
746+
if (typeof keyText !== undefined && typeof value !== undefined) {
747+
result[keyText] = value;
748+
}
749+
}
750+
}
751+
return result;
752+
}
753+
754+
function convertArrayLiteralExpressionToJson(node: ArrayLiteralExpression): any[] {
755+
const result: any[] = [];
756+
for (const element of node.elements) {
757+
result.push(parseValue(element));
758+
}
759+
return result;
760+
}
761+
762+
function parseValue(node: Expression): any {
763+
switch (node.kind) {
764+
case SyntaxKind.TrueKeyword:
765+
return true;
766+
767+
case SyntaxKind.FalseKeyword:
768+
return false;
769+
770+
case SyntaxKind.NullKeyword:
771+
return null; // tslint:disable-line:no-null-keyword
772+
773+
case SyntaxKind.StringLiteral:
774+
if (!isDoubleQuotedString(node)) {
775+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_literal_with_double_quotes_expected));
776+
}
777+
return (<StringLiteral>node).text;
778+
779+
case SyntaxKind.NumericLiteral:
780+
return Number((<NumericLiteral>node).text);
781+
782+
case SyntaxKind.ObjectLiteralExpression:
783+
return convertObjectLiteralExpressionToJson(<ObjectLiteralExpression>node);
784+
785+
case SyntaxKind.ArrayLiteralExpression:
786+
return convertArrayLiteralExpressionToJson(<ArrayLiteralExpression>node);
787+
}
788+
789+
// Not in expected format
790+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, node, Diagnostics.String_number_object_array_true_false_or_null_expected));
791+
return undefined;
792+
}
793+
794+
function isDoubleQuotedString(node: Node) {
795+
return node.kind === SyntaxKind.StringLiteral && getSourceTextOfNodeFromSourceFile(sourceFile, node).charCodeAt(0) === CharacterCodes.doubleQuote;
707796
}
708797
}
709798

@@ -795,31 +884,6 @@ namespace ts {
795884
}
796885
}
797886

798-
/**
799-
* Remove the comments from a json like text.
800-
* Comments can be single line comments (starting with # or //) or multiline comments using / * * /
801-
*
802-
* This method replace comment content by whitespace rather than completely remove them to keep positions in json parsing error reporting accurate.
803-
*/
804-
function removeComments(jsonText: string): string {
805-
let output = "";
806-
const scanner = createScanner(ScriptTarget.ES5, /* skipTrivia */ false, LanguageVariant.Standard, jsonText);
807-
let token: SyntaxKind;
808-
while ((token = scanner.scan()) !== SyntaxKind.EndOfFileToken) {
809-
switch (token) {
810-
case SyntaxKind.SingleLineCommentTrivia:
811-
case SyntaxKind.MultiLineCommentTrivia:
812-
// replace comments with whitespace to preserve original character positions
813-
output += scanner.getTokenText().replace(/\S/g, " ");
814-
break;
815-
default:
816-
output += scanner.getTokenText();
817-
break;
818-
}
819-
}
820-
return output;
821-
}
822-
823887
/**
824888
* Parse the contents of a config file (tsconfig.json).
825889
* @param json The contents of the config file to parse
@@ -896,8 +960,8 @@ namespace ts {
896960
}
897961
}
898962
const extendedResult = readConfigFile(extendedConfigPath, path => host.readFile(path));
899-
if (extendedResult.error) {
900-
errors.push(extendedResult.error);
963+
if (extendedResult.errors.length) {
964+
errors.push(...extendedResult.errors);
901965
return;
902966
}
903967
const extendedDirname = getDirectoryPath(extendedConfigPath);

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,14 @@
851851
"category": "Error",
852852
"code": 1317
853853
},
854+
"String literal with double quotes expected.": {
855+
"category": "Error",
856+
"code": 1318
857+
},
858+
"String, number, object, array, true, false or null expected.": {
859+
"category": "Error",
860+
"code": 1319
861+
},
854862
"Duplicate identifier '{0}'.": {
855863
"category": "Error",
856864
"code": 2300

src/compiler/parser.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,17 @@ namespace ts {
449449
return Parser.parseIsolatedEntityName(text, languageVersion);
450450
}
451451

452+
export type ParsedNodeResults<T extends Node> = { node: T; errors: Diagnostic[] };
453+
454+
/**
455+
* Parse json text into SyntaxTree and return node and parse errors if any
456+
* @param fileName
457+
* @param sourceText
458+
*/
459+
export function parseJsonText(fileName: string, sourceText: string): ParsedNodeResults<JsonNode> {
460+
return Parser.parseJsonText(fileName, sourceText);
461+
}
462+
452463
export function isExternalModule(file: SourceFile): boolean {
453464
return file.externalModuleIndicator !== undefined;
454465
}
@@ -610,6 +621,33 @@ namespace ts {
610621
return isInvalid ? entityName : undefined;
611622
}
612623

624+
export function parseJsonText(fileName: string, sourceText: string): ParsedNodeResults<JsonNode> {
625+
initializeState(sourceText, ScriptTarget.ES2015, /*syntaxCursor*/ undefined, ScriptKind.JS);
626+
// Set source file so that errors will be reported with this file name
627+
sourceFile = <SourceFile>{ kind: SyntaxKind.SourceFile, text: sourceText, fileName };
628+
let node: JsonNode;
629+
// Prime the scanner.
630+
nextToken();
631+
if (token() === SyntaxKind.EndOfFileToken) {
632+
node = <EndOfFileToken>parseTokenNode();
633+
}
634+
else if (token() === SyntaxKind.OpenBraceToken ||
635+
lookAhead(() => token() === SyntaxKind.StringLiteral)) {
636+
node = parseObjectLiteralExpression();
637+
parseExpected(SyntaxKind.EndOfFileToken, Diagnostics.Unexpected_token);
638+
}
639+
else {
640+
parseExpected(SyntaxKind.OpenBraceToken);
641+
}
642+
643+
if (node) {
644+
node.parent = sourceFile;
645+
}
646+
const errors = parseDiagnostics;
647+
clearState();
648+
return { node, errors };
649+
}
650+
613651
function getLanguageVariant(scriptKind: ScriptKind) {
614652
// .tsx and .jsx files are treated as jsx language variant.
615653
return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS ? LanguageVariant.JSX : LanguageVariant.Standard;

src/compiler/tsc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,9 @@ namespace ts {
367367
}
368368

369369
const result = parseConfigFileTextToJson(configFileName, cachedConfigFileText);
370+
reportDiagnostics(result.errors, /* compilerHost */ undefined);
370371
const configObject = result.config;
371372
if (!configObject) {
372-
reportDiagnostics([result.error], /* compilerHost */ undefined);
373373
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
374374
return;
375375
}

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,8 @@ namespace ts {
525525
export type AtToken = Token<SyntaxKind.AtToken>;
526526
export type ReadonlyToken = Token<SyntaxKind.ReadonlyKeyword>;
527527

528+
export type JsonNode = ObjectLiteralExpression | EndOfFileToken;
529+
528530
export type Modifier
529531
= Token<SyntaxKind.AbstractKeyword>
530532
| Token<SyntaxKind.AsyncKeyword>

src/harness/projectsRunner.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,13 @@ class ProjectRunner extends RunnerBase {
208208
configFileName = ts.findConfigFile("", fileExists);
209209
}
210210

211+
let errors: ts.Diagnostic[];
211212
if (configFileName) {
212213
const result = ts.readConfigFile(configFileName, getSourceFileText);
213-
if (result.error) {
214+
if (!result.config) {
214215
return {
215216
moduleKind,
216-
errors: [result.error]
217+
errors: result.errors
217218
};
218219
}
219220

@@ -228,11 +229,12 @@ class ProjectRunner extends RunnerBase {
228229
if (configParseResult.errors.length > 0) {
229230
return {
230231
moduleKind,
231-
errors: configParseResult.errors
232+
errors: result.errors.concat(configParseResult.errors)
232233
};
233234
}
234235
inputFiles = configParseResult.fileNames;
235236
compilerOptions = configParseResult.options;
237+
errors = result.errors;
236238
}
237239

238240
const projectCompilerResult = compileProjectFiles(moduleKind, () => inputFiles, getSourceFileText, writeFile, compilerOptions);
@@ -242,7 +244,7 @@ class ProjectRunner extends RunnerBase {
242244
compilerOptions,
243245
sourceMapData: projectCompilerResult.sourceMapData,
244246
outputFiles,
245-
errors: projectCompilerResult.errors,
247+
errors: errors ? errors.concat(projectCompilerResult.errors) : projectCompilerResult.errors,
246248
};
247249

248250
function createCompilerOptions() {

src/harness/unittests/configurationExtension.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ namespace ts {
112112
], ([testName, basePath, host]) => {
113113
function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) {
114114
it(name, () => {
115-
const {config, error} = ts.readConfigFile(entry, name => host.readFile(name));
116-
assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n"));
115+
const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name));
116+
assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n"));
117117
const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry);
118118
assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n"));
119119
expected.configFilePath = entry;
@@ -124,8 +124,8 @@ namespace ts {
124124

125125
function testFailure(name: string, entry: string, expectedDiagnostics: {code: number, category: DiagnosticCategory, messageText: string}[]) {
126126
it(name, () => {
127-
const {config, error} = ts.readConfigFile(entry, name => host.readFile(name));
128-
assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n"));
127+
const {config, errors} = ts.readConfigFile(entry, name => host.readFile(name));
128+
assert(config && !errors.length, flattenDiagnosticMessageText(errors[0] && errors[0].messageText, "\n"));
129129
const parsed = ts.parseJsonConfigFileContent(config, host, basePath, {}, entry);
130130
verifyDiagnostics(parsed.errors, expectedDiagnostics);
131131
});

src/harness/unittests/projectErrors.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,7 @@ namespace ts.projectSystem {
123123
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
124124
assert.isTrue(configuredProject !== undefined, "should find configured project");
125125
checkProjectErrors(configuredProject, [
126-
"')' expected.",
127-
"Declaration or statement expected.",
128-
"Declaration or statement expected.",
129-
"Failed to parse file '/a/b/tsconfig.json'"
126+
"'{' expected."
130127
]);
131128
}
132129
// fix config and trigger watcher
@@ -175,10 +172,7 @@ namespace ts.projectSystem {
175172
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
176173
assert.isTrue(configuredProject !== undefined, "should find configured project");
177174
checkProjectErrors(configuredProject, [
178-
"')' expected.",
179-
"Declaration or statement expected.",
180-
"Declaration or statement expected.",
181-
"Failed to parse file '/a/b/tsconfig.json'"
175+
"'{' expected."
182176
]);
183177
}
184178
});

src/harness/unittests/tsconfigParsing.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33

44
namespace ts {
55
describe("parseConfigFileTextToJson", () => {
6-
function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic }) {
6+
function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; errors?: Diagnostic[] }) {
77
const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText);
8+
if (!expectedConfigObject.errors) {
9+
expectedConfigObject.errors = [];
10+
}
811
assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject));
912
}
1013

1114
function assertParseError(jsonText: string) {
1215
const parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText);
1316
assert.isTrue(undefined === parsed.config);
14-
assert.isTrue(undefined !== parsed.error);
17+
assert.isTrue(!!parsed.errors.length);
1518
}
1619

1720
function assertParseErrorWithExcludesKeyword(jsonText: string) {
@@ -199,7 +202,7 @@ namespace ts {
199202
}
200203
"files": ["file1.ts"]
201204
}`;
202-
const { configJsonObject, diagnostics } = sanitizeConfigFile("config.json", content);
205+
const { config: configJsonObject, errors: diagnostics } = parseConfigFileTextToJson("config.json", content);
203206
const expectedResult = {
204207
compilerOptions: {
205208
allowJs: true,

0 commit comments

Comments
 (0)