Skip to content

Commit fc00ee7

Browse files
author
Brenda Huang
committed
convert JSDoc typedef to type, issue#50644
1 parent cb4c768 commit fc00ee7

9 files changed

+364
-1
lines changed

src/compiler/diagnosticMessages.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -6622,7 +6622,14 @@
66226622
"category": "Suggestion",
66236623
"code": 80008
66246624
},
6625-
6625+
"JSDoc typedef may be converted to type": {
6626+
"category": "Suggestion",
6627+
"code": 80009
6628+
},
6629+
"JSDoc typedefs may be converted to types": {
6630+
"category": "Suggestion",
6631+
"code": 80010
6632+
},
66266633
"Add missing 'super()' call": {
66276634
"category": "Message",
66286635
"code": 90001

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,249 @@
1+
import {
2+
Diagnostics,
3+
factory,
4+
flatMap,
5+
getTokenAtPosition,
6+
HasJSDoc,
7+
hasJSDocNodes,
8+
InterfaceDeclaration,
9+
isInJSDoc,
10+
isJSDocTypedefTag,
11+
JSDocPropertyTag,
12+
JSDocTypedefTag,
13+
JSDocTypeExpression,
14+
JSDocTypeLiteral,
15+
Node,
16+
ParenthesizedTypeNode,
17+
PropertySignature,
18+
reduceLeft,
19+
some,
20+
SourceFile,
21+
SyntaxKind,
22+
textChanges,
23+
TypeAliasDeclaration,
24+
TypeNode,
25+
TypeNodeSyntaxKind,
26+
UnionTypeNode,
27+
} from "../_namespaces/ts";
28+
import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix";
29+
30+
const fixId = "convertTypedefToType";
31+
const errorCodes = [Diagnostics.JSDoc_typedef_may_be_converted_to_type.code];
32+
registerCodeFix({
33+
fixIds: [fixId],
34+
errorCodes,
35+
getCodeActions(context) {
36+
const node = getTokenAtPosition(
37+
context.sourceFile,
38+
context.span.start
39+
);
40+
if (!node) return;
41+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile));
42+
43+
if (changes.length > 0) {
44+
return [
45+
createCodeFixAction(
46+
fixId,
47+
changes,
48+
Diagnostics.JSDoc_typedef_may_be_converted_to_type,
49+
fixId,
50+
Diagnostics.JSDoc_typedefs_may_be_converted_to_types
51+
),
52+
];
53+
}
54+
},
55+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
56+
const node = getTokenAtPosition(diag.file, diag.start);
57+
if (node) doChange(changes, node, diag.file);
58+
})
59+
});
60+
61+
function doChange(changes: textChanges.ChangeTracker, node: Node, sourceFile: SourceFile) {
62+
if (containsTypeDefTag(node)) {
63+
fixSingleTypeDef(changes, node, sourceFile);
64+
}
65+
}
66+
67+
function fixSingleTypeDef(
68+
changes: textChanges.ChangeTracker,
69+
typeDefNode: JSDocTypedefTag | undefined,
70+
sourceFile: SourceFile,
71+
) {
72+
if (!typeDefNode) return;
73+
74+
const declaration = createDeclaration(typeDefNode);
75+
if (!declaration) return;
76+
77+
const comment = typeDefNode.parent;
78+
79+
changes.replaceNode(
80+
sourceFile,
81+
comment,
82+
declaration
83+
);
84+
}
85+
86+
function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined {
87+
const { typeExpression } = tag;
88+
if (!typeExpression) return;
89+
const typeName = tag.name?.getFullText().trim();
90+
if (!typeName) return;
91+
92+
// For use case @typedef {object}Foo @property{bar}number
93+
// But object type can be nested, meaning the value in the k/v pair can be object itself
94+
if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
95+
return createInterfaceForTypeLiteral(typeName, typeExpression);
96+
}
97+
// for use case @typedef {(number|string|undefined)} Foo or @typedef {number|string|undefined} Foo
98+
// In this case, we reach the leaf node of AST.
99+
// Here typeExpression.type is a TypeNode, e.g. a UnionType or a primitive type, such as "number".
100+
if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
101+
return createTypeAliasForTypeExpression(typeName, typeExpression);
102+
}
103+
}
104+
105+
function createInterfaceForTypeLiteral(
106+
typeName: string,
107+
typeLiteral: JSDocTypeLiteral
108+
): InterfaceDeclaration | undefined {
109+
const propertySignatures = createSignatureFromTypeLiteral(typeLiteral);
110+
if (!propertySignatures || propertySignatures.length === 0) return;
111+
const interfaceDeclaration = factory.createInterfaceDeclaration(
112+
[],
113+
typeName,
114+
[],
115+
[],
116+
propertySignatures,
117+
);
118+
return interfaceDeclaration;
119+
}
120+
121+
function createTypeAliasForTypeExpression(
122+
typeName: string,
123+
typeExpression: JSDocTypeExpression
124+
): TypeAliasDeclaration | undefined {
125+
const typeReference = createTypeReference(typeExpression.type);
126+
if (!typeReference) return;
127+
const declaration = factory.createTypeAliasDeclaration(
128+
[],
129+
factory.createIdentifier(typeName),
130+
[],
131+
typeReference
132+
);
133+
return declaration;
134+
}
135+
136+
function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): PropertySignature[] | undefined {
137+
const propertyTags = typeLiteral.jsDocPropertyTags;
138+
if (!propertyTags || propertyTags.length === 0) return;
139+
140+
const getSignatures = (signatures: PropertySignature[], tag: JSDocPropertyTag) => {
141+
const name = getPropertyName(tag);
142+
const type = tag.typeExpression?.type;
143+
let typeReference;
144+
145+
// Recursively handle nested object type
146+
if (type && type.kind === SyntaxKind.JSDocTypeLiteral) {
147+
const signatures = createSignatureFromTypeLiteral(type as JSDocTypeLiteral);
148+
typeReference = factory.createTypeLiteralNode(signatures);
149+
}
150+
// Leaf node, where type.kind === SyntaxKind.JSDocTypeExpression
151+
else if (type) {
152+
typeReference = createTypeReference(type);
153+
}
154+
if (typeReference && name) {
155+
const prop = factory.createPropertySignature(
156+
[],
157+
name,
158+
// eslint-disable-next-line local/boolean-trivia
159+
undefined,
160+
typeReference
161+
);
162+
return [...signatures, prop];
163+
}
164+
};
165+
166+
const props = reduceLeft(propertyTags, getSignatures, []);
167+
return props;
168+
}
169+
170+
function getPropertyName(tag: JSDocPropertyTag): string | undefined {
171+
const { name } = tag;
172+
if (!name) return;
173+
174+
let propertyName;
175+
// for "@property {string} parent.child" or "@property {string} parent.child.grandchild" in nested object type
176+
// We'll get "child" in the first example or "grandchild" in the second example as the prop name
177+
if (name.kind === SyntaxKind.QualifiedName) {
178+
propertyName = name.right.getFullText().trim();
179+
}
180+
else {
181+
propertyName = tag.name.getFullText().trim();
182+
}
183+
return propertyName;
184+
}
185+
186+
// Create TypeReferenceNode when we reach the leaf node of AST
187+
function createTypeReference(type: TypeNode): TypeNode | undefined {
188+
let typeReference;
189+
if (type.kind === SyntaxKind.ParenthesizedType) {
190+
type = (type as ParenthesizedTypeNode).type;
191+
}
192+
// Create TypeReferenceNode for UnionType
193+
if (type.kind === SyntaxKind.UnionType) {
194+
const elements = (type as UnionTypeNode).types;
195+
const nodes = reduceLeft(
196+
elements,
197+
(nodeArray, element) => {
198+
const node = transformUnionTypeKeyword(element.kind);
199+
if (node) return [...nodeArray, node];
200+
},
201+
[]
202+
);
203+
204+
if (!nodes) return;
205+
typeReference = factory.createUnionTypeNode(nodes);
206+
}
207+
//Create TypeReferenceNode for primitive types
208+
else {
209+
typeReference = transformUnionTypeKeyword(type.kind);
210+
}
211+
return typeReference;
212+
}
213+
214+
function transformUnionTypeKeyword(keyword: TypeNodeSyntaxKind): TypeNode | undefined {
215+
switch (keyword) {
216+
case SyntaxKind.NumberKeyword:
217+
return factory.createTypeReferenceNode("number");
218+
case SyntaxKind.StringKeyword:
219+
return factory.createTypeReferenceNode("string");
220+
case SyntaxKind.UndefinedKeyword:
221+
return factory.createTypeReferenceNode("undefined");
222+
case SyntaxKind.ObjectKeyword:
223+
return factory.createTypeReferenceNode("object");
224+
case SyntaxKind.VoidKeyword:
225+
return factory.createTypeReferenceNode("void");
226+
default:
227+
return;
228+
}
229+
}
230+
231+
/** @internal */
232+
export function containsJSDocTypedef(node: Node): node is HasJSDoc {
233+
return hasJSDocNodes(node) && some(node.jsDoc, node => some(node.tags, tag => isJSDocTypedefTag(tag)));
234+
}
235+
236+
/** @internal */
237+
export function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag {
238+
const jsDocNodes = node.jsDoc || [];
239+
240+
return flatMap(jsDocNodes, (node) => {
241+
const tags = node.tags || [];
242+
return tags.filter((tag) => isJSDocTypedefTag(tag));
243+
})[0] as unknown as JSDocTypedefTag;
244+
}
245+
246+
/** @internal */
247+
export function containsTypeDefTag(node: Node): node is JSDocTypedefTag {
248+
return isInJSDoc(node) && isJSDocTypedefTag(node);
249+
}

src/services/suggestionDiagnostics.ts

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

120+
if (codefix.containsJSDocTypedef(node)) {
121+
const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
122+
diags.push(createDiagnosticForNode(jsdocTypedefNode, Diagnostics.Convert_typedef_to_type));
123+
}
124+
120125
if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
121126
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
122127
}
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_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_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 {string} name - person's name
9+
//// * @property {number} age - person's age
10+
//// */
11+
////
12+
13+
verify.codeFix({
14+
description: ts.Diagnostics.Convert_typedef_to_type.message,
15+
index: 0,
16+
newFileContent: `
17+
interface Foo {
18+
id: string;
19+
name: string;
20+
age: number;
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_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_type.message,
11+
index: 0,
12+
newFileContent: `
13+
type Foo = number;
14+
`,
15+
});

0 commit comments

Comments
 (0)