-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Ensure that JSDoc parsing happens within a ParsingContext #52710
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
Changes from all commits
a84f0b9
bd85412
472bf7e
81c697a
6b2888a
ba1da57
0acd844
d71cc52
13fde97
961c6c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1491,6 +1491,8 @@ namespace Parser { | |
var identifiers: Map<string, string>; | ||
var identifierCount: number; | ||
|
||
// TODO(jakebailey): This type is a lie; this value actually contains the result | ||
// of ORing a bunch of `1 << ParsingContext.XYZ`. | ||
var parsingContext: ParsingContext; | ||
|
||
var notParenthesizedArrow: Set<number> | undefined; | ||
|
@@ -2872,9 +2874,13 @@ namespace Parser { | |
return tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.OpenBraceToken; | ||
case ParsingContext.JsxChildren: | ||
return true; | ||
case ParsingContext.JSDocComment: | ||
return true; | ||
case ParsingContext.Count: | ||
return Debug.fail("ParsingContext.Count used as a context"); // Not a real context, only a marker. | ||
default: | ||
Debug.assertNever(parsingContext, "Non-exhaustive case in 'isListElement'."); | ||
} | ||
|
||
return Debug.fail("Non-exhaustive case in 'isListElement'."); | ||
} | ||
|
||
function isValidHeritageClauseObjectLiteral() { | ||
|
@@ -3010,6 +3016,9 @@ namespace Parser { | |
|
||
// True if positioned at element or terminator of the current list or any enclosing list | ||
function isInSomeParsingContext(): boolean { | ||
// We should be in at least one parsing context, be it SourceElements while parsing | ||
// a SourceFile, or JSDocComment when lazily parsing JSDoc. | ||
Debug.assert(parsingContext, "Missing parsing context"); | ||
for (let kind = 0; kind < ParsingContext.Count; kind++) { | ||
if (parsingContext & (1 << kind)) { | ||
if (isListElement(kind, /*inErrorRecovery*/ true) || isListTerminator(kind)) { | ||
|
@@ -3385,6 +3394,7 @@ namespace Parser { | |
case ParsingContext.JsxAttributes: return parseErrorAtCurrentToken(Diagnostics.Identifier_expected); | ||
case ParsingContext.JsxChildren: return parseErrorAtCurrentToken(Diagnostics.Identifier_expected); | ||
case ParsingContext.AssertEntries: return parseErrorAtCurrentToken(Diagnostics.Identifier_or_string_literal_expected); // AssertionKey. | ||
case ParsingContext.JSDocComment: return parseErrorAtCurrentToken(Diagnostics.Identifier_expected); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it mean to provide an error in a JSDoc parsing comment? Where would we issue this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is part of recovery in parseList/parseDelimitedList and feasibly only only happens when a list ends prematurely. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably don't need an error but it's fine. In a follow-up PR, I'd be curious to see what happens if it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, yeah, you can just But, I am probably close to eliminating this entire parse mode entirely too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably this doesn't break because the tests aren't good enough. |
||
case ParsingContext.Count: return Debug.fail("ParsingContext.Count used as a context"); // Not a real context, only a marker. | ||
default: Debug.assertNever(context); | ||
} | ||
|
@@ -7289,6 +7299,8 @@ namespace Parser { | |
|
||
function tryReuseAmbientDeclaration(pos: number): Statement | undefined { | ||
return doInsideOfContext(NodeFlags.Ambient, () => { | ||
// TODO(jakebailey): this is totally wrong; `parsingContext` is the result of ORing a bunch of `1 << ParsingContext.XYZ`. | ||
// The enum should really be a bunch of flags. | ||
Comment on lines
+7302
to
+7303
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of the other uses get this right, at least, so node reuse has feasibly only been broken for ambient declarations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right - what happens if you pass in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But we have tests that ensure that some node reuse occurs, don't we? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All fourslash server tests which edit their files are incremental, which covers things in some aspect, but the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add an incremental test with an ambient context where node reuse should happen, and which fails when you pass in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In trying to make a test case for this, I've found that this can only be a problem if there's an ambient declaration inside of something else, like: {
declare module "foo" {
// ...
}
} This is technically illegal code, and this bug here means we never use any nodes from this tree. Ambients are only being reused now because they can only be a child of the SourceFile itself, and its ParsingContext happens to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I could fix the bug, but it'd only fix broken trees that probably never happen. But it's still totally wrong and only works because of a fluke! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I'm wrong, the "ambient in a block" also works because of a fluke; you get parsingContext |
||
const node = currentNode(parsingContext, pos); | ||
if (node) { | ||
return consumeNode(node) as Statement; | ||
|
@@ -8492,7 +8504,8 @@ namespace Parser { | |
TupleElementTypes, // Element types in tuple element type list | ||
HeritageClauses, // Heritage clauses for a class or interface declaration. | ||
ImportOrExportSpecifiers, // Named import clause's import specifier list, | ||
AssertEntries, // Import entries list. | ||
AssertEntries, // Import entries list. | ||
JSDocComment, // Parsing via JSDocParser | ||
Count // Number of parsing contexts | ||
} | ||
|
||
|
@@ -8598,6 +8611,9 @@ namespace Parser { | |
} | ||
|
||
function parseJSDocCommentWorker(start = 0, length: number | undefined): JSDoc | undefined { | ||
const saveParsingContext = parsingContext; | ||
parsingContext |= 1 << ParsingContext.JSDocComment; | ||
|
||
const content = sourceText; | ||
const end = length === undefined ? content.length : start + length; | ||
length = end - start; | ||
|
@@ -8620,7 +8636,11 @@ namespace Parser { | |
const parts: JSDocComment[] = []; | ||
|
||
// + 3 for leading /**, - 5 in total for /** */ | ||
return scanner.scanRange(start + 3, length - 5, () => { | ||
const result = scanner.scanRange(start + 3, length - 5, doJSDocScan); | ||
parsingContext = saveParsingContext; | ||
return result; | ||
|
||
function doJSDocScan() { | ||
// Initially we can parse out a tag. We also have seen a starting asterisk. | ||
// This is so that /** * @type */ doesn't parse. | ||
let state = JSDocState.SawAsterisk; | ||
|
@@ -8726,7 +8746,7 @@ namespace Parser { | |
if (parts.length && tags) Debug.assertIsDefined(commentsPos, "having parsed tags implies that the end of the comment span should be set"); | ||
const tagsArray = tags && createNodeArray(tags, tagsPos, tagsEnd); | ||
return finishNode(factory.createJSDocComment(parts.length ? createNodeArray(parts, start, commentsPos) : trimmedComments.length ? trimmedComments : undefined, tagsArray), start, end); | ||
}); | ||
} | ||
|
||
function removeLeadingNewlines(comments: string[]) { | ||
while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,7 @@ | |
// zee(''); | ||
// ^ | ||
// | ---------------------------------------------------------------------- | ||
// | zee(**arg0: any**, arg1: any, arg2: any): any | ||
// | zee(**arg0: any**, arg1: any): any | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is:
Clearly, this has two parameters (not three!), and now we get that right. |
||
// | ---------------------------------------------------------------------- | ||
|
||
[ | ||
|
@@ -259,30 +259,6 @@ | |
], | ||
"isOptional": false, | ||
"isRest": false | ||
}, | ||
{ | ||
"name": "arg2", | ||
"documentation": [], | ||
"displayParts": [ | ||
{ | ||
"text": "arg2", | ||
"kind": "parameterName" | ||
}, | ||
{ | ||
"text": ":", | ||
"kind": "punctuation" | ||
}, | ||
{ | ||
"text": " ", | ||
"kind": "space" | ||
}, | ||
{ | ||
"text": "any", | ||
"kind": "keyword" | ||
} | ||
], | ||
"isOptional": false, | ||
"isRest": false | ||
} | ||
], | ||
"documentation": [], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,12 @@ | ||
/a.js(1,13): error TS1098: Type parameter list cannot be empty. | ||
/a.js(1,14): error TS1139: Type parameter declaration expected. | ||
/a.js(1,17): error TS1003: Identifier expected. | ||
/a.js(1,17): error TS1110: Type expected. | ||
/a.js(1,17): error TS8024: JSDoc '@param' tag has name '', but there is no parameter with that name. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is:
Clearly this has a parameter named |
||
/a.js(1,18): error TS1005: '>' expected. | ||
/a.js(1,18): error TS1005: '}' expected. | ||
|
||
|
||
==== /a.js (6 errors) ==== | ||
==== /a.js (2 errors) ==== | ||
/** @param {<} x */ | ||
~~ | ||
!!! error TS1098: Type parameter list cannot be empty. | ||
~ | ||
!!! error TS1139: Type parameter declaration expected. | ||
|
||
!!! error TS1003: Identifier expected. | ||
|
||
!!! error TS1110: Type expected. | ||
|
||
!!! error TS8024: JSDoc '@param' tag has name '', but there is no parameter with that name. | ||
|
||
!!! error TS1005: '>' expected. | ||
|
||
!!! error TS1005: '}' expected. | ||
function f(x) {} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
=== /a.js === | ||
/** @param {<} x */ | ||
function f(x) {} | ||
>f : (x: any) => void | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IDK if this is bad or not; this is clearly malformed JSDoc but before we just got any. Now we think we're starting type parameters (which is normal for parsing a type) and then assume it's probably a function signature which is the only thing that can start with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks fine to me. |
||
>x : any | ||
>f : (x: () => any) => void | ||
>x : () => any | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
// @filename: index.js | ||
//// class I18n { | ||
//// /** | ||
//// * @param {{dot|fulltext}} [stringMode] - which mode our translation keys use | ||
//// */ | ||
//// constructor(options = {}) {} | ||
//// } | ||
|
||
verify.encodedSyntacticClassificationsLength(69); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how did this (mostly) work before? Do we never call
isListElement
inside jsdoc? (Unlikely). DidcurrentNode(parsingContext)
always return a value, so the switch never executed? Was there some other incorrect parsing context set during jsdoc parsing, except for the bug case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code was actually never hit for JSDoc, because the loop in
isInSomeParsingContext
checks each bit and then uses it. Unlike source files (which are always inside of a statement list), the standalone JSDoc parser was not, and so its parsing context was zero, and skipped all of this code (which was the bug).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to think about this particular block is that it's checking equiality on the context, which are values like
{ 1, 2, 3, 4 ... }
... but the context actually stored in the parser are actually1 << { 1, 2, 3, 4 ... }
, so anyone trying to use this code has to un-flag-ify the value before using it, and that's the other broken thing I found in this code.