inferFromUsage codefix now emits JSDoc in JS files#27610
Conversation
Probably everything else, but not as well.
It's still bad, but at least it's not wrong.
Also, remove redundant is(Set|Get)Accessor functions.
By surrounding the parameter name with brackets. It is super, super ugly right now.
More to come, though. annotate is only called in annotateVariableDeclaration where we don't know whether we're in JS or not.
| switch (errorCode) { | ||
| case Diagnostics.Parameter_0_implicitly_has_an_1_type.code: | ||
| return isSetAccessor(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217 | ||
| return isSetAccessorDeclaration(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217 |
There was a problem hiding this comment.
Why do we have both of these -- isSetAccessor and isSetAccessorDeclaration?
There was a problem hiding this comment.
No reason, but removing one would change the public API, so I decided not do it in this PR.
|
|
||
| // @allowJs: true | ||
| // @checkJs: true | ||
| // @noImplicitAny: true |
There was a problem hiding this comment.
I think this shouldn't be necessary because we should make it a suggestion diagnostic anyway, if we don't already.
There was a problem hiding this comment.
Either noImplicitAny isn't triggered as a suggestion (perhaps just for JS?), or the test is only seeing codefixes from errors. Any idea which? I don't know how to tell what gets triggered as a suggestion, do you?
There was a problem hiding this comment.
verify.getSuggestionDiagnostics should allow you to test that.
There was a problem hiding this comment.
There is one suggestion, but it's "'numeric' is declared but its value is never read.". I don't think noImplicitAny is leading to a infer-from-usage suggestion. It probably should someday, but I'd like to improve the precision and recall of it first.
|
|
||
| verify.fileAfterCodeFix( | ||
| `/** @param {number} a */ | ||
| /** |
There was a problem hiding this comment.
Could you file this as a bug? We should combine comments, not add a new one.
There was a problem hiding this comment.
I'll file it after merging the PR since the bug depends on being able to emit JSDoc in the first place.
I personally think the current approach is a good compromise and that we'll rarely have to generate partial JSDoc. The bigger lack is untyped JSDoc:
/** @param x Just a description, no type */
function f(x) {
return x * x
}| newFileContent: | ||
| `export class C { | ||
| /** | ||
| * @param {Promise<typeof import("/a")>} val |
There was a problem hiding this comment.
I think this should be "./a" when we synthesize the import. Doesn't look so bad in this example but in a real example this might import from /home/myname/projects/typescript/someproject/src/components/a.
There was a problem hiding this comment.
Wow, nice catch. I asked Wesley and he said that getTypeNodeIfAccessible needs to provide more members to the symbol tracker host (from Program and LanguageServiceHost). It sounds fairly simple, but it's not related to this change, so I'll put it in a separate PR.
|
Fixes #20113 |
src/services/textChanges.ts
Outdated
| } | ||
|
|
||
| public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void { | ||
| const printed = createPrinter().printNode(EmitHint.Unspecified, type, sourceFile); |
There was a problem hiding this comment.
Think this could also use getNonformattedText.
|
|
||
| public insertCommentThenNewline(sourceFile: SourceFile, character: number, position: number, commentText: string): void { | ||
| const token = getTouchingToken(sourceFile, position); | ||
| const text = "/**" + commentText + "*/" + this.newLineCharacter + repeatString(" ", character); |
There was a problem hiding this comment.
" " [](start = 91, length = 3)
Will the formatting done after the fix is applied convert this to tabs if that's what the user prefers?
There was a problem hiding this comment.
What we do for other fixes is to copy exactly the indentation string that they were using -- if their preferred indentation is tab, space, space, tab, space, space, we should copy that. I'll make a new PR unifying our comment code since #27565 has its own separate jsdoc-adding helpers.
There was a problem hiding this comment.
There are cases where we need to make to new indentation, though, as in the single-line test case that needs to annotate class C {m(x) {return x;}}
| commentText += this.printJSDocParameter(indent, printed, declaration.name, isOptional); | ||
| } | ||
| } | ||
| commentText += repeatString(" ", indent + 1); |
There was a problem hiding this comment.
commentText += repeatString(" ", indent + 1); [](start = 12, length = 45)
This seems like it belongs in insertCommentThenNewline (which would then either need to check for newlines in the text or accept a flag indicating whether or not the */ should be indented).
| if (isOptionalParameter) { | ||
| printName = `[${printName}]`; | ||
| } | ||
| return repeatString(" ", indent) + ` * @param {${printed}} ${printName}\n`; |
There was a problem hiding this comment.
repeatString(" ", indent) [](start = 19, length = 25)
Having these scattered through the code seems fragile. What about passing an array of lines to insertCommentThenNewline and letting it handle breaks and indentation?
|
|
||
| /** Note: output node may be mutated input node. */ | ||
| function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { | ||
| export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { |
There was a problem hiding this comment.
export [](start = 8, length = 6)
Why is this exported?
| this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " }); | ||
| } | ||
|
|
||
| public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void { |
There was a problem hiding this comment.
tryInsertJSDocType [](start = 15, length = 18)
I feel like I'm missing something obvious, but why "try"?
| changes.tryInsertJSDocParameters(sourceFile, result); | ||
| } | ||
|
|
||
| function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined { |
There was a problem hiding this comment.
[](start = 0, length = 3)
This looks unintentional.
| return callContexts && declaration.parameters.map((parameter, parameterIndex) => { | ||
| const types: Type[] = []; | ||
| const isRest = isRestParameter(parameter); | ||
| let isOptional = false; |
There was a problem hiding this comment.
isOptional [](start = 20, length = 10)
I don't understand this flag. I'll drop by.
| let isOptional = false; | ||
| for (const callContext of callContexts) { | ||
| if (callContext.argumentTypes.length <= parameterIndex) { | ||
| isOptional = isInJSFile(declaration); |
There was a problem hiding this comment.
isInJSFile [](start = 37, length = 10)
I assume isInJSFile is very cheap but I would habitually have either pulled it out of the loop or checked isOptional ||.
inferFromUsage now emits JSDoc. It emits
@typeby default, but@paramfor parameters and@returnfor get accessors. It does not emit@returntype annotations because the codefix itself does not. (It probably should, but I'll address that in a followup PR.)For example:
Gives the annotation
Currently, only
@paramblocks are multi-line.Note that I generate strings for JSDoc. I don't know enough about printing trivia to know whether it's worthwhile to generate a structure and then print it.