Skip to content

Commit 6d218e2

Browse files
committed
Refactor JSDoc types to Typescript types
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.
1 parent 0abfd6a commit 6d218e2

File tree

5 files changed

+193
-4
lines changed

5 files changed

+193
-4
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3703,5 +3703,9 @@
37033703
"Extract to {0}": {
37043704
"category": "Message",
37053705
"code": 95004
3706+
},
3707+
"Convert to Typescript type": {
3708+
"category": "Message",
3709+
"code": 95005
37063710
}
37073711
}

src/compiler/emitter.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ namespace ts {
545545
case SyntaxKind.TypeReference:
546546
return emitTypeReference(<TypeReferenceNode>node);
547547
case SyntaxKind.FunctionType:
548+
case SyntaxKind.JSDocFunctionType:
548549
return emitFunctionType(<FunctionTypeNode>node);
549550
case SyntaxKind.ConstructorType:
550551
return emitConstructorType(<ConstructorTypeNode>node);
@@ -574,6 +575,18 @@ namespace ts {
574575
return emitMappedType(<MappedTypeNode>node);
575576
case SyntaxKind.LiteralType:
576577
return emitLiteralType(<LiteralTypeNode>node);
578+
case SyntaxKind.JSDocAllType:
579+
case SyntaxKind.JSDocUnknownType:
580+
write("any");
581+
break;
582+
case SyntaxKind.JSDocNullableType:
583+
return emitJSDocNullableType(node as JSDocNullableType);
584+
case SyntaxKind.JSDocNonNullableType:
585+
return emitJSDocNonNullableType(node as JSDocNonNullableType);
586+
case SyntaxKind.JSDocOptionalType:
587+
return emitJSDocOptionalType(node as JSDocOptionalType);
588+
case SyntaxKind.JSDocVariadicType:
589+
return emitJSDocVariadicType(node as JSDocVariadicType);
577590

578591
// Binding patterns
579592
case SyntaxKind.ObjectBindingPattern:
@@ -914,7 +927,15 @@ namespace ts {
914927
emitDecorators(node, node.decorators);
915928
emitModifiers(node, node.modifiers);
916929
emitIfPresent(node.dotDotDotToken);
917-
emit(node.name);
930+
if (node.name) {
931+
emit(node.name);
932+
}
933+
else if (node.parent.kind === SyntaxKind.JSDocFunctionType) {
934+
const i = (node.parent as JSDocFunctionType).parameters.indexOf(node);
935+
if (i > -1) {
936+
write("arg" + i);
937+
}
938+
}
918939
emitIfPresent(node.questionToken);
919940
emitWithPrefix(": ", node.type);
920941
emitExpressionWithPrefix(" = ", node.initializer);
@@ -1035,6 +1056,20 @@ namespace ts {
10351056
emit(node.type);
10361057
}
10371058

1059+
function emitJSDocNullableType(node: JSDocNullableType) {
1060+
emit(node.type);
1061+
write(" | null");
1062+
}
1063+
1064+
function emitJSDocNonNullableType(node: JSDocNonNullableType) {
1065+
emit(node.type);
1066+
}
1067+
1068+
function emitJSDocOptionalType(node: JSDocOptionalType) {
1069+
emit(node.type);
1070+
write(" | undefined");
1071+
}
1072+
10381073
function emitConstructorType(node: ConstructorTypeNode) {
10391074
write("new ");
10401075
emitTypeParameters(node, node.typeParameters);
@@ -1060,6 +1095,11 @@ namespace ts {
10601095
write("[]");
10611096
}
10621097

1098+
function emitJSDocVariadicType(node: JSDocVariadicType) {
1099+
emit(node.type);
1100+
write("[]");
1101+
}
1102+
10631103
function emitTupleType(node: TupleTypeNode) {
10641104
write("[");
10651105
emitList(node, node.elementTypes, ListFormat.TupleTypeElements);
@@ -2357,7 +2397,7 @@ namespace ts {
23572397
emitList(parentNode, parameters, ListFormat.Parameters);
23582398
}
23592399

2360-
function canEmitSimpleArrowHead(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray<ParameterDeclaration>) {
2400+
function canEmitSimpleArrowHead(parentNode: FunctionTypeNode | ArrowFunction | JSDocFunctionType, parameters: NodeArray<ParameterDeclaration>) {
23612401
const parameter = singleOrUndefined(parameters);
23622402
return parameter
23632403
&& parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter
@@ -2374,7 +2414,7 @@ namespace ts {
23742414
&& isIdentifier(parameter.name); // parameter name must be identifier
23752415
}
23762416

2377-
function emitParametersForArrow(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray<ParameterDeclaration>) {
2417+
function emitParametersForArrow(parentNode: FunctionTypeNode | ArrowFunction | JSDocFunctionType, parameters: NodeArray<ParameterDeclaration>) {
23782418
if (canEmitSimpleArrowHead(parentNode, parameters)) {
23792419
emitList(parentNode, parameters, ListFormat.Parameters & ~ListFormat.Parenthesis);
23802420
}

src/compiler/utilities.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5034,7 +5034,14 @@ namespace ts {
50345034
|| kind === SyntaxKind.UndefinedKeyword
50355035
|| kind === SyntaxKind.NullKeyword
50365036
|| kind === SyntaxKind.NeverKeyword
5037-
|| kind === SyntaxKind.ExpressionWithTypeArguments;
5037+
|| kind === SyntaxKind.ExpressionWithTypeArguments
5038+
|| kind === SyntaxKind.JSDocAllType
5039+
|| kind === SyntaxKind.JSDocUnknownType
5040+
|| kind === SyntaxKind.JSDocNullableType
5041+
|| kind === SyntaxKind.JSDocNonNullableType
5042+
|| kind === SyntaxKind.JSDocOptionalType
5043+
|| kind === SyntaxKind.JSDocFunctionType
5044+
|| kind === SyntaxKind.JSDocVariadicType;
50385045
}
50395046

50405047
/**
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/* @internal */
2+
namespace ts.refactor.convertJSDocToTypes {
3+
const actionName = "convert";
4+
5+
const convertJSDocToTypes: Refactor = {
6+
name: "Convert to Typescript type",
7+
description: Diagnostics.Convert_to_Typescript_type.message,
8+
getEditsForAction,
9+
getAvailableActions
10+
};
11+
12+
type DeclarationWithType =
13+
| FunctionLikeDeclaration
14+
| VariableDeclaration
15+
| ParameterDeclaration
16+
| PropertySignature
17+
| PropertyDeclaration;
18+
19+
registerRefactor(convertJSDocToTypes);
20+
21+
function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
22+
if (isInJavaScriptFile(context.file)) {
23+
return undefined;
24+
}
25+
26+
const node = getTokenAtPosition(context.file, context.startPosition, /*includeJsDocComment*/ false);
27+
const decl = findAncestor(node, isTypedNode);
28+
if (decl && (getJSDocType(decl) || getJSDocReturnType(decl)) && !decl.type) {
29+
return [
30+
{
31+
name: convertJSDocToTypes.name,
32+
description: convertJSDocToTypes.description,
33+
actions: [
34+
{
35+
description: convertJSDocToTypes.description,
36+
name: actionName
37+
}
38+
]
39+
}
40+
];
41+
}
42+
}
43+
44+
function getEditsForAction(context: RefactorContext, action: string): RefactorEditInfo | undefined {
45+
// Somehow wrong action got invoked?
46+
if (actionName !== action) {
47+
Debug.fail(`actionName !== action: ${actionName} !== ${action}`);
48+
return undefined;
49+
}
50+
51+
const start = context.startPosition;
52+
const sourceFile = context.file;
53+
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);
54+
const decl = findAncestor(token, isTypedNode);
55+
const jsdocType = getJSDocType(decl);
56+
const jsdocReturn = getJSDocReturnType(decl);
57+
if (!decl || !jsdocType && !jsdocReturn || decl.type) {
58+
Debug.fail(`!decl || !jsdocType && !jsdocReturn || decl.type: !${decl} || !${jsdocType} && !{jsdocReturn} || ${decl.type}`);
59+
return undefined;
60+
}
61+
62+
const changeTracker = textChanges.ChangeTracker.fromContext(context);
63+
if (isParameterOfSimpleArrowFunction(decl)) {
64+
// `x => x` becomes `(x: number) => x`, but in order to make the changeTracker generate the parentheses,
65+
// we have to replace the entire function; it doesn't check that the node it's replacing might require
66+
// other syntax changes
67+
const arrow = decl.parent as ArrowFunction;
68+
const param = decl as ParameterDeclaration;
69+
const replacementParam = createParameter(param.decorators, param.modifiers, param.dotDotDotToken, param.name, param.questionToken, jsdocType, param.initializer);
70+
const replacement = createArrowFunction(arrow.modifiers, arrow.typeParameters, [replacementParam], arrow.type, arrow.equalsGreaterThanToken, arrow.body);
71+
changeTracker.replaceRange(sourceFile, { pos: arrow.getStart(), end: arrow.end }, replacement);
72+
}
73+
else {
74+
changeTracker.replaceRange(sourceFile, { pos: decl.getStart(), end: decl.end }, replaceType(decl, jsdocType, jsdocReturn));
75+
}
76+
return {
77+
edits: changeTracker.getChanges(),
78+
renameFilename: undefined,
79+
renameLocation: undefined
80+
};
81+
}
82+
83+
function isTypedNode(node: Node): node is DeclarationWithType {
84+
return isFunctionLikeDeclaration(node) ||
85+
node.kind === SyntaxKind.VariableDeclaration ||
86+
node.kind === SyntaxKind.Parameter ||
87+
node.kind === SyntaxKind.PropertySignature ||
88+
node.kind === SyntaxKind.PropertyDeclaration;
89+
}
90+
91+
function replaceType(decl: DeclarationWithType, jsdocType: TypeNode, jsdocReturn: TypeNode) {
92+
switch (decl.kind) {
93+
case SyntaxKind.VariableDeclaration:
94+
return createVariableDeclaration(decl.name, jsdocType, decl.initializer);
95+
case SyntaxKind.Parameter:
96+
return createParameter(decl.decorators, decl.modifiers, decl.dotDotDotToken, decl.name, decl.questionToken, jsdocType, decl.initializer);
97+
case SyntaxKind.PropertySignature:
98+
return createPropertySignature(decl.modifiers, decl.name, decl.questionToken, jsdocType, decl.initializer);
99+
case SyntaxKind.PropertyDeclaration:
100+
return createProperty(decl.decorators, decl.modifiers, decl.name, decl.questionToken, jsdocType, decl.initializer);
101+
case SyntaxKind.FunctionDeclaration:
102+
return createFunctionDeclaration(decl.decorators, decl.modifiers, decl.asteriskToken, decl.name, decl.typeParameters, decl.parameters, jsdocReturn, decl.body);
103+
case SyntaxKind.FunctionExpression:
104+
return createFunctionExpression(decl.modifiers, decl.asteriskToken, decl.name, decl.typeParameters, decl.parameters, jsdocReturn, decl.body);
105+
case SyntaxKind.ArrowFunction:
106+
return createArrowFunction(decl.modifiers, decl.typeParameters, decl.parameters, jsdocReturn, decl.equalsGreaterThanToken, decl.body);
107+
case SyntaxKind.MethodDeclaration:
108+
return createMethod(decl.decorators, decl.modifiers, decl.asteriskToken, decl.name, decl.questionToken, decl.typeParameters, decl.parameters, jsdocReturn, decl.body);
109+
case SyntaxKind.GetAccessor:
110+
return createGetAccessor(decl.decorators, decl.modifiers, decl.name, decl.parameters, jsdocReturn, decl.body);
111+
default:
112+
Debug.fail(`Unexpected SyntaxKind: ${decl.kind}`);
113+
return undefined;
114+
}
115+
}
116+
117+
function isParameterOfSimpleArrowFunction(decl: DeclarationWithType) {
118+
return decl.kind === SyntaxKind.Parameter && decl.parent.kind === SyntaxKind.ArrowFunction && isSimpleArrowFunction(decl.parent);
119+
}
120+
121+
function isSimpleArrowFunction(parentNode: FunctionTypeNode | ArrowFunction | JSDocFunctionType) {
122+
const parameter = singleOrUndefined(parentNode.parameters);
123+
return parameter
124+
&& parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter
125+
&& !(isArrowFunction(parentNode) && parentNode.type) // arrow function may not have return type annotation
126+
&& !some(parentNode.decorators) // parent may not have decorators
127+
&& !some(parentNode.modifiers) // parent may not have modifiers
128+
&& !some(parentNode.typeParameters) // parent may not have type parameters
129+
&& !some(parameter.decorators) // parameter may not have decorators
130+
&& !some(parameter.modifiers) // parameter may not have modifiers
131+
&& !parameter.dotDotDotToken // parameter may not be rest
132+
&& !parameter.questionToken // parameter may not be optional
133+
&& !parameter.type // parameter may not have a type annotation
134+
&& !parameter.initializer // parameter may not have an initializer
135+
&& isIdentifier(parameter.name); // parameter name must be identifier
136+
}
137+
}

src/services/refactors/refactors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
/// <reference path="convertJSDocToTypes.ts" />
12
/// <reference path="convertFunctionToEs6Class.ts" />
23
/// <reference path="extractMethod.ts" />

0 commit comments

Comments
 (0)