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

Refactor jsdoc types to typescript #18747

Merged
merged 19 commits into from
Oct 13, 2017
Merged

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Sep 25, 2017

When the caret is on a Typescript declaration that has no type, but does have a JSDoc annotation with a type, this refactor will add the Typescript equivalent of the JSDoc type. If the caret is on a parameter or a function, then it will add types to all the parameters of the function, the return type, and the type parameters, if any of these are available in JSDoc and are not already present on the function.

Notes:

  1. This doesn't delete the JSDoc comment or delete parts of it.
  2. As a bonus, when noImplicitAny: true, this shows up as a code fix in VS Code whenever there is a no-implicit-any error. With noImplicityAny: false, this code must be invoked via the refactoring command.
  3. The emitter needs to understand JSDoc types in case we emit some, but I ended up converting JSDoc types to Typescript types as part of the refactoring.
When the caret is on a Typescript declaration that has no type, but does
have a JSDoc annotation with a type, this refactor will add the
Typescript equivalent of the JSDoc type.

Notes:

1. This doesn't delete the JSDoc comment or delete parts of it. In fact,
due to bugs in trivia handling, it sometimes duplicates the comment.
These bugs are tracked in #18626.

2. As a bonus, when `noImplicitAny: true`, this shows up as a code fix in VS Code
whenever there is a no-implicit-any error. With `noImplicityAny: false`,
this code must be invoked via the refactoring command.
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 25, 2017

I haven't looked at the implementation yet, but I just tried this out in a JSDoc codebase. Some feedback about the current functionality.

  • Boolean, Undefined and the like need to convert properly to boolean, undefined, etc.
  • Object needs to convert to any or give the option between object and any.
  • Array needs to get the appropriate type arguments.
  • We should reconsider whether we understand Mixed.
  • I don't think Convert to TypeScript type is the appropriate text.
    1. You're annotating the variable, you're not converting it.
    2. Please make it explicit that you're inferring from JSDoc.
    3. For return types, you should make it explicit that you're adding the return type to a function/method. For example
      class FooBlah {
         /**
         * @return {string}
         */
         foo() {
          this.blah;
        }
      }
      There's an error span on blah, and your refactoring actually gives me a misleading suggestion to "Convert to TypeScript type".

Other than that I notice we currently don't account for

  1. JSDoc type assertions
  2. JSDoc typedefs
  3. JSDoc augments clauses
@sandersn
Copy link
Member Author

@DanielRosenwasser
Here are some replies, in reverse order:

This refactoring only tries to hit the common cases. I can add the other cases later. In particular, JSDoc type assertions are Closure syntax and isn't common outside Closure and mixed TS/JS codebases.

How about "Annotate [return] type from JSDoc"? "Annotate type based on JSDoc"? A smaller repro of your example is:

/** @return {number} */
function f() {
  /*1*/return 12;
}

I didn't think about the fact that other nodes besides FunctionDeclaration could trigger the addition of a return type to the function declaration. You're right, the refactor should at least indicate that a return type will be added.

As for Boolean, Undefined, Object, etc, I'm worried that we'll overshoot the intelligence of this refactor into annoying territory. I'm not even sure we should be converting * and T=, except that those are always errors in Typescript, and I feel bad about creating nodes that are guaranteed to be red-underlined immediately.

I'd like some way to figure out the right amount of translation to do here, but I don't have any data or intuitions right now.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 26, 2017

I'm worried that we'll overshoot the intelligence of this refactor into annoying territory.

I very much doubt this - I don't think I've ever once seen String used correctly outside of lib.d.ts. You also end up not being able to call anything that expects a string instead.

those are always errors in Typescript

So is Undefined and Null unless you can statically resolve it to something else (probably not worth it)

}

function visitJSDocParameter(node: ParameterDeclaration) {
const name = node.name || "arg" + node.parent.parameters.indexOf(node);
Copy link
Member

Choose a reason for hiding this comment

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

Consider rest for a rest parameter instead of argN?

@@ -574,6 +576,20 @@ namespace ts {
return emitMappedType(<MappedTypeNode>node);
case SyntaxKind.LiteralType:
return emitLiteralType(<LiteralTypeNode>node);
case SyntaxKind.JSDocAllType:
write("*");
break;
Copy link
Member

Choose a reason for hiding this comment

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

These should be return and not break, otherwise someone might add them to the isToken check at the bottom in the future and they could get printed twice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice catch, thanks.

return createParameter(node.decorators, node.modifiers, node.dotDotDotToken, name, node.questionToken, node.type, node.initializer);
}

function visitJSDocTypeReference(node: TypeReferenceNode) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it be worth converting Array.<T> to (T)[], rather than Array<T>?

Copy link
Member

@DanielRosenwasser DanielRosenwasser Sep 28, 2017

Choose a reason for hiding this comment

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

@weswigham why parentheses?

Copy link
Member Author

Choose a reason for hiding this comment

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

@weswigham Nope. I don't want to introduce arbitrary style fixups, just ones that we believe are nearly always wrong. And Array<T> is a style choice that is favoured by lots (a majority?) of the JS community.

Copy link
Member

Choose a reason for hiding this comment

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

@DanielRosenwasser I just included those in case T was string | number, to serve as a reminder parens may be needed. But if @sandersn wants to preserve the existing style, that's fine.

@sandersn
Copy link
Member Author

@DanielRosenwasser After talking again to you and @mhegazy, I added a transform that explicitly makes the Stringstring as well as other transforms.

@DanielRosenwasser
Copy link
Member

If this works, I think the thing we need to provide is the ability to do it per a signature, if not on the entire file. Too small a level of granularity and users will find this too cumbersome to bother with.

@sandersn
Copy link
Member Author

@DanielRosenwasser I agree, but apply-entire-file is definitely an architectural change for refactorings, and apply-entire-signature might be. Either way I'd like to ship this as-is and expand it later.

@sandersn
Copy link
Member Author

I swapped out the annotate-return-type refactor entirely for annotate-entire-signature. It's actually 3 lines shorter and would be even shorter if not for limitations in the change tracker.

};
}

function isTypedNode(node: Node): node is DeclarationWithType {
Copy link
Contributor

Choose a reason for hiding this comment

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

TypeNode has a rather well defined meaning in other parts of the system. so i would make it isDeclarationWithType instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that name is much better.

case SyntaxKind.MethodDeclaration:
return createMethod(decl.decorators, decl.modifiers, decl.asteriskToken, decl.name, decl.questionToken, decl.typeParameters, parameters, returnType, decl.body);
case SyntaxKind.GetAccessor:
return createGetAccessor(decl.decorators, decl.modifiers, decl.name, parameters, returnType, decl.body);
Copy link
Contributor

Choose a reason for hiding this comment

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

a get accessor should have 0 args, so why not just use decl.parameters.

p => createParameter(p.decorators, p.modifiers, p.dotDotDotToken, p.name, p.questionToken, p.type || transformJSDocType(getJSDocType(p)) as TypeNode, p.initializer));
switch (decl.kind) {
case SyntaxKind.FunctionDeclaration:
return createFunctionDeclaration(decl.decorators, decl.modifiers, decl.asteriskToken, decl.name, decl.typeParameters, parameters, returnType, decl.body);
Copy link
Contributor

Choose a reason for hiding this comment

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

random thought, seems weird that we allow this refactoring on functions with type parameters..

Copy link
Member

Choose a reason for hiding this comment

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

Should probably use getEffectiveTypeParameterDeclarations to pull type parameters from @template tags, no (or a version of it which does not limit it looking at jsdoc to js files)?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's easy to add, I think.

(Also, adding it exposed a bug: functions required @return in order to trigger the refactoring at all, which is wrong.)

@sandersn
Copy link
Member Author

I incorporated suppressLeadingAndTrailingTrivia, which was just added by @amcasey, into the PR. This fixes the duplicated JSDoc comments that happened sometimes.

case SyntaxKind.JSDocUnknownType:
return createTypeReferenceNode("any", emptyArray);
case SyntaxKind.JSDocOptionalType:
return visitJSDocOptionalType(node as JSDocOptionalType);
Copy link
Contributor

@mhegazy mhegazy Oct 12, 2017

Choose a reason for hiding this comment

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

consider calling them all transform*

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

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

Looks good! Would appreciate if you could integrate some of the nits though. :)

//// /*1*/p = null
////}

// NOTE: The duplicated comment is unintentional but needs a serious fix in trivia handling
Copy link
Member

Choose a reason for hiding this comment

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

What duplicated 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.

Nice catch. 🐱

case SyntaxKind.SetAccessor:
return createSetAccessor(decl.decorators, decl.modifiers, decl.name, parameters, decl.body);
default:
return Debug.fail(`Unexpected SyntaxKind: ${(decl as any).kind}`);
Copy link
Member

Choose a reason for hiding this comment

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

Consider a never assertion

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice. I didn't know we had Debug.assertNever.


const node = getTokenAtPosition(context.file, context.startPosition, /*includeJsDocComment*/ false);
const decl = findAncestor(node, isDeclarationWithType);
if (decl && !decl.type) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you use early bailouts to reduce nesting here?

const node = getTokenAtPosition(context.file, context.startPosition, /*includeJsDocComment*/ false);
const decl = findAncestor(node, isDeclarationWithType);
if (decl && !decl.type) {
const type = getJSDocType(decl);
Copy link
Member

Choose a reason for hiding this comment

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

jsDocType

if (decl && !decl.type) {
const type = getJSDocType(decl);
const isFunctionWithJSDoc = isFunctionLikeDeclaration(decl) && (getJSDocReturnType(decl) || decl.parameters.some(p => !!getJSDocType(p)));
const annotate = (isFunctionWithJSDoc || type && decl.kind === SyntaxKind.Parameter) ? annotateFunctionFromJSDoc :
Copy link
Member

Choose a reason for hiding this comment

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

refactoringType

function getEditsForAnnotation(context: RefactorContext, action: string): RefactorEditInfo | undefined {
if (actionName !== action) {
Debug.fail(`actionName !== action: ${actionName} !== ${action}`);
return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

return Debug.fail(...)?

@sandersn sandersn merged commit 22769d9 into master Oct 13, 2017
@sandersn sandersn deleted the refactor-jsdoc-types-to-typescript branch October 13, 2017 17:21
@cyrilletuzi
Copy link

Trying this with typescript@next, and with this simple code :

/**
 * @param {boolean} option 
 */
function test(option) {}

option is still indicated as any, and then raising an error with noImplicitAny. Is this normal, or did I misunderstood this feature ?

@sandersn
Copy link
Member Author

@cyrilletuzi It sounds like you are thinking of using JSDoc as type annotation. This works in JS files only, so if you copy your example there, you should see option get the right type.

This PR give you a refactoring to move the JSDoc down into type annotation position in TS files. But JSDoc still isn't checked — it's just copied to type annotation position, which is then checked. If the refactoring isn't showing up in VS Code, you may need to add this to your config:

    "typescript.tsdk": "/home/YOURNAME/ts/built/local",
    "typescript.tsdk_version": "2.5.0",
  1. Checking JSDoc types inside TS files is tricky to do well: if there are duplicates, the binder shouldn't use the JSDoc, but this means the binder has to detect duplicates before deciding which annotations to bind.
  2. Checking JSDoc types inside TS is currently not part of the upgrade workflow we envision -- we think people will either (1) import their JS as-is and use it alongside newly-written TS files or (2) Convert existing JS to TS, which means converting JSDoc to type annotations.
@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
7 participants