Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use parser to parse tsconfig json instead of using Json.parse #12336

Merged
merged 34 commits into from Jun 15, 2017

Conversation

sheetalkamat
Copy link
Member

@sheetalkamat sheetalkamat commented Nov 17, 2016

  • This uses our object literal parser to parse the json text. New api called parseJsonText and return JsonSourceFile - syntax tree
  • There is also api convertToJson that converts JsonSourceFile to json
  • New api called parseJsonSourceFileConfigFileContent that takes this JsonSourceFile to convert to actual ParsedCommandLine and convert the JsonSourceFile to json in one shot so there are error locations for the invalid json options in the tsconfig.json
  • As part of this change I had to refactor code a little bit so that parser can be included with minimal changes into the jsTypingsInstaller
  • JsonSourceFile is stored as configFile in compilerOptions so the options related diagnostics can be reported in the actual tsconfig file
  • config file names are included in project files, and getSemanticDiagnostic on the config file returns the project errors and options diagnostics in that file

"category": "Error",
"code": 1320
},
"String, number, object, array, true, false or null expected.": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about: A property value can only string literal, numeric literal, 'true', 'false', 'null', object literal or array literal.

@@ -859,6 +859,14 @@
"category": "Error",
"code": 1319
},
"String literal with double quotes expected.": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about: A property name must be wrapped in double quotes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not just for the property name though. It could be value that is string literal. Json file can only accept string literals that are double quoted

sourceFile.endOfFileToken = <EndOfFileToken>parseTokenNode();
}
else if (token() === SyntaxKind.OpenBraceToken ||
lookAhead(() => token() === SyntaxKind.StringLiteral)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this happen often? i am assuming this is to handle [ "compilerOptions" : ..., but do we need to ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to handle one of the failing test case which was used to sanitize the tsconfig.json

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give a comment why we need a look ahead to be string literal? also would the the property is allow to be identifier since we use parseObjectLiteralExpression to parse the object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to ECMA-404, the key for the property of a JSON object must be a string literal.

@@ -710,13 +714,259 @@ 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 } {
export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a public method, so i would leave the optional parameter to avoid breaking users who pass it along.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we do if strip comments is false?

* Read tsconfig.json file
* @param fileName The path to the config file
*/
export function readConfigFileToJsonSourceFile(fileName: string, readFile: (path: string) => string): JsonSourceFile {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about readJsonConfigFile.

@@ -3389,6 +3395,8 @@
/* @internal */
export interface TsConfigOnlyOption extends CommandLineOptionBase {
type: "object";
optionDeclarations?: CommandLineOption[];
extraKeyDiagnosticMessage?: DiagnosticMessage;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just report one message unknown option '{0}' for all of the top level nodes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. currently we report unknown compiler option | unknown type acquisition option. I tried to keep the same behavior. Is it ok to change the behavior?

{
name: "compilerOptions",
type: "object",
optionDeclarations: optionDeclarations,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, here is an idea, just merge them all in one big object, and make type accept an array of CommandlineOptions along with string, number, and list. then for the maps, we can switch them to have a new type called "map", and an optional mapElements member, similar to how we handle list today.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also just keep the optionsDeclarations, but can we just combine all the options in one object, instead of two separate hierarchies?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But those are two different hierarchies isn't it? The options specified here can only be present at root of json object and other command line options are possible only inside object called compiler Options?

* @param jsonNode
* @param errors
*/
export function convertToJson(sourceFile: JsonSourceFile, errors: Diagnostic[]): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. i would say these functions should be convert*ToObject and not to json.

compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors);
}
else {
options = getDefaultCompilerOptions(configFileName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined with my other suggestion of having one options definition, can we just parse the whole file, and generate diagnostics for all known options, then extract them here afterwards?

}

function createDiagnosticForOptionName(message: DiagnosticMessage, option1: string, option2?: string) {
createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be better to call the same createDiagnosticForOption twice, once for each option, instead of passing the second key through out the whole stack.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This avoid iterating in compiler options object literal multiple times to find those two options. We have most of the error messages reported on 2 options and there are very select messages that are reported on single option so it seems ok to pass that undefined for those few scenarios?

@sheetalkamat
Copy link
Member Author

The build is failing because of incorrect version of tslint.

@@ -2365,6 +2365,11 @@ namespace ts {
sourceFiles: SourceFile[];
}

export interface JsonSourceFile extends SourceFile {
Copy link

@ghost ghost Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't extend from SourceFile. There's an existing SourceFileLike interface, could you use that? Or just create a SourceFileBase with common properties.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I don't really understand why some functions need to be moved, but if it works, 👍
  • Be sure to test that we give an error for trailing commas.
  • We should also have a test for trying to use single quotes or unquoted properties, or a spread property.
  • The absolute nittiest of nits: We should allow \u2028 in json files.

/**
* Convert the json syntax tree into the json value
*/
export function convertToObject(sourceFile: JsonSourceFile, errors: Diagnostic[]): any {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be @internal?
Also, I'd recommend using errors: Push<Diagnostic>, since it's really an output, not an input.
It would be a good idea to separate parse tree -> object conversion and options validation. This function does both at once.

errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnosticMessage, keyText));
}
const value = convertPropertyValueToJson(element.initializer, option);
if (typeof keyText !== undefined && typeof value !== undefined) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean keyText !== undefined?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was merged like that??

Looks like an overlooked bug.

https://github.com/Microsoft/TypeScript/blob/master/src/compiler/commandLineParser.ts#L1056

Surely it was meant to be typeof keyText !== 'undefined' and same for value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sheetalkamat @andy-ms can you please have a look? This looks like a bug.
Thanks!

let extendedConfigPath: Path;

const optionsIterator: JsonConversionNotifier = {
onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotations on parameters are not necessary, those should be inherited from JsonConversionNotifier.

if (!isDoubleQuotedString(valueExpression)) {
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected));
}
reportInvalidOptionValue(option && (typeof option.type === "string" && option.type !== "string"));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: refactor to check option && typeof option.type === "string" only once.

// vs what we set in the json
// If need arises, we can modify this interface and callbacks as needed
if (option) {
const { elementOptions, extraKeyDiagnosticMessage, name: optionName } = <TsConfigOnlyOption>option;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cast isn't valid. Someone might have passed an object literal to an option that shouldn't have an object value. We will report an error, but still continue on to this point.

return result;
}

hasConfigFile(configFilePath: NormalizedPath) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer isConfigFile -- it doesn't return whether the project has a config file, it returns whether the parameter is the path to one of the project's config files.

@@ -657,7 +694,7 @@ namespace ts.server {
if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
// if current structure version is the same - return info without any changes
if (this.projectStructureVersion === this.lastReportedVersion && !updatedFileNames) {
return { info, projectErrors: this.projectErrors };
return { info, projectErrors: this.getGlobalProjectErrors() };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the change here? It's gone from erporting every error to returning only errors with no associated file. (What errors would those be?)

}

private convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics: Diagnostic[]) {
return diagnostics.map(d => <protocol.DiagnosticWithLinePosition>{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casted object literal

@@ -1011,6 +1011,12 @@ namespace ts {
return false;
}

export function cloneCompilerOptions(options: CompilerOptions): CompilerOptions {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just make sure clone correctly handles enumerability?

{
const actual = ts.parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack);
expected.errors = map(expected.errors, error => {
return <Diagnostic>{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casted object literal

@@ -0,0 +1,180 @@
/* @internal */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that these function are pulled from factory.ts but would get @rbuckton opinion as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious as to why they needed to be moved.

sourceFile.endOfFileToken = <EndOfFileToken>parseTokenNode();
}
else if (token() === SyntaxKind.OpenBraceToken ||
lookAhead(() => token() === SyntaxKind.StringLiteral)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give a comment why we need a look ahead to be string literal? also would the the property is allow to be identifier since we use parseObjectLiteralExpression to parse the object?

clearState();
return result;
}

function getLanguageVariant(scriptKind: ScriptKind) {
// .tsx and .jsx files are treated as jsx language variant.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment probably should be removed

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;
return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSON ? LanguageVariant.JSX : LanguageVariant.Standard;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we LanguageVariant.JSX be renamed ? since it is more than just TSX and JSX

@@ -2365,6 +2365,11 @@ namespace ts {
sourceFiles: SourceFile[];
}

export interface JsonSourceFile extends SourceFile {
jsonObject?: ObjectLiteralExpression;
extendedSourceFiles?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this another tsconfig. this tsconfig extends from??

@mhegazy
Copy link
Contributor

mhegazy commented Jun 13, 2017

@sheetalkamat let's get this ready to be merged

@@ -2365,6 +2365,11 @@ namespace ts {
sourceFiles: SourceFile[];
}

export interface JsonSourceFile extends SourceFile {
jsonObject?: ObjectLiteralExpression;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it makes sense for parsing tsconfig.json, JSON isn't necessarily always an object literal. In the lib reference PR, I started using ts.parseConfigFileTextToJson to parse a single libs.json file shared between the gulpfile and jakefile. I'd have to make some changes since libs.json has an array as its root.

@@ -0,0 +1,180 @@
/* @internal */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious as to why they needed to be moved.

/**
* Gets flags that control emit behavior of a node.
*/
export function getEmitFlags(node: Node): EmitFlags | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this moved here? All of the other emitNode related functions are still in factory, including the corresponding setEmitFlags function.

@@ -4215,6 +4158,16 @@ namespace ts {
return node.kind === SyntaxKind.ParenthesizedExpression;
}

export function skipPartiallyEmittedExpressions(node: Expression): Expression;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this moved? All of the other related skip functions are still in factory.ts.

sourceFile.endOfFileToken = parseExpectedToken(SyntaxKind.EndOfFileToken, /*reportAtCurrentPosition*/ false, Diagnostics.Unexpected_token);
}
else {
parseExpected(SyntaxKind.OpenBraceToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are only planning on using this to parse tsconfig.json, then this is fine. If we are planning on using this to parse other JSON files, there are other legal root objects for a JSON file (e.g. Array Literals, string literals, numeric literals, true, false, and null).

@mihailik
Copy link
Contributor

mihailik commented Jun 19, 2017

Is there a written up rationale for this change anywhere?

I can think of several reasons to do custom-parse JSON (and a few reasons not to as well!) — so it would be very helpful to clarify the intention here. Or add a link to the detailed explanation, if it's already mentioned.

Many thanks!

@kitsonk
Copy link
Contributor

kitsonk commented Jun 20, 2017

It is part of the Roadmap for 2.4 in order to provide better error reporting for tsconfig.json.

@DanielRosenwasser
Copy link
Member

This will probably be going into the 2.4.2 which is slated for next month.

@sheetalkamat sheetalkamat deleted the ownJsonParsing branch October 10, 2017 18:07
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants