-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Conversation
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.
I haven't looked at the implementation yet, but I just tried this out in a JSDoc codebase. Some feedback about the current functionality.
Other than that I notice we currently don't account for
|
@DanielRosenwasser 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 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 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. |
I very much doubt this - I don't think I've ever once seen
So is |
The emitter now understands JSDoc types but emits them in the original format.
} | ||
|
||
function visitJSDocParameter(node: ParameterDeclaration) { | ||
const name = node.name || "arg" + node.parent.parameters.indexOf(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.
Consider rest
for a rest parameter instead of argN
?
src/compiler/emitter.ts
Outdated
@@ -574,6 +576,20 @@ namespace ts { | |||
return emitMappedType(<MappedTypeNode>node); | |||
case SyntaxKind.LiteralType: | |||
return emitLiteralType(<LiteralTypeNode>node); | |||
case SyntaxKind.JSDocAllType: | |||
write("*"); | |||
break; |
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.
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.
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.
Nice catch, thanks.
return createParameter(node.decorators, node.modifiers, node.dotDotDotToken, name, node.questionToken, node.type, node.initializer); | ||
} | ||
|
||
function visitJSDocTypeReference(node: 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.
Would it be worth converting Array.<T>
to (T)[]
, rather than Array<T>
?
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.
@weswigham why parentheses?
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.
@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.
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.
@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.
@DanielRosenwasser After talking again to you and @mhegazy, I added a transform that explicitly makes the |
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. |
@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. |
And give a better name for rest params
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 { |
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.
TypeNode has a rather well defined meaning in other parts of the system. so i would make it isDeclarationWithType
instead.
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.
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); |
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.
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); |
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.
random thought, seems weird that we allow this refactoring on functions with type parameters..
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.
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)?
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.
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.)
Incorporate suppressLeadingAndTrailingTrivia just added by @amcasey.
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); |
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.
consider calling them all transform*
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.
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 |
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 duplicated 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.
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}`); |
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.
Consider a never
assertion
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.
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) { |
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 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); |
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.
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 : |
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.
refactoringType
function getEditsForAnnotation(context: RefactorContext, action: string): RefactorEditInfo | undefined { | ||
if (actionName !== action) { | ||
Debug.fail(`actionName !== action: ${actionName} !== ${action}`); | ||
return undefined; |
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.
return Debug.fail(...)
?
Trying this with /**
* @param {boolean} option
*/
function test(option) {}
|
@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 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",
|
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:
noImplicitAny: true
, this shows up as a code fix in VS Code whenever there is a no-implicit-any error. WithnoImplicityAny: false
, this code must be invoked via the refactoring command.