-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Parse jsdoc with normal TS type parser #17176
Conversation
This means that JSDoc types can include the full range of Typescript types now. It also means that Typescript annotations can include JSDoc types. This is disallowed with a new error, however. But Typescript can still give the correct types to JSDoc that shows up in .ts files by mistake. This can easily happen, for example with types like ```ts var x: number? = null; var y: ?string = null; var z: function(string,string): string = (s,t) => s + t; // less likely to show up, but still understood. var ka: ? = 1; ``` In the future, I will add a quick fix to convert these into the correct types. Fixes #16550
@andy-ms asked for fourslash tests for
If any of these fails I will probably file issues instead of fixing in this PR because (1) It's not clear how much of this worked before because the binder didn't handle these parameters, and (2) I'm not sure anybody even uses this syntax. |
@@ -62,28 +62,26 @@ namespace ts { | |||
parsesCorrectly("tupleType1", "{[number]}"); | |||
parsesCorrectly("tupleType2", "{[number,string]}"); | |||
parsesCorrectly("tupleType3", "{[number,string,boolean]}"); | |||
parsesCorrectly("tupleTypeWithTrailingComma", "{[number,]}"); | |||
parsesCorrectly("typeOfType", "{typeof M}"); | |||
parsesCorrectly("tsConstructoType", "{new () => string}"); |
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.
Typo: constructo
rather than constructor
.
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.
Fixed.
src/compiler/checker.ts
Outdated
@@ -6339,7 +6339,7 @@ namespace ts { | |||
const resolvedSymbol = resolveName(param, paramSymbol.name, SymbolFlags.Value, undefined, undefined); | |||
paramSymbol = resolvedSymbol; | |||
} | |||
if (i === 0 && paramSymbol.name === "this") { | |||
if (i === 0 && paramSymbol.name === "this" || (param.type && param.type.kind === SyntaxKind.JSDocThisType)) { |
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.
I'm not a fan of JSDocThisType
. It looks like you would have a parameter with no name, and a type of kind JSDocThisType
, where the type itself has a .type
property storing the real type.
I think this: T
should be parsed to the same AST whether we're in TypeScript or in JSDoc, so you don't need a condition for both here.
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.
I removed JSDocThisType and -ConstructorType like you suggested.
src/compiler/utilities.ts
Outdated
@@ -3298,6 +3297,10 @@ namespace ts { | |||
return false; | |||
} | |||
|
|||
export function isJSDocTypeReference(node: TypeReferenceType): node is TypeReferenceNode { |
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 is going to be super slow if we're not in jsdoc -- for any type reference not in jsdoc we will climb the whole tree looking for a JSDocTypeExpression that never comes. Can you use isInJavaScriptFile
?
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.
That's correct. Any ideas for a better implementation?
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.
See edited comment.
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.
isInJavaScriptFile may not be sufficient because JSDoc types can appear anywhere in TS or JS files, inside or outside JSDoc. For example:
/** @type {...number} */
var x;
var y: ...number; // annotations are errors in JS file, but still fully checked and treated as number[]
and
/** @type {...number} */ // actually, this isn't checked.
var x;
var y: ...number; // ... is an error in TS file, but still fully checked and treated as number[]
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.
So if you have let o: ...object;
in a JS file, you will see object
and climb up the tree to determine whether it's in a JSDoc comment? Since that's a grammar error anyway, I don't think it's a big deal if we give the object
there JSDoc semantics instead of TS semantics. Sure it appears in a TypeScript type position, but it's in a JS file, so we might as well treat it the same as any other JSDoc type (and issue a grammar error).
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.
There's a JSDoc Parser context flag now, right? Can it not just be persisted into all nodes as a node flag when they are created within a jsdoc context? So you just check if your node has the flag, and if so, then its inside a jsdoc and has jsdoc semantics?
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.
Yep, I realised that last night. In fact, finishNode
in the parser persists the context flag, so it was there all along.
src/compiler/checker.ts
Outdated
@@ -7852,16 +7842,18 @@ namespace ts { | |||
case SyntaxKind.NeverKeyword: | |||
return neverType; | |||
case SyntaxKind.ObjectKeyword: | |||
return nonPrimitiveType; | |||
if (node.flags & NodeFlags.JavaScriptFile) { | |||
return anyType; |
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.
There seems to be duplicate code in getPrimitiveTypeFromJSDocTypeReference
, are these both needed?
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.
getPrimitiveTypeFromJSDocTypeReference
operates on TypeReferenceNode not on a TypeNode with SyntaxKind.Object. Actually, the "object" case there no longer applies since it @param {object} x
is no longer parsed as a type reference.
src/compiler/checker.ts
Outdated
@@ -18474,6 +18460,9 @@ namespace ts { | |||
|
|||
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) { | |||
checkGrammarTypeArguments(node, node.typeArguments); | |||
if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDot && !isInJavaScriptFile(node) && !findAncestor(node, n => n.kind === SyntaxKind.JSDocTypeExpression)) { |
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 findAncestor
call here. We shouldn't be checking a type reference in a jsdoc comment if we're not in a JS file anyway, right?
src/compiler/checker.ts
Outdated
@@ -18474,6 +18460,9 @@ namespace ts { | |||
|
|||
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) { | |||
checkGrammarTypeArguments(node, node.typeArguments); | |||
if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDot && !isInJavaScriptFile(node) && !findAncestor(node, n => n.kind === SyntaxKind.JSDocTypeExpression)) { | |||
grammarErrorOnNode(node, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); |
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.
So if I write: let x: N.M.O.<P, Q>;
I'll get a grammar error on the whole node... it would be hard to spot the extra .
. Could we try to calculate the range between O
and <
for the error, or use a more specific message?
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.
I'll just save the dots as we parse them and retain it for this error in the jsdoc 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.
Actually, that creates additional garbage from the dots we don't use, which is all dots except the last one in a JSDoc-syntax generic. These dots are relatively very rare, so I will save the start and end instead and manually specify the error span.
src/compiler/checker.ts
Outdated
@@ -19372,7 +19361,17 @@ namespace ts { | |||
} | |||
} | |||
|
|||
function checkJsDoc(node: FunctionDeclaration | MethodDeclaration) { |
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 could be inlined. forEach(node.jsDoc, checkSourceElement)
.
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.
I'd like to keep the function around for readability and because it may someday be used from more locations.
src/compiler/checker.ts
Outdated
case SyntaxKind.JSDocNullableType: | ||
case SyntaxKind.JSDocAllType: | ||
case SyntaxKind.JSDocUnknownType: | ||
if (!isInJavaScriptFile(node) && !findAncestor(node, n => n.kind === SyntaxKind.JSDocTypeExpression)) { |
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 findAncestor
here
src/compiler/checker.ts
Outdated
function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration): void { | ||
checkJsDoc(node); |
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.
Seems silly to call this in a TS file if the checkers for every possible jsdoc element will test if we're in a TS file and then do nothing?
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.
Good point. I moved the check into checkJSDoc
src/compiler/parser.ts
Outdated
@@ -1969,9 +1928,14 @@ namespace ts { | |||
|
|||
// The allowReservedWords parameter controls whether reserved words are permitted after the first dot | |||
function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: DiagnosticMessage): EntityName { | |||
let entity: EntityName = parseIdentifier(diagnosticMessage); | |||
let entity: EntityName = allowReservedWords ? parseIdentifierName() : parseIdentifier(diagnosticMessage); |
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.
Why this change? So we can write const.const
?
Comment needs an update if so.
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.
Yes, you can say @param {const} number
in JSDoc. I'll update the comment.
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.
Actually, without the "right hand side" qualification, it's a garbage comment ("allowReservedWords allows reserved words!"). I deleted it.
src/compiler/parser.ts
Outdated
return finishNode(parameter); | ||
} | ||
|
||
function parseJSDocNodeWithType(kind: SyntaxKind): TypeNode { |
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.
Could provide a more specific type to kind
.
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.
Sure.
src/compiler/parser.ts
Outdated
@@ -2171,7 +2198,7 @@ namespace ts { | |||
} | |||
|
|||
function isStartOfParameter(): boolean { | |||
return token() === SyntaxKind.DotDotDotToken || isIdentifierOrPattern() || isModifierKind(token()) || token() === SyntaxKind.AtToken || token() === SyntaxKind.ThisKeyword; | |||
return token() === SyntaxKind.DotDotDotToken || isIdentifierOrPattern() || isModifierKind(token()) || token() === SyntaxKind.AtToken || token() === SyntaxKind.ThisKeyword || token() === SyntaxKind.NewKeyword; |
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.
If JSDoc parameters can include any type, doesn't this leave out a lot of things?
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.
isIdentifierOrPattern and isModifierKind cover a lot of things like number
, const
, etc. But they happen not to cover this
or new
.
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.
Can we parse this?
/** @type {function({ x: number }, 1 | 2): number} */
const f = (x, y) => x.x + y;
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.
not 1 | 2
. I will try adding isStartOfType
to the end of isStartOfParameter
but I'm not sure if it will be successful.
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.
At least one test entered an infinite loop with isStartOfType
inside isStartOfParameter
. Instead I just added StringLiteral
and NumericLiteral
to the end.
src/compiler/parser.ts
Outdated
@@ -2538,10 +2567,14 @@ namespace ts { | |||
return finishNode(node); | |||
} | |||
|
|||
function parseFunctionOrConstructorType(kind: SyntaxKind): FunctionOrConstructorTypeNode { | |||
function parseFunctionOrConstructorType(kind: SyntaxKind): FunctionOrConstructorTypeNode | JSDocConstructorType { |
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.
Could provide a more specific type to kind
. I only see this being called with kind
of SyntaxKind.FunctionType
of SyntaxKind.ConstructorType
, never SyntaxKind.JSDocConstructorType
.
src/compiler/parser.ts
Outdated
const node = <FunctionOrConstructorTypeNode>createNode(kind); | ||
if (kind === SyntaxKind.ConstructorType) { | ||
parseExpected(SyntaxKind.NewKeyword); | ||
if (token() === SyntaxKind.ColonToken) { | ||
// JSDoc -- `new:T` as in `function(new:T, string, string)`; an infix constructor-return-type |
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.
What happens if we see new:T
and it's not inside of function()
?
@@ -2649,6 +2694,26 @@ namespace ts { | |||
return token() === SyntaxKind.CloseParenToken || isStartOfParameter() || isStartOfType(); | |||
} | |||
|
|||
function parseJSDocPostfixTypeOrHigher(): TypeNode { |
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.
Move this next to (or into) parseTypeOperatorOrHigher
.
Can we parse foo?[]
or foo![]
? Could merge with parseArrayTypeOrHigher
(also only called in one place).
I took a stab at making this neater:
function parseJSDocPostfixTypeOrHigher(): TypeNode {
const type = parseArrayTypeOrHigher();
const kind = getKind(token());
if (!kind) return type;
const postfix = createNode(kind, type.pos) as JSDocOptionalType | JSDocNonNullableType | JSDocNullableType;
postfix.type = type;
return finishNode(postfix);
function getKind(tokenKind: SyntaxKind): SyntaxKind | undefined {
switch (tokenKind) {
case SyntaxKind.EqualsToken:
// only parse postfix = inside jsdoc, because it's ambiguous elsewhere
return contextFlags & NodeFlags.JSDoc ? SyntaxKind.JSDocOptionalType : undefined;
case SyntaxKind.ExclamationToken:
return SyntaxKind.JSDocNonNullableType;
case SyntaxKind.QuestionToken:
return SyntaxKind.JSDocNullableType;
}
}
}
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.
- No,
foo?[]
would need to parse inside parseArrayTypeOrHigher. That does seem like it should parse asArray<foo | null>
. - I don't think we should merge functions that would map to individual rules in a grammar.
- Thanks for the refactor.
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.
Seems like solving 1 would require ditching 2. If we want to be able to parse foo?[]?[]!
, we would need to handle postfix types in a uniform way.
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.
So now I can type syntactically valid jsdoc types in place of swearing.
If you're checking out this branch on windows, note that I inadvertently created a new test |
@@ -6839,6 +6581,18 @@ namespace ts { | |||
|
|||
return finishNode(typedefTag); | |||
|
|||
function isObjectTypeReference(node: TypeNode) { |
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.
node.kind === SyntaxKind.ObjectKeyword || isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object";
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.
Much better.
@@ -1,18 +1,16 @@ | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(4,9): error TS2304: Cannot find name 'z'. | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(5,15): error TS2304: Cannot find name 'z'. | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(6,14): error TS2339: Property 'z' does not exist on type 'C'. | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(7,15): error TS1003: Identifier expected. | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(7,20): error TS2339: Property 'z' does not exist on type 'C'. | |||
tests/cases/conformance/classes/propertyMemberDeclarations/initializerReferencingConstructorLocals.ts(7,15): error TS2304: Cannot find name 'this'. |
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 tests a TS file -- it looks like we are now parsing this
as an identifier.
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.
That's because parseEntityName
now allows reserved words anywhere, not just on the right of a dot. That affects even single identifiers, but in this case it now allows this
in this.x
to be parsed as an identifier.
Other callers that pass allowReservedWords: true
are parseTypeQuery
and parseIsolatedEntityName
. Plus parseTypeReference
in the case of JSDoc.
I think the change is fine in parseTypeQuery
since this error is at least as easy to read as the old one. parseIsolatedEntityName
just seems to be used for parsing --jsxFactory
, so that change should be fine too (although I'm not sure why it allows reserved words at all).
var weird1: new:string = {}; | ||
~~~~~~ | ||
!!! error TS2322: Type '{}' is not assignable to type 'string'. | ||
~~~~~~~ |
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 range seems off -- shouldn't it include new
?
// @module: commonjs | ||
// @filename: node.d.ts | ||
// @noImplicitAny: true | ||
declare function require(id: string): any; |
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 doesn't seem relevant to the test?
Also, the test name seems to imply that jsdoc types will appear inside of type annotations.
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.
Actually, I don't think this test is testing anything unique. I'll just remove it.
From in-person discussion with @andy-ms: I should maybe try to check JSDocFunctionType with checkFunctionLikeDeclaration in order to further reduce duplication and improve checking thoroughness. |
1. Remove checkJSDocFunctionType in favour of checkSignature. 2. Check that 'new', in addition to 'this', must be the first parameter. 3. Remove prematurely added JSDoc-quickfix test.
Also some cleanup from PR comments
I think I've addressed all the comments up to this afternoon. |
@@ -61,3 +61,9 @@ z.length; | |||
>z : Symbol(z, Decl(functions.js, 26, 3)) | |||
>length : Symbol(length, Decl(functions.js, 12, 27)) | |||
|
|||
/** @type {function ("a" | "b"): 1 | 2} */ |
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 doesn't actually test the ability to parse a number literal as a parameter type, that would be function("a" | "b", 1 | 2): whatever
.
This means that JSDoc types can include the full range of Typescript types now. It also means that Typescript annotations can include JSDoc types. This is disallowed with a new error, however. But Typescript can still give the correct types to JSDoc that shows up in .ts files by mistake. This can easily happen, for example with types like:
This change saves around 300 lines of code and some of the confusion from having exact duplicate parsing and types for JSDoc vs normal types. Now only JSDoc types and syntax that differ from Typescript's need to be handled separately.
In the future, I will add a quick fix to convert these into the correct types.
Fixes #16550