Skip to content

Commit 88d3c6f

Browse files
authored
inferFromUsage codefix now emits JSDoc in JS files (#27610)
* Now adding @type to variable declarations, at least Probably everything else, but not as well. * Improve @param output and add test It's still bad, but at least it's not wrong. * Add some js/inferFromUsage tests and fixes Also, remove redundant is(Set|Get)Accessor functions. * Fix @typedef refactor * Emit JSDoc optional parameters By surrounding the parameter name with brackets. It is super, super ugly right now. * Get rest of existing tests working * Correct location of comments * Handle @param blocks 1. Format multiple params nicely in a single-multiline block. 2. Generate only params that haven't already been documented. Existing documentation is not touched. * Re-add isGet/SetAccessor -- it is part of the API * Move isSet/GetAccessor back to the original location * Oh no I missed a newline and a space * Switch to an object type * A lot of cleanup More to come, though. annotate is only called in annotateVariableDeclaration where we don't know whether we're in JS or not. * Move and delegate to annotateJSDocParameters * Address PR comments * Lint: newline problems!!!! * Switch another call to getNonformattedText * Update baseline missed after merge
1 parent 1a5e669 commit 88d3c6f

24 files changed

+584
-40
lines changed

Diff for: src/compiler/transformers/declarations/diagnostics.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,4 @@ namespace ts {
466466
};
467467
}
468468
}
469-
}
469+
}

Diff for: src/harness/fourslash.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2516,10 +2516,10 @@ Actual: ${stringify(fullActual)}`);
25162516
* @param expectedContents The contents of the file after the fixes are applied.
25172517
* @param fileName The file to check. If not supplied, the current open file is used.
25182518
*/
2519-
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) {
2519+
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string, index?: number) {
25202520
fileName = fileName ? fileName : this.activeFile.fileName;
25212521

2522-
this.applyCodeActions(this.getCodeFixes(fileName));
2522+
this.applyCodeActions(this.getCodeFixes(fileName), index);
25232523

25242524
const actualContents: string = this.getFileContent(fileName);
25252525
if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) {
@@ -4388,6 +4388,10 @@ namespace FourSlashInterface {
43884388
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
43894389
}
43904390

4391+
public fileAfterCodeFix(expectedContents: string, fileName?: string, index?: number) {
4392+
this.state.verifyFileAfterCodeFix(expectedContents, fileName, index);
4393+
}
4394+
43914395
public codeFixAll(options: VerifyCodeFixAllOptions): void {
43924396
this.state.verifyCodeFixAll(options);
43934397
}

Diff for: src/services/codefixes/inferFromUsage.ts

+69-25
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ namespace ts.codefix {
2626
errorCodes,
2727
getCodeActions(context) {
2828
const { sourceFile, program, span: { start }, errorCode, cancellationToken, host } = context;
29-
if (isSourceFileJS(sourceFile)) {
30-
return undefined; // TODO: GH#20113
31-
}
3229

3330
const token = getTokenAtPosition(sourceFile, start);
3431
let declaration!: Declaration | undefined;
@@ -50,7 +47,7 @@ namespace ts.codefix {
5047
function getDiagnostic(errorCode: number, token: Node): DiagnosticMessage {
5148
switch (errorCode) {
5249
case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
53-
return isSetAccessor(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217
50+
return isSetAccessorDeclaration(getContainingFunction(token)!) ? Diagnostics.Infer_type_of_0_from_usage : Diagnostics.Infer_parameter_types_from_usage; // TODO: GH#18217
5451
case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code:
5552
return Diagnostics.Infer_parameter_types_from_usage;
5653
default:
@@ -59,7 +56,7 @@ namespace ts.codefix {
5956
}
6057

6158
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node, errorCode: number, program: Program, cancellationToken: CancellationToken, markSeen: NodeSeenTracker, host: LanguageServiceHost): Declaration | undefined {
62-
if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken) {
59+
if (!isParameterPropertyModifier(token.kind) && token.kind !== SyntaxKind.Identifier && token.kind !== SyntaxKind.DotDotDotToken && token.kind !== SyntaxKind.ThisKeyword) {
6360
return undefined;
6461
}
6562

@@ -72,6 +69,14 @@ namespace ts.codefix {
7269
annotateVariableDeclaration(changes, sourceFile, parent, program, host, cancellationToken);
7370
return parent;
7471
}
72+
if (isPropertyAccessExpression(parent)) {
73+
const type = inferTypeForVariableFromUsage(parent.name, program, cancellationToken);
74+
const typeNode = type && getTypeNodeIfAccessible(type, parent, program, host);
75+
if (typeNode) {
76+
changes.tryInsertJSDocType(sourceFile, parent, typeNode);
77+
}
78+
return parent;
79+
}
7580
return undefined;
7681

7782
case Diagnostics.Variable_0_implicitly_has_an_1_type.code: {
@@ -92,7 +97,7 @@ namespace ts.codefix {
9297
switch (errorCode) {
9398
// Parameter declarations
9499
case Diagnostics.Parameter_0_implicitly_has_an_1_type.code:
95-
if (isSetAccessor(containingFunction)) {
100+
if (isSetAccessorDeclaration(containingFunction)) {
96101
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken);
97102
return containingFunction;
98103
}
@@ -108,15 +113,15 @@ namespace ts.codefix {
108113
// Get Accessor declarations
109114
case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code:
110115
case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code:
111-
if (isGetAccessor(containingFunction) && isIdentifier(containingFunction.name)) {
116+
if (isGetAccessorDeclaration(containingFunction) && isIdentifier(containingFunction.name)) {
112117
annotate(changes, sourceFile, containingFunction, inferTypeForVariableFromUsage(containingFunction.name, program, cancellationToken), program, host);
113118
return containingFunction;
114119
}
115120
return undefined;
116121

117122
// Set Accessor declarations
118123
case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code:
119-
if (isSetAccessor(containingFunction)) {
124+
if (isSetAccessorDeclaration(containingFunction)) {
120125
annotateSetAccessor(changes, sourceFile, containingFunction, program, host, cancellationToken);
121126
return containingFunction;
122127
}
@@ -150,35 +155,61 @@ namespace ts.codefix {
150155
return;
151156
}
152157

153-
const types = inferTypeForParametersFromUsage(containingFunction, sourceFile, program, cancellationToken) ||
154-
containingFunction.parameters.map(p => isIdentifier(p.name) ? inferTypeForVariableFromUsage(p.name, program, cancellationToken) : undefined);
155-
// We didn't actually find a set of type inference positions matching each parameter position
156-
if (!types || containingFunction.parameters.length !== types.length) {
157-
return;
158-
}
158+
const parameterInferences = inferTypeForParametersFromUsage(containingFunction, sourceFile, program, cancellationToken) ||
159+
containingFunction.parameters.map<ParameterInference>(p => ({
160+
declaration: p,
161+
type: isIdentifier(p.name) ? inferTypeForVariableFromUsage(p.name, program, cancellationToken) : undefined
162+
}));
163+
Debug.assert(containingFunction.parameters.length === parameterInferences.length);
159164

160-
zipWith(containingFunction.parameters, types, (parameter, type) => {
161-
if (!parameter.type && !parameter.initializer) {
162-
annotate(changes, sourceFile, parameter, type, program, host);
165+
if (isInJSFile(containingFunction)) {
166+
annotateJSDocParameters(changes, sourceFile, parameterInferences, program, host);
167+
}
168+
else {
169+
for (const { declaration, type } of parameterInferences) {
170+
if (declaration && !declaration.type && !declaration.initializer) {
171+
annotate(changes, sourceFile, declaration, type, program, host);
172+
}
163173
}
164-
});
174+
}
165175
}
166176

167177
function annotateSetAccessor(changes: textChanges.ChangeTracker, sourceFile: SourceFile, setAccessorDeclaration: SetAccessorDeclaration, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken): void {
168178
const param = firstOrUndefined(setAccessorDeclaration.parameters);
169179
if (param && isIdentifier(setAccessorDeclaration.name) && isIdentifier(param.name)) {
170180
const type = inferTypeForVariableFromUsage(setAccessorDeclaration.name, program, cancellationToken) ||
171181
inferTypeForVariableFromUsage(param.name, program, cancellationToken);
172-
annotate(changes, sourceFile, param, type, program, host);
182+
if (isInJSFile(setAccessorDeclaration)) {
183+
annotateJSDocParameters(changes, sourceFile, [{ declaration: param, type }], program, host);
184+
}
185+
else {
186+
annotate(changes, sourceFile, param, type, program, host);
187+
}
173188
}
174189
}
175190

176191
function annotate(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: textChanges.TypeAnnotatable, type: Type | undefined, program: Program, host: LanguageServiceHost): void {
177192
const typeNode = type && getTypeNodeIfAccessible(type, declaration, program, host);
178-
if (typeNode) changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode);
193+
if (typeNode) {
194+
if (isInJSFile(sourceFile) && declaration.kind !== SyntaxKind.PropertySignature) {
195+
changes.tryInsertJSDocType(sourceFile, declaration, typeNode);
196+
}
197+
else {
198+
changes.tryInsertTypeAnnotation(sourceFile, declaration, typeNode);
199+
}
200+
}
179201
}
180202

181-
function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined {
203+
function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: ParameterInference[], program: Program, host: LanguageServiceHost): void {
204+
const result = mapDefined(parameterInferences, inference => {
205+
const param = inference.declaration;
206+
const typeNode = inference.type && getTypeNodeIfAccessible(inference.type, param, program, host);
207+
return typeNode && !param.initializer && !getJSDocType(param) ? { ...inference, typeNode } : undefined;
208+
});
209+
changes.tryInsertJSDocParameters(sourceFile, result);
210+
}
211+
212+
function getTypeNodeIfAccessible(type: Type, enclosingScope: Node, program: Program, host: LanguageServiceHost): TypeNode | undefined {
182213
const checker = program.getTypeChecker();
183214
let typeIsAccessible = true;
184215
const notAccessible = () => { typeIsAccessible = false; };
@@ -212,7 +243,7 @@ namespace ts.codefix {
212243
return InferFromReference.inferTypeFromReferences(getReferences(token, program, cancellationToken), program.getTypeChecker(), cancellationToken);
213244
}
214245

215-
function inferTypeForParametersFromUsage(containingFunction: FunctionLikeDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): (Type | undefined)[] | undefined {
246+
function inferTypeForParametersFromUsage(containingFunction: FunctionLikeDeclaration, sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): ParameterInference[] | undefined {
216247
switch (containingFunction.kind) {
217248
case SyntaxKind.Constructor:
218249
case SyntaxKind.FunctionExpression:
@@ -228,6 +259,13 @@ namespace ts.codefix {
228259
}
229260
}
230261

262+
interface ParameterInference {
263+
declaration: ParameterDeclaration;
264+
type?: Type;
265+
typeNode?: TypeNode;
266+
isOptional?: boolean;
267+
}
268+
231269
namespace InferFromReference {
232270
interface CallContext {
233271
argumentTypes: Type[];
@@ -255,7 +293,7 @@ namespace ts.codefix {
255293
return getTypeFromUsageContext(usageContext, checker);
256294
}
257295

258-
export function inferTypeForParametersFromReferences(references: ReadonlyArray<Identifier>, declaration: FunctionLikeDeclaration, checker: TypeChecker, cancellationToken: CancellationToken): (Type | undefined)[] | undefined {
296+
export function inferTypeForParametersFromReferences(references: ReadonlyArray<Identifier>, declaration: FunctionLikeDeclaration, checker: TypeChecker, cancellationToken: CancellationToken): ParameterInference[] | undefined {
259297
if (references.length === 0) {
260298
return undefined;
261299
}
@@ -274,8 +312,10 @@ namespace ts.codefix {
274312
return callContexts && declaration.parameters.map((parameter, parameterIndex) => {
275313
const types: Type[] = [];
276314
const isRest = isRestParameter(parameter);
315+
let isOptional = false;
277316
for (const callContext of callContexts) {
278317
if (callContext.argumentTypes.length <= parameterIndex) {
318+
isOptional = isInJSFile(declaration);
279319
continue;
280320
}
281321

@@ -289,10 +329,14 @@ namespace ts.codefix {
289329
}
290330
}
291331
if (!types.length) {
292-
return undefined;
332+
return { declaration: parameter };
293333
}
294334
const type = checker.getWidenedType(checker.getUnionType(types, UnionReduction.Subtype));
295-
return isRest ? checker.createArrayType(type) : type;
335+
return {
336+
type: isRest ? checker.createArrayType(type) : type,
337+
isOptional: isOptional && !isRest,
338+
declaration: parameter
339+
};
296340
});
297341
}
298342

Diff for: src/services/textChanges.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ namespace ts.textChanges {
209209

210210
export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature;
211211

212+
interface JSDocParameter {
213+
declaration: ParameterDeclaration;
214+
typeNode: TypeNode;
215+
isOptional?: boolean;
216+
}
217+
212218
export class ChangeTracker {
213219
private readonly changes: Change[] = [];
214220
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
@@ -339,6 +345,12 @@ namespace ts.textChanges {
339345
this.insertText(sourceFile, token.getStart(sourceFile), text);
340346
}
341347

348+
public insertCommentThenNewline(sourceFile: SourceFile, character: number, position: number, commentText: string): void {
349+
const token = getTouchingToken(sourceFile, position);
350+
const text = "/**" + commentText + "*/" + this.newLineCharacter + repeatString(" ", character);
351+
this.insertText(sourceFile, token.getStart(sourceFile), text);
352+
}
353+
342354
public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string) {
343355
this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text });
344356
}
@@ -347,6 +359,23 @@ namespace ts.textChanges {
347359
this.replaceRangeWithText(sourceFile, createRange(pos), text);
348360
}
349361

362+
public tryInsertJSDocParameters(sourceFile: SourceFile, parameters: JSDocParameter[]) {
363+
if (parameters.length === 0) {
364+
return;
365+
}
366+
const parent = parameters[0].declaration.parent;
367+
const indent = getLineAndCharacterOfPosition(sourceFile, parent.getStart()).character;
368+
let commentText = "\n";
369+
for (const { declaration, typeNode, isOptional } of parameters) {
370+
if (isIdentifier(declaration.name)) {
371+
const printed = changesToText.getNonformattedText(typeNode, sourceFile, this.newLineCharacter).text;
372+
commentText += this.printJSDocParameter(indent, printed, declaration.name, isOptional);
373+
}
374+
}
375+
commentText += repeatString(" ", indent + 1);
376+
this.insertCommentThenNewline(sourceFile, indent, parent.getStart(), commentText);
377+
}
378+
350379
/** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
351380
public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): void {
352381
let endNode: Node | undefined;
@@ -365,6 +394,27 @@ namespace ts.textChanges {
365394
this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
366395
}
367396

397+
public tryInsertJSDocType(sourceFile: SourceFile, node: Node, type: TypeNode): void {
398+
const printed = changesToText.getNonformattedText(type, sourceFile, this.newLineCharacter).text;
399+
let commentText;
400+
if (isGetAccessorDeclaration(node)) {
401+
commentText = ` @return {${printed}} `;
402+
}
403+
else {
404+
commentText = ` @type {${printed}} `;
405+
node = node.parent;
406+
}
407+
this.insertCommentThenNewline(sourceFile, getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).character, node.getStart(sourceFile), commentText);
408+
}
409+
410+
private printJSDocParameter(indent: number, printed: string, name: Identifier, isOptionalParameter: boolean | undefined) {
411+
let printName = unescapeLeadingUnderscores(name.escapedText);
412+
if (isOptionalParameter) {
413+
printName = `[${printName}]`;
414+
}
415+
return repeatString(" ", indent) + ` * @param {${printed}} ${printName}\n`;
416+
}
417+
368418
public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: ReadonlyArray<TypeParameterDeclaration>): void {
369419
// If no `(`, is an arrow function `x => x`, so use the pos of the first parameter
370420
const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile);
@@ -806,7 +856,7 @@ namespace ts.textChanges {
806856
}
807857

808858
/** Note: output node may be mutated input node. */
809-
function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } {
859+
export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } {
810860
const writer = new Writer(newLineCharacter);
811861
const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed;
812862
createPrinter({ newLine, neverAsciiEscape: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer);

Diff for: tests/cases/fourslash/codeFixInferFromUsageCallJS.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
// @checkJs: true
5+
// @noImplicitAny: true
6+
// @Filename: test.js
7+
////function wat(b) {
8+
//// b();
9+
////}
10+
11+
verify.codeFixAll({
12+
fixId: "inferFromUsage",
13+
fixAllDescription: "Infer all types from usage",
14+
newFileContent:
15+
`/**
16+
* @param {() => void} b
17+
*/
18+
function wat(b) {
19+
b();
20+
}`});

Diff for: tests/cases/fourslash/codeFixInferFromUsageJS.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
// @checkJs: true
5+
// @noImplicitAny: true
6+
// @Filename: test.js
7+
////[|var foo;|]
8+
////function f() {
9+
//// foo += 2;
10+
////}
11+
12+
verify.rangeAfterCodeFix("/** @type {number} */\nvar foo;",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 2);
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
// @checkJs: true
5+
// @noImplicitAny: true
6+
// @Filename: important.js
7+
8+
/////** @typedef {{ [|p |]}} I */
9+
/////** @type {I} */
10+
////var i;
11+
////i.p = 0;
12+
13+
14+
verify.rangeAfterCodeFix("p: number", undefined, undefined, 1);

0 commit comments

Comments
 (0)