• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    Debug, Diagnostics, EmitFlags, emptyArray, factory, findChildOfKind, first, FunctionLikeDeclaration,
3    getJSDocReturnType, getJSDocType, getJSDocTypeParameterDeclarations, getTokenAtPosition, isArrowFunction,
4    isFunctionLikeDeclaration, isIdentifier, isJSDocIndexSignature, isParameter, JSDocFunctionType,
5    JSDocNonNullableType, JSDocNullableType, JSDocOptionalType, JSDocVariadicType, last, Node,
6    nullTransformationContext, ParameterDeclaration, PropertyDeclaration, PropertySignature, setEmitFlags, SourceFile,
7    SyntaxKind, textChanges, tryCast, TypeNode, TypeReferenceNode, VariableDeclaration, visitEachChild, visitNode,
8    visitNodes,
9} from "../_namespaces/ts";
10import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix";
11
12const fixId = "annotateWithTypeFromJSDoc";
13const errorCodes = [Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types.code];
14registerCodeFix({
15    errorCodes,
16    getCodeActions(context) {
17        const decl = getDeclaration(context.sourceFile, context.span.start);
18        if (!decl) return;
19        const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, decl));
20        return [createCodeFixAction(fixId, changes, Diagnostics.Annotate_with_type_from_JSDoc, fixId, Diagnostics.Annotate_everything_with_types_from_JSDoc)];
21    },
22    fixIds: [fixId],
23    getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
24        const decl = getDeclaration(diag.file, diag.start);
25        if (decl) doChange(changes, diag.file, decl);
26    }),
27});
28
29function getDeclaration(file: SourceFile, pos: number): DeclarationWithType | undefined {
30    const name = getTokenAtPosition(file, pos);
31    // For an arrow function with no name, 'name' lands on the first parameter.
32    return tryCast(isParameter(name.parent) ? name.parent.parent : name.parent, parameterShouldGetTypeFromJSDoc);
33}
34
35/** @internal */
36export type DeclarationWithType =
37    | FunctionLikeDeclaration
38    | VariableDeclaration
39    | PropertySignature
40    | PropertyDeclaration;
41
42/** @internal */
43export function parameterShouldGetTypeFromJSDoc(node: Node): node is DeclarationWithType {
44    return isDeclarationWithType(node) && hasUsableJSDoc(node);
45}
46
47function hasUsableJSDoc(decl: DeclarationWithType | ParameterDeclaration): boolean {
48    return isFunctionLikeDeclaration(decl)
49        ? decl.parameters.some(hasUsableJSDoc) || (!decl.type && !!getJSDocReturnType(decl))
50        : !decl.type && !!getJSDocType(decl);
51}
52
53function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, decl: DeclarationWithType): void {
54    if (isFunctionLikeDeclaration(decl) && (getJSDocReturnType(decl) || decl.parameters.some(p => !!getJSDocType(p)))) {
55        if (!decl.typeParameters) {
56            const typeParameters = getJSDocTypeParameterDeclarations(decl);
57            if (typeParameters.length) changes.insertTypeParameters(sourceFile, decl, typeParameters);
58        }
59        const needParens = isArrowFunction(decl) && !findChildOfKind(decl, SyntaxKind.OpenParenToken, sourceFile);
60        if (needParens) changes.insertNodeBefore(sourceFile, first(decl.parameters), factory.createToken(SyntaxKind.OpenParenToken));
61        for (const param of decl.parameters) {
62            if (!param.type) {
63                const paramType = getJSDocType(param);
64                if (paramType) changes.tryInsertTypeAnnotation(sourceFile, param, transformJSDocType(paramType));
65            }
66        }
67        if (needParens) changes.insertNodeAfter(sourceFile, last(decl.parameters), factory.createToken(SyntaxKind.CloseParenToken));
68        if (!decl.type) {
69            const returnType = getJSDocReturnType(decl);
70            if (returnType) changes.tryInsertTypeAnnotation(sourceFile, decl, transformJSDocType(returnType));
71        }
72    }
73    else {
74        const jsdocType = Debug.checkDefined(getJSDocType(decl), "A JSDocType for this declaration should exist"); // If not defined, shouldn't have been an error to fix
75        Debug.assert(!decl.type, "The JSDocType decl should have a type"); // If defined, shouldn't have been an error to fix.
76        changes.tryInsertTypeAnnotation(sourceFile, decl, transformJSDocType(jsdocType));
77    }
78}
79
80function isDeclarationWithType(node: Node): node is DeclarationWithType {
81    return isFunctionLikeDeclaration(node) ||
82        node.kind === SyntaxKind.VariableDeclaration ||
83        node.kind === SyntaxKind.PropertySignature ||
84        node.kind === SyntaxKind.PropertyDeclaration;
85}
86
87function transformJSDocType(node: TypeNode): TypeNode {
88    switch (node.kind) {
89        case SyntaxKind.JSDocAllType:
90        case SyntaxKind.JSDocUnknownType:
91            return factory.createTypeReferenceNode("any", emptyArray);
92        case SyntaxKind.JSDocOptionalType:
93            return transformJSDocOptionalType(node as JSDocOptionalType);
94        case SyntaxKind.JSDocNonNullableType:
95            return transformJSDocType((node as JSDocNonNullableType).type);
96        case SyntaxKind.JSDocNullableType:
97            return transformJSDocNullableType(node as JSDocNullableType);
98        case SyntaxKind.JSDocVariadicType:
99            return transformJSDocVariadicType(node as JSDocVariadicType);
100        case SyntaxKind.JSDocFunctionType:
101            return transformJSDocFunctionType(node as JSDocFunctionType);
102        case SyntaxKind.TypeReference:
103            return transformJSDocTypeReference(node as TypeReferenceNode);
104        default:
105            const visited = visitEachChild(node, transformJSDocType, nullTransformationContext);
106            setEmitFlags(visited, EmitFlags.SingleLine);
107            return visited;
108    }
109}
110
111function transformJSDocOptionalType(node: JSDocOptionalType) {
112    return factory.createUnionTypeNode([visitNode(node.type, transformJSDocType), factory.createTypeReferenceNode("undefined", emptyArray)]);
113}
114
115function transformJSDocNullableType(node: JSDocNullableType) {
116    return factory.createUnionTypeNode([visitNode(node.type, transformJSDocType), factory.createTypeReferenceNode("null", emptyArray)]);
117}
118
119function transformJSDocVariadicType(node: JSDocVariadicType) {
120    return factory.createArrayTypeNode(visitNode(node.type, transformJSDocType));
121}
122
123function transformJSDocFunctionType(node: JSDocFunctionType) {
124    // TODO: This does not properly handle `function(new:C, string)` per https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System#the-javascript-type-language
125    //       however we do handle it correctly in `serializeTypeForDeclaration` in checker.ts
126    return factory.createFunctionTypeNode(emptyArray, node.parameters.map(transformJSDocParameter), node.type ?? factory.createKeywordTypeNode(SyntaxKind.AnyKeyword));
127}
128
129function transformJSDocParameter(node: ParameterDeclaration) {
130    const index = node.parent.parameters.indexOf(node);
131    const isRest = node.type!.kind === SyntaxKind.JSDocVariadicType && index === node.parent.parameters.length - 1; // TODO: GH#18217
132    const name = node.name || (isRest ? "rest" : "arg" + index);
133    const dotdotdot = isRest ? factory.createToken(SyntaxKind.DotDotDotToken) : node.dotDotDotToken;
134    return factory.createParameterDeclaration(node.modifiers, dotdotdot, name, node.questionToken, visitNode(node.type, transformJSDocType), node.initializer);
135}
136
137function transformJSDocTypeReference(node: TypeReferenceNode) {
138    let name = node.typeName;
139    let args = node.typeArguments;
140    if (isIdentifier(node.typeName)) {
141        if (isJSDocIndexSignature(node)) {
142            return transformJSDocIndexSignature(node);
143        }
144        let text = node.typeName.text;
145        switch (node.typeName.text) {
146            case "String":
147            case "Boolean":
148            case "Object":
149            case "Number":
150                text = text.toLowerCase();
151                break;
152            case "array":
153            case "date":
154            case "promise":
155                text = text[0].toUpperCase() + text.slice(1);
156                break;
157        }
158        name = factory.createIdentifier(text);
159        if ((text === "Array" || text === "Promise") && !node.typeArguments) {
160            args = factory.createNodeArray([factory.createTypeReferenceNode("any", emptyArray)]);
161        }
162        else {
163            args = visitNodes(node.typeArguments, transformJSDocType);
164        }
165    }
166    return factory.createTypeReferenceNode(name, args);
167}
168
169function transformJSDocIndexSignature(node: TypeReferenceNode) {
170    const index = factory.createParameterDeclaration(
171        /*modifiers*/ undefined,
172        /*dotDotDotToken*/ undefined,
173        node.typeArguments![0].kind === SyntaxKind.NumberKeyword ? "n" : "s",
174        /*questionToken*/ undefined,
175        factory.createTypeReferenceNode(node.typeArguments![0].kind === SyntaxKind.NumberKeyword ? "number" : "string", []),
176        /*initializer*/ undefined);
177    const indexSignature = factory.createTypeLiteralNode([factory.createIndexSignature(/*modifiers*/ undefined, [index], node.typeArguments![1])]);
178    setEmitFlags(indexSignature, EmitFlags.SingleLine);
179    return indexSignature;
180}
181