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

Parse jsdoc with normal TS type parser #17176

Merged
merged 14 commits into from
Jul 17, 2017
Merged

Conversation

sandersn
Copy link
Member

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:

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;

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

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
@sandersn sandersn requested review from rbuckton and a user July 13, 2017 19:21
@sandersn
Copy link
Member Author

sandersn commented Jul 13, 2017

@andy-ms asked for fourslash tests for function(this: T, string): string and function(new: T, string) for the following areas:

  • parameter help for these functions
  • completions in the scope of these functions
  • find all refs on the new parameter should not crash (true of JSDoc function parameters)
  • find all refs on the this parameter should find all uses of this in the function.

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}");
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

@@ -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)) {
Copy link

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.

Copy link
Member Author

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.

@@ -3298,6 +3297,10 @@ namespace ts {
return false;
}

export function isJSDocTypeReference(node: TypeReferenceType): node is TypeReferenceNode {
Copy link

@ghost ghost Jul 13, 2017

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?

Copy link
Member Author

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?

Copy link

Choose a reason for hiding this comment

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

See edited comment.

Copy link
Member Author

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[]

Copy link

@ghost ghost Jul 14, 2017

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).

Copy link
Member

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?

Copy link
Member Author

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.

@@ -7852,16 +7842,18 @@ namespace ts {
case SyntaxKind.NeverKeyword:
return neverType;
case SyntaxKind.ObjectKeyword:
return nonPrimitiveType;
if (node.flags & NodeFlags.JavaScriptFile) {
return anyType;
Copy link

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?

Copy link
Member Author

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.

@@ -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)) {
Copy link

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?

@@ -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);
Copy link

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

@@ -19372,7 +19361,17 @@ namespace ts {
}
}

function checkJsDoc(node: FunctionDeclaration | MethodDeclaration) {
Copy link

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).

Copy link
Member Author

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.

case SyntaxKind.JSDocNullableType:
case SyntaxKind.JSDocAllType:
case SyntaxKind.JSDocUnknownType:
if (!isInJavaScriptFile(node) && !findAncestor(node, n => n.kind === SyntaxKind.JSDocTypeExpression)) {
Copy link

Choose a reason for hiding this comment

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

Another findAncestor here

function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration): void {
checkJsDoc(node);
Copy link

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?

Copy link
Member Author

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

@@ -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);
Copy link

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

return finishNode(parameter);
}

function parseJSDocNodeWithType(kind: SyntaxKind): TypeNode {
Copy link

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.

@@ -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;
Copy link

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?

Copy link
Member Author

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.

Copy link

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;

Copy link
Member Author

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.

Copy link
Member Author

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.

@@ -2538,10 +2567,14 @@ namespace ts {
return finishNode(node);
}

function parseFunctionOrConstructorType(kind: SyntaxKind): FunctionOrConstructorTypeNode {
function parseFunctionOrConstructorType(kind: SyntaxKind): FunctionOrConstructorTypeNode | JSDocConstructorType {
Copy link

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.

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
Copy link

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 {
Copy link

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;
        }
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. No, foo?[] would need to parse inside parseArrayTypeOrHigher. That does seem like it should parse as Array<foo | null>.
  2. I don't think we should merge functions that would map to individual rules in a grammar.
  3. Thanks for the refactor.

Copy link

@ghost ghost Jul 14, 2017

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.

Copy link

@ghost ghost Jul 14, 2017

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.

@sandersn
Copy link
Member Author

If you're checking out this branch on windows, note that I inadvertently created a new test jsdocInTypescript.ts that differs only in case from an existing test jsdocInTypeScript.ts. I'll fix this in the next commit, but in the meantime, Wesley found that the tests are broken.

@@ -6839,6 +6581,18 @@ namespace ts {

return finishNode(typedefTag);

function isObjectTypeReference(node: TypeNode) {
Copy link

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";

Copy link
Member Author

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'.
Copy link

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.

Copy link
Member Author

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'.
~~~~~~~
Copy link

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;
Copy link

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.

Copy link
Member Author

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.

@sandersn
Copy link
Member Author

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
@sandersn
Copy link
Member Author

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} */
Copy link

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.

@sandersn sandersn merged commit 240f1f1 into master Jul 17, 2017
@mhegazy mhegazy deleted the parse-jsdoc-with-ts-type-parser branch November 2, 2017 21:04
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 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.

JSDoc: support intersection types
3 participants