Skip to content

Commit 1281a38

Browse files
author
Brenda Huang
committed
convert JSDoc typedef to type, issue#50644
1 parent 555d174 commit 1281a38

9 files changed

+267
-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,187 @@
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(tagNode: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined {
67+
const typeName = tagNode.name?.getFullText().trim();
68+
const typeExpression = tagNode.typeExpression;
69+
if (!typeName || !typeExpression) return;
70+
71+
// for use case @typedef {object}Foo @property{bar}number
72+
if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
73+
return createDeclarationFromTypeLiteral(typeName, typeExpression);
74+
}
75+
// for use case @typedef {(number|string|undefined)} Foo or @typedef {number|string|undefined} Foo
76+
else if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
77+
return createDeclarationFromTypeExpression(typeName, typeExpression);
78+
}
79+
}
80+
81+
function createDeclarationFromTypeLiteral(typeName: string, typeExpression: JSDocTypeLiteral): InterfaceDeclaration | undefined {
82+
if (!typeName || !typeExpression) return;
83+
const propertyTags = typeExpression.jsDocPropertyTags;
84+
if (!propertyTags) return;
85+
86+
const propertySignatures = createPropertySignatures(propertyTags as JSDocPropertyTag[]);
87+
88+
if(!propertySignatures || propertySignatures.length === 0) return;
89+
const interfaceDeclaration = factory.createInterfaceDeclaration(
90+
[],
91+
typeName,
92+
[],
93+
[],
94+
propertySignatures,
95+
);
96+
return interfaceDeclaration;
97+
}
98+
99+
function createDeclarationFromTypeExpression(typeName: string, typeExpression: JSDocTypeExpression): TypeAliasDeclaration | undefined {
100+
if (!typeName || !typeExpression) return;
101+
102+
let type = typeExpression.type;
103+
if (type.kind === SyntaxKind.ParenthesizedType) {
104+
type = (type as ParenthesizedTypeNode).type;
105+
}
106+
107+
if (type.kind === SyntaxKind.UnionType) {
108+
return createTypeAliasForUnionType(type as UnionTypeNode, typeName);
109+
}
110+
}
111+
112+
function createPropertySignatures(tags: JSDocPropertyTag[]): PropertySignature[] | undefined{
113+
const props = tags.reduce((signatures: PropertySignature[], tag: JSDocPropertyTag) => {
114+
const propertyName = tag.name.getFullText().trim();
115+
const propertyType = (tag.typeExpression?.type)?.getFullText().trim();
116+
if(propertyName && propertyType) {
117+
const prop = factory.createPropertySignature(
118+
[],
119+
propertyName,
120+
// eslint-disable-next-line local/boolean-trivia
121+
undefined,
122+
factory.createTypeReferenceNode(propertyType)
123+
);
124+
return [...signatures, prop];
125+
}
126+
127+
}, []);
128+
return props;
129+
}
130+
131+
function createTypeAliasForUnionType(type: UnionTypeNode, typeName: string): TypeAliasDeclaration | undefined {
132+
const elements = type.types;
133+
const nodes = elements.reduce((nodeArray, element) => {
134+
const node = transformUnionTypeKeyword(element.kind);
135+
if (node) return [...nodeArray, node];
136+
}, []);
137+
if (!nodes) return;
138+
const typeReference = factory.createUnionTypeNode(nodes);
139+
const unionDeclaration = factory.createTypeAliasDeclaration(
140+
[],
141+
factory.createIdentifier(typeName),
142+
[],
143+
typeReference
144+
);
145+
return unionDeclaration;
146+
}
147+
148+
function transformUnionTypeKeyword(keyword: TypeNodeSyntaxKind): TypeNode | undefined {
149+
switch (keyword) {
150+
case SyntaxKind.NumberKeyword:
151+
return factory.createTypeReferenceNode("number");
152+
case SyntaxKind.StringKeyword:
153+
return factory.createTypeReferenceNode("string");
154+
case SyntaxKind.UndefinedKeyword:
155+
return factory.createTypeReferenceNode("undefined");
156+
case SyntaxKind.ObjectKeyword:
157+
return factory.createTypeReferenceNode("object");
158+
case SyntaxKind.VoidKeyword:
159+
return factory.createTypeReferenceNode("void");
160+
default:
161+
return;
162+
}
163+
}
164+
165+
export function _containsJSDocTypedef(node: Node): node is HasJSDoc {
166+
if (hasJSDocNodes(node)) {
167+
const jsDocNodes = node.jsDoc || [];
168+
return jsDocNodes.some((node) => {
169+
const tags = node.tags || [];
170+
return tags.some((tag) => isJSDocTypedefTag(tag));
171+
});
172+
}
173+
return false;
174+
}
175+
176+
export function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag {
177+
const jsDocNodes = node.jsDoc || [];
178+
179+
return flatMap(jsDocNodes, (node) => {
180+
const tags = node.tags || [];
181+
return tags.filter((tag) => isJSDocTypedefTag(tag));
182+
})[0] as unknown as JSDocTypedefTag;
183+
}
184+
185+
export function containsTypeDefTag(node: Node): node is JSDocTypedefTag {
186+
return isInJSDoc(node) && isJSDocTypedefTag(node);
187+
}

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
}

tests/baselines/reference/api/tsserverlibrary.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -9834,6 +9834,11 @@ declare namespace ts {
98349834
emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject<T> | undefined;
98359835
}
98369836
type InvalidatedProject<T extends BuilderProgram> = UpdateOutputFileStampsProject | BuildInvalidedProject<T> | UpdateBundleProject<T>;
9837+
namespace codefix {
9838+
function _containsJSDocTypedef(node: Node): node is HasJSDoc;
9839+
function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag;
9840+
function containsTypeDefTag(node: Node): node is JSDocTypedefTag;
9841+
}
98379842
function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings;
98389843
/**
98399844
* Represents an immutable snapshot of a script at a specified time.Once acquired, the

tests/baselines/reference/api/typescript.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -5969,6 +5969,11 @@ declare namespace ts {
59695969
readonly kind: ActionSet;
59705970
}
59715971
}
5972+
namespace codefix {
5973+
function _containsJSDocTypedef(node: Node): node is HasJSDoc;
5974+
function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag;
5975+
function containsTypeDefTag(node: Node): node is JSDocTypedefTag;
5976+
}
59725977
function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings;
59735978
/**
59745979
* Represents an immutable snapshot of a script at a specified time.Once acquired, the
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,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+
});

0 commit comments

Comments
 (0)