Skip to content

Commit 970503c

Browse files
author
Brenda Huang
committed
convert JSDoc typedef to type, issue#50644
1 parent 9ede7ce commit 970503c

9 files changed

+347
-0
lines changed

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -7548,5 +7548,13 @@
75487548
"The value '{0}' cannot be used here.": {
75497549
"category": "Error",
75507550
"code": 18050
7551+
},
7552+
"Convert typedef to type": {
7553+
"category": "Message",
7554+
"code": 18051
7555+
},
7556+
"Convert all typedefs to types": {
7557+
"category": "Message",
7558+
"code": 18052
75517559
}
75527560
}

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

src/services/suggestionDiagnostics.ts

+5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Pr
6666
}
6767
}
6868

69+
if (codefix._containsJSDocTypedef(node)) {
70+
const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
71+
diags.push(createDiagnosticForNode(jsdocTypedefNode, Diagnostics.Convert_typedef_to_type));
72+
}
73+
6974
if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
7075
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
7176
}
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)