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

inferFromUsage codefix now emits JSDoc in JS files #27610

Merged
merged 24 commits into from
Oct 9, 2018
Merged

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Oct 8, 2018

inferFromUsage now emits JSDoc. It emits @type by default, but @param for parameters and @return for get accessors. It does not emit @return type annotations because the codefix itself does not. (It probably should, but I'll address that in a followup PR.)

For example:

function f(x) {
}
f(1)

Gives the annotation

/** 
 * @param {number} x 
 */

Currently, only @param blocks 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.

@sandersn sandersn requested review from a user and amcasey October 8, 2018 16:13
@@ -50,7 +47,7 @@ namespace ts.codefix {
function getDiagnostic(errorCode: number, token: Node): DiagnosticMessage {
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
Copy link

@ghost ghost Oct 8, 2018

Choose a reason for hiding this comment

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

Why do we have both of these -- isSetAccessor and isSetAccessorDeclaration?

Copy link
Member Author

Choose a reason for hiding this comment

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

No reason, but removing one would change the public API, so I decided not do it in this PR.

src/services/codefixes/inferFromUsage.ts Outdated Show resolved Hide resolved
src/services/codefixes/inferFromUsage.ts Outdated Show resolved Hide resolved
src/services/codefixes/inferFromUsage.ts Outdated Show resolved Hide resolved
src/services/codefixes/inferFromUsage.ts Outdated Show resolved Hide resolved
src/services/textChanges.ts Outdated Show resolved Hide resolved

// @allowJs: true
// @checkJs: true
// @noImplicitAny: true
Copy link

Choose a reason for hiding this comment

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

I think this shouldn't be necessary because we should make it a suggestion diagnostic anyway, if we don't already.

Copy link
Member Author

Choose a reason for hiding this comment

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

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?

Copy link

Choose a reason for hiding this comment

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

verify.getSuggestionDiagnostics should allow you to test that.

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filed #27624


verify.fileAfterCodeFix(
`/** @param {number} a */
/**
Copy link

Choose a reason for hiding this comment

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

Could you file this as a bug? We should combine comments, not add a new one.

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

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

@ghost
Copy link

ghost commented Oct 8, 2018

Fixes #20113

@@ -365,6 +394,27 @@ namespace ts.textChanges {
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
}

public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void {
const printed = createPrinter().printNode(EmitHint.Unspecified, type, sourceFile);
Copy link

Choose a reason for hiding this comment

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

Think this could also use getNonformattedText.

@sandersn sandersn merged commit 88d3c6f into master Oct 9, 2018
@sandersn sandersn deleted the js/infer-from-usage branch October 9, 2018 17:38
@@ -339,6 +345,12 @@ namespace ts.textChanges {
this.insertText(sourceFile, token.getStart(sourceFile), text);
}

public insertCommentThenNewline(sourceFile: SourceFile, character: number, position: number, commentText: string): void {
const token = getTouchingToken(sourceFile, position);
const text = "/**" + commentText + "*/" + this.newLineCharacter + repeatString(" ", character);
Copy link
Member

Choose a reason for hiding this comment

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

" " [](start = 91, length = 3)

Will the formatting done after the fix is applied convert this to tabs if that's what the user prefers?

Copy link

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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?

@@ -806,7 +856,7 @@ namespace ts.textChanges {
}

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

Choose a reason for hiding this comment

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

export [](start = 8, length = 6)

Why is this exported?

@@ -365,6 +394,27 @@ namespace ts.textChanges {
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
}

public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void {
Copy link
Member

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

[](start = 0, length = 3)

This looks unintentional.

@@ -274,8 +312,10 @@ namespace ts.codefix {
return callContexts && declaration.parameters.map((parameter, parameterIndex) => {
const types: Type[] = [];
const isRest = isRestParameter(parameter);
let isOptional = false;
Copy link
Member

Choose a reason for hiding this comment

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

isOptional [](start = 20, length = 10)

I don't understand this flag. I'll drop by.

for (const callContext of callContexts) {
if (callContext.argumentTypes.length <= parameterIndex) {
isOptional = isInJSFile(declaration);
Copy link
Member

Choose a reason for hiding this comment

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants