Skip to content

Commit 1b692de

Browse files
brendaHuang-2023Brenda Huang
and
Brenda Huang
authored
convert JSDoc typedef to type, issue 50644 (#51430)
Co-authored-by: Brenda Huang <[email protected]>
1 parent 02885b1 commit 1b692de

10 files changed

+303
-1
lines changed

src/compiler/diagnosticMessages.json

+17-1
Original file line numberDiff line numberDiff line change
@@ -6694,7 +6694,15 @@
66946694
"category": "Suggestion",
66956695
"code": 80008
66966696
},
6697-
6697+
"JSDoc typedef may be converted to TypeScript type.": {
6698+
"category": "Suggestion",
6699+
"code": 80009
6700+
},
6701+
"JSDoc typedefs may be converted to TypeScript types.": {
6702+
"category": "Suggestion",
6703+
"code": 80010
6704+
},
6705+
66986706
"Add missing 'super()' call": {
66996707
"category": "Message",
67006708
"code": 90001
@@ -7552,6 +7560,14 @@
75527560
"category": "Message",
75537561
"code": 95175
75547562
},
7563+
"Convert typedef to TypeScript type.": {
7564+
"category": "Message",
7565+
"code": 95176
7566+
},
7567+
"Convert all typedef to TypeScript types.": {
7568+
"category": "Message",
7569+
"code": 95177
7570+
},
75557571

75567572
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
75577573
"category": "Error",

src/services/_namespaces/ts.codefix.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from "../codefixes/convertToEsModule";
1717
export * from "../codefixes/correctQualifiedNameToIndexedAccessType";
1818
export * from "../codefixes/convertToTypeOnlyExport";
1919
export * from "../codefixes/convertToTypeOnlyImport";
20+
export * from "../codefixes/convertTypedefToType";
2021
export * from "../codefixes/convertLiteralTypeToMappedType";
2122
export * from "../codefixes/fixClassIncorrectlyImplementsInterface";
2223
export * from "../codefixes/importFixes";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
Diagnostics,
3+
factory,
4+
forEach,
5+
getSynthesizedDeepClone,
6+
getTokenAtPosition,
7+
hasJSDocNodes,
8+
InterfaceDeclaration,
9+
isJSDocTypedefTag,
10+
isJSDocTypeLiteral,
11+
JSDocPropertyLikeTag,
12+
JSDocTypedefTag,
13+
JSDocTypeExpression,
14+
JSDocTypeLiteral,
15+
mapDefined,
16+
Node,
17+
PropertySignature,
18+
some,
19+
SourceFile,
20+
SyntaxKind,
21+
textChanges,
22+
TypeAliasDeclaration,
23+
} from "../_namespaces/ts";
24+
import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix";
25+
26+
const fixId = "convertTypedefToType";
27+
const errorCodes = [Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type.code];
28+
registerCodeFix({
29+
fixIds: [fixId],
30+
errorCodes,
31+
getCodeActions(context) {
32+
const node = getTokenAtPosition(
33+
context.sourceFile,
34+
context.span.start
35+
);
36+
if (!node) return;
37+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile));
38+
39+
if (changes.length > 0) {
40+
return [
41+
createCodeFixAction(
42+
fixId,
43+
changes,
44+
Diagnostics.Convert_typedef_to_TypeScript_type,
45+
fixId,
46+
Diagnostics.Convert_all_typedef_to_TypeScript_types,
47+
),
48+
];
49+
}
50+
},
51+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
52+
const node = getTokenAtPosition(diag.file, diag.start);
53+
if (node) doChange(changes, node, diag.file);
54+
})
55+
});
56+
57+
function doChange(changes: textChanges.ChangeTracker, node: Node, sourceFile: SourceFile) {
58+
if (isJSDocTypedefTag(node)) {
59+
fixSingleTypeDef(changes, node, sourceFile);
60+
}
61+
}
62+
63+
function fixSingleTypeDef(
64+
changes: textChanges.ChangeTracker,
65+
typeDefNode: JSDocTypedefTag | undefined,
66+
sourceFile: SourceFile,
67+
) {
68+
if (!typeDefNode) return;
69+
70+
const declaration = createDeclaration(typeDefNode);
71+
if (!declaration) return;
72+
73+
const comment = typeDefNode.parent;
74+
75+
changes.replaceNode(
76+
sourceFile,
77+
comment,
78+
declaration
79+
);
80+
}
81+
82+
function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined {
83+
const { typeExpression } = tag;
84+
if (!typeExpression) return;
85+
const typeName = tag.name?.getText();
86+
if (!typeName) return;
87+
88+
// For use case @typedef {object}Foo @property{bar}number
89+
// But object type can be nested, meaning the value in the k/v pair can be object itself
90+
if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
91+
return createInterfaceForTypeLiteral(typeName, typeExpression);
92+
}
93+
// for use case @typedef {(number|string|undefined)} Foo or @typedef {number|string|undefined} Foo
94+
// In this case, we reach the leaf node of AST.
95+
if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
96+
return createTypeAliasForTypeExpression(typeName, typeExpression);
97+
}
98+
}
99+
100+
function createInterfaceForTypeLiteral(
101+
typeName: string,
102+
typeLiteral: JSDocTypeLiteral
103+
): InterfaceDeclaration | undefined {
104+
const propertySignatures = createSignatureFromTypeLiteral(typeLiteral);
105+
if (!some(propertySignatures)) return;
106+
const interfaceDeclaration = factory.createInterfaceDeclaration(
107+
/*modifiers*/ undefined,
108+
typeName,
109+
/*typeParameters*/ undefined,
110+
/*heritageClauses*/ undefined,
111+
propertySignatures,
112+
);
113+
return interfaceDeclaration;
114+
}
115+
116+
function createTypeAliasForTypeExpression(
117+
typeName: string,
118+
typeExpression: JSDocTypeExpression
119+
): TypeAliasDeclaration | undefined {
120+
const typeReference = getSynthesizedDeepClone(typeExpression.type);
121+
if (!typeReference) return;
122+
const declaration = factory.createTypeAliasDeclaration(
123+
/*modifiers*/ undefined,
124+
factory.createIdentifier(typeName),
125+
/*typeParameters*/ undefined,
126+
typeReference
127+
);
128+
return declaration;
129+
}
130+
131+
function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): PropertySignature[] | undefined {
132+
const propertyTags = typeLiteral.jsDocPropertyTags;
133+
if (!some(propertyTags)) return;
134+
135+
const getSignature = (tag: JSDocPropertyLikeTag) => {
136+
const name = getPropertyName(tag);
137+
const type = tag.typeExpression?.type;
138+
const isOptional = tag.isBracketed;
139+
let typeReference;
140+
141+
// Recursively handle nested object type
142+
if (type && isJSDocTypeLiteral(type)) {
143+
const signatures = createSignatureFromTypeLiteral(type);
144+
typeReference = factory.createTypeLiteralNode(signatures);
145+
}
146+
// Leaf node, where type.kind === SyntaxKind.JSDocTypeExpression
147+
else if (type) {
148+
typeReference = getSynthesizedDeepClone(type);
149+
}
150+
151+
if (typeReference && name) {
152+
const questionToken = isOptional ? factory.createToken(SyntaxKind.QuestionToken) : undefined;
153+
const prop = factory.createPropertySignature(
154+
/*modifiers*/ undefined,
155+
name,
156+
questionToken,
157+
typeReference
158+
);
159+
160+
return prop;
161+
}
162+
};
163+
164+
const props = mapDefined(propertyTags, getSignature);
165+
return props;
166+
}
167+
168+
function getPropertyName(tag: JSDocPropertyLikeTag): string | undefined {
169+
return tag.name.kind === SyntaxKind.Identifier ? tag.name.text : tag.name.right.text;
170+
}
171+
172+
/** @internal */
173+
export function getJSDocTypedefNode(node: Node): JSDocTypedefTag | undefined {
174+
if (hasJSDocNodes(node)) {
175+
return forEach(node.jsDoc, (node) => node.tags?.find(isJSDocTypedefTag));
176+
}
177+
return undefined;
178+
}

src/services/suggestionDiagnostics.ts

+5
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Pr
116116
}
117117
}
118118

119+
const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
120+
if (jsdocTypedefNode) {
121+
diags.push(createDiagnosticForNode(jsdocTypedefNode, Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type));
122+
}
123+
119124
if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
120125
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
121126
}

tests/baselines/reference/tsserver/plugins/getSupportedCodeFixes-can-be-proxied.js

+1
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ Info 32 [00:01:13.000] response:
309309
"2713",
310310
"1205",
311311
"1371",
312+
"80009",
312313
"2690",
313314
"2420",
314315
"2720",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////
4+
//// /**
5+
//// * @typedef {Object}Foo
6+
//// * @property {number}bar
7+
//// */
8+
////
9+
10+
verify.codeFix({
11+
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
12+
index: 0,
13+
newFileContent: `
14+
interface Foo {
15+
bar: number;
16+
}
17+
`,
18+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////
4+
//// /**
5+
//// * @typedef {(number|string|undefined)} Foo
6+
//// */
7+
////
8+
9+
verify.codeFix({
10+
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
11+
index: 0,
12+
newFileContent: `
13+
type Foo = (number | string | undefined);
14+
`,
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////
4+
//// /**
5+
//// * @typedef Foo
6+
//// * type {object}
7+
//// * @property {string} id - person's ID
8+
//// * @property name {string} // person's name
9+
//// * @property {number|undefined} age - person's age
10+
//// */
11+
////
12+
13+
verify.codeFix({
14+
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
15+
index: 0,
16+
newFileContent: `
17+
interface Foo {
18+
id: string;
19+
name: string;
20+
age: number | undefined;
21+
}
22+
`,
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////
4+
//// /**
5+
//// * @typedef {object} Person
6+
//// * @property {object} data
7+
//// * @property {string} data.name
8+
//// * @property {number} data.age
9+
//// * @property {object} data.contact
10+
//// * @property {string} data.contact.address
11+
//// * @property {string} [data.contact.phone]
12+
//// */
13+
////
14+
15+
verify.codeFix({
16+
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
17+
index: 0,
18+
newFileContent: `
19+
interface Person {
20+
data: {
21+
name: string;
22+
age: number;
23+
contact: {
24+
address: string;
25+
phone?: string;
26+
};
27+
};
28+
}
29+
`,
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////
4+
//// /**
5+
//// * @typedef {number} Foo
6+
//// */
7+
////
8+
9+
verify.codeFix({
10+
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
11+
index: 0,
12+
newFileContent: `
13+
type Foo = number;
14+
`,
15+
});

0 commit comments

Comments
 (0)