Skip to content

Parse jsdoc with normal TS type parser#17176

Merged
sandersn merged 14 commits intomasterfrom
parse-jsdoc-with-ts-type-parser
Jul 17, 2017
Merged

Parse jsdoc with normal TS type parser#17176
sandersn merged 14 commits intomasterfrom
parse-jsdoc-with-ts-type-parser

Conversation

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

sandersn added 3 commits July 13, 2017 11:27
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 a user and rbuckton July 13, 2017 19:21
@sandersn
Copy link
Copy Markdown
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.

Comment thread src/harness/unittests/jsDocParsing.ts Outdated
parsesCorrectly("tupleType3", "{[number,string,boolean]}");
parsesCorrectly("tupleTypeWithTrailingComma", "{[number,]}");
parsesCorrectly("typeOfType", "{typeof M}");
parsesCorrectly("tsConstructoType", "{new () => string}");
Copy link
Copy Markdown
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
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

Comment thread src/compiler/checker.ts Outdated
paramSymbol = resolvedSymbol;
}
if (i === 0 && paramSymbol.name === "this") {
if (i === 0 && paramSymbol.name === "this" || (param.type && param.type.kind === SyntaxKind.JSDocThisType)) {
Copy link
Copy Markdown

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
Copy Markdown
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.

Comment thread src/compiler/utilities.ts Outdated
return false;
}

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

@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
Copy Markdown
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
Copy Markdown

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

@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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
case SyntaxKind.ObjectKeyword:
return nonPrimitiveType;
if (node.flags & NodeFlags.JavaScriptFile) {
return anyType;
Copy link
Copy Markdown

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
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated

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

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?

Comment thread src/compiler/checker.ts Outdated
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
}
}

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

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
Copy Markdown
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.

Comment thread src/compiler/checker.ts Outdated
case SyntaxKind.JSDocNullableType:
case SyntaxKind.JSDocAllType:
case SyntaxKind.JSDocUnknownType:
if (!isInJavaScriptFile(node) && !findAncestor(node, n => n.kind === SyntaxKind.JSDocTypeExpression)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Another findAncestor here

Comment thread src/compiler/checker.ts Outdated
}

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

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

Comment thread src/compiler/parser.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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/parser.ts Outdated
return finishNode(parameter);
}

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

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
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sure.

Comment thread src/compiler/parser.ts Outdated

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

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

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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/compiler/parser.ts Outdated
}

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

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.

Comment thread src/compiler/parser.ts Outdated
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
Copy Markdown

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()?

Comment thread src/compiler/parser.ts
return token() === SyntaxKind.CloseParenToken || isStartOfParameter() || isStartOfType();
}

function parseJSDocPostfixTypeOrHigher(): TypeNode {
Copy link
Copy Markdown

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

@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
Copy Markdown

@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
Copy Markdown
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.

Comment thread src/compiler/parser.ts

return finishNode(typedefTag);

function isObjectTypeReference(node: TypeNode) {
Copy link
Copy Markdown

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
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Much better.

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

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

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

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
Copy Markdown
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
Copy Markdown
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.

sandersn added 6 commits July 14, 2017 09:36
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
Copy Markdown
Member Author

I think I've addressed all the comments up to this afternoon.

>z : Symbol(z, Decl(functions.js, 26, 3))
>length : Symbol(length, Decl(functions.js, 12, 27))

/** @type {function ("a" | "b"): 1 | 2} */
Copy link
Copy Markdown

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