Skip to content

Commit 12a6d88

Browse files
committed
Wip codeaction for microsoft#50644
diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 7a8f319..12a8ab2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5146,7 +5146,7 @@ "category": "Error", "code": 6258 }, - "Found 1 error in {1}": { + "Found 1 error in {1}": { "category": "Message", "code": 6259 }, @@ -6028,9 +6028,9 @@ "category": "Message", "code": 6930 }, - "List of file name suffixes to search when resolving a module." : { - "category": "Error", - "code": 6931 + "List of file name suffixes to search when resolving a module.": { + "category": "Error", + "code": 6931 }, "Variable '{0}' implicitly has an '{1}' type.": { @@ -7368,7 +7368,6 @@ "category": "Message", "code": 95175 }, - "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", "code": 18004 @@ -7524,5 +7523,13 @@ "The value '{0}' cannot be used here.": { "category": "Error", "code": 18050 + }, + "Convert @typedef to type": { + "category": "Message", + "code": 18051 + }, + "Convert all @typedefs to types": { + "category": "Message", + "code": 18051 } } diff --git a/src/services/codefixes/convertTypedefToType.ts b/src/services/codefixes/convertTypedefToType.ts new file mode 100644 index 0000000000..fbb8d53 --- /dev/null +++ b/src/services/codefixes/convertTypedefToType.ts @@ -0,0 +1,165 @@ +namespace ts.codefix { + const fixId = "convertTypedefToType"; + registerCodeFix({ + fixIds: [fixId], + errorCodes: [Diagnostics.Convert_typedef_to_type.code], + getCodeActions(context) { + debugger; + const checker = context.program.getTypeChecker(); + const info = getInfo( + context.sourceFile, + context.span.start, + checker + ); + const changes = textChanges.ChangeTracker.with(context, (t) => { + const node = getTokenAtPosition( + context.sourceFile, + context.span.start + ); + + if (containsTypeDefTag(node)) { + fixSingleTypeDef(t, node, context, info?.typeNode); + } + }); + + if (changes.length > 0) { + return [ + createCodeFixAction( + fixId, + changes, + Diagnostics.Convert_typedef_to_type, + fixId, + Diagnostics.Convert_all_typedefs_to_types + ), + ]; + } + }, + getAllCodeActions() { + debugger; + return { changes: [] }; + }, + }); + + function fixSingleTypeDef( + changes: textChanges.ChangeTracker, + typeDefNode: JSDocTypedefTag | undefined, + context: CodeFixContextBase, + typeNode?: TypeNode + ) { + if (!typeDefNode?.name || !typeDefNode?.fullName || !typeNode) { + return undefined; + } + + const comment = typeDefNode.parent; + + changes.replaceNode( + context.sourceFile, + comment, + factory.createTypeAliasDeclaration( + [], + typeDefNode.fullName.getFullText(), + getTypeParameters(typeDefNode), + typeNode + ) + // factory.createTypeParameterDeclaration() + ); + } + + function getInfo( + sourceFile: SourceFile, + pos: number, + checker: TypeChecker + ): { readonly typeNode: TypeNode; readonly type: Type } | undefined { + const decl = findAncestor( + getTokenAtPosition(sourceFile, pos), + isTypeContainer + ); + const typeNode = decl && decl.type; + return ( + typeNode && { + typeNode, + type: checker.getTypeFromTypeNode(typeNode), + } + ); + } + + type TypeContainer = + | AsExpression + | CallSignatureDeclaration + | ConstructSignatureDeclaration + | FunctionDeclaration + | GetAccessorDeclaration + | IndexSignatureDeclaration + | MappedTypeNode + | MethodDeclaration + | MethodSignature + | ParameterDeclaration + | PropertyDeclaration + | PropertySignature + | SetAccessorDeclaration + | TypeAliasDeclaration + | TypeAssertion + | VariableDeclaration; + function isTypeContainer(node: Node): node is TypeContainer { + // NOTE: Some locations are not handled yet: + // MappedTypeNode.typeParameters and SignatureDeclaration.typeParameters, as well as CallExpression.typeArguments + switch (node.kind) { + case SyntaxKind.AsExpression: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.IndexSignature: + case SyntaxKind.MappedType: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Parameter: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.SetAccessor: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.VariableDeclaration: + return true; + default: + return false; + } + } + + function getTypeParameters(node: JSDocTypedefTag) { + if (node.typeExpression && isJSDocTypeLiteral(node.typeExpression)) { + node.typeExpression.jsDocPropertyTags?.map((tag) => { + return factory.createTypeParameterDeclaration( + [], + tag.name.getFullText() + ); + }); + } + + return []; + } + + export function _containsJSDocTypedef(node: Node): node is HasJSDoc { + if (hasJSDocNodes(node)) { + const jsDocNodes = node.jsDoc || []; + return jsDocNodes.some((node) => { + const tags = node.tags || []; + return tags.some((tag) => isJSDocTypedefTag(tag)); + }); + } + return false; + } + + export function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag { + const jsDocNodes = node.jsDoc || []; + + return flatMap(jsDocNodes, (node) => { + const tags = node.tags || []; + return tags.filter((tag) => isJSDocTypedefTag(tag)); + })[0] as unknown as JSDocTypedefTag; + } + + export function containsTypeDefTag(node: Node): node is JSDocTypedefTag { + return isInJSDoc(node) && isJSDocTypedefTag(node); + } +} diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index 7be5d21..b1db358 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -54,6 +54,11 @@ namespace ts { } } + if (codefix._containsJSDocTypedef(node)) { + const jsdocTypedefNode = codefix.getJSDocTypedefNode(node); + diags.push(createDiagnosticForNode(jsdocTypedefNode.name || jsdocTypedefNode, Diagnostics.Convert_typedef_to_type)); + } + if (codefix.parameterShouldGetTypeFromJSDoc(node)) { diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); } diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 2237163..7eacc16 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -116,6 +116,7 @@ "codefixes/convertConstToLet.ts", "codefixes/fixExpectedComma.ts", "codefixes/fixAddVoidToPromise.ts", + "codefixes/convertTypedefToType.ts", "refactors/convertExport.ts", "refactors/convertImport.ts", "refactors/convertToOptionalChainExpression.ts", diff --git a/tests/cases/fourslash/codeFixConvertTypedefToType.ts b/tests/cases/fourslash/codeFixConvertTypedefToType.ts new file mode 100644 index 0000000000..e22f5da --- /dev/null +++ b/tests/cases/fourslash/codeFixConvertTypedefToType.ts @@ -0,0 +1,14 @@ +/// <reference path='fourslash.ts' /> + +/////** +//// * @typedef {Object} Foo +//// * @Property {Number} bar +//// */ + +verify.codeFix({ + description: ts.Diagnostics.Convert_typedef_to_type.message, + errorCode: ts.Diagnostics.Convert_typedef_to_type.code, + newFileContent: `type Foo = { + bar: number +};`, +});
1 parent 63791f5 commit 12a6d88

File tree

5 files changed

+197
-5
lines changed

5 files changed

+197
-5
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5146,7 +5146,7 @@
51465146
"category": "Error",
51475147
"code": 6258
51485148
},
5149-
"Found 1 error in {1}": {
5149+
"Found 1 error in {1}": {
51505150
"category": "Message",
51515151
"code": 6259
51525152
},
@@ -6028,9 +6028,9 @@
60286028
"category": "Message",
60296029
"code": 6930
60306030
},
6031-
"List of file name suffixes to search when resolving a module." : {
6032-
"category": "Error",
6033-
"code": 6931
6031+
"List of file name suffixes to search when resolving a module.": {
6032+
"category": "Error",
6033+
"code": 6931
60346034
},
60356035

60366036
"Variable '{0}' implicitly has an '{1}' type.": {
@@ -7368,7 +7368,6 @@
73687368
"category": "Message",
73697369
"code": 95175
73707370
},
7371-
73727371
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
73737372
"category": "Error",
73747373
"code": 18004
@@ -7524,5 +7523,13 @@
75247523
"The value '{0}' cannot be used here.": {
75257524
"category": "Error",
75267525
"code": 18050
7526+
},
7527+
"Convert @typedef to type": {
7528+
"category": "Message",
7529+
"code": 18051
7530+
},
7531+
"Convert all @typedefs to types": {
7532+
"category": "Message",
7533+
"code": 18051
75277534
}
75287535
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
namespace ts.codefix {
2+
const fixId = "convertTypedefToType";
3+
registerCodeFix({
4+
fixIds: [fixId],
5+
errorCodes: [Diagnostics.Convert_typedef_to_type.code],
6+
getCodeActions(context) {
7+
debugger;
8+
const checker = context.program.getTypeChecker();
9+
const info = getInfo(
10+
context.sourceFile,
11+
context.span.start,
12+
checker
13+
);
14+
const changes = textChanges.ChangeTracker.with(context, (t) => {
15+
const node = getTokenAtPosition(
16+
context.sourceFile,
17+
context.span.start
18+
);
19+
20+
if (containsTypeDefTag(node)) {
21+
fixSingleTypeDef(t, node, context, info?.typeNode);
22+
}
23+
});
24+
25+
if (changes.length > 0) {
26+
return [
27+
createCodeFixAction(
28+
fixId,
29+
changes,
30+
Diagnostics.Convert_typedef_to_type,
31+
fixId,
32+
Diagnostics.Convert_all_typedefs_to_types
33+
),
34+
];
35+
}
36+
},
37+
getAllCodeActions() {
38+
debugger;
39+
return { changes: [] };
40+
},
41+
});
42+
43+
function fixSingleTypeDef(
44+
changes: textChanges.ChangeTracker,
45+
typeDefNode: JSDocTypedefTag | undefined,
46+
context: CodeFixContextBase,
47+
typeNode?: TypeNode
48+
) {
49+
if (!typeDefNode?.name || !typeDefNode?.fullName || !typeNode) {
50+
return undefined;
51+
}
52+
53+
const comment = typeDefNode.parent;
54+
55+
changes.replaceNode(
56+
context.sourceFile,
57+
comment,
58+
factory.createTypeAliasDeclaration(
59+
[],
60+
typeDefNode.fullName.getFullText(),
61+
getTypeParameters(typeDefNode),
62+
typeNode
63+
)
64+
// factory.createTypeParameterDeclaration()
65+
);
66+
}
67+
68+
function getInfo(
69+
sourceFile: SourceFile,
70+
pos: number,
71+
checker: TypeChecker
72+
): { readonly typeNode: TypeNode; readonly type: Type } | undefined {
73+
const decl = findAncestor(
74+
getTokenAtPosition(sourceFile, pos),
75+
isTypeContainer
76+
);
77+
const typeNode = decl && decl.type;
78+
return (
79+
typeNode && {
80+
typeNode,
81+
type: checker.getTypeFromTypeNode(typeNode),
82+
}
83+
);
84+
}
85+
86+
type TypeContainer =
87+
| AsExpression
88+
| CallSignatureDeclaration
89+
| ConstructSignatureDeclaration
90+
| FunctionDeclaration
91+
| GetAccessorDeclaration
92+
| IndexSignatureDeclaration
93+
| MappedTypeNode
94+
| MethodDeclaration
95+
| MethodSignature
96+
| ParameterDeclaration
97+
| PropertyDeclaration
98+
| PropertySignature
99+
| SetAccessorDeclaration
100+
| TypeAliasDeclaration
101+
| TypeAssertion
102+
| VariableDeclaration;
103+
function isTypeContainer(node: Node): node is TypeContainer {
104+
// NOTE: Some locations are not handled yet:
105+
// MappedTypeNode.typeParameters and SignatureDeclaration.typeParameters, as well as CallExpression.typeArguments
106+
switch (node.kind) {
107+
case SyntaxKind.AsExpression:
108+
case SyntaxKind.CallSignature:
109+
case SyntaxKind.ConstructSignature:
110+
case SyntaxKind.FunctionDeclaration:
111+
case SyntaxKind.GetAccessor:
112+
case SyntaxKind.IndexSignature:
113+
case SyntaxKind.MappedType:
114+
case SyntaxKind.MethodDeclaration:
115+
case SyntaxKind.MethodSignature:
116+
case SyntaxKind.Parameter:
117+
case SyntaxKind.PropertyDeclaration:
118+
case SyntaxKind.PropertySignature:
119+
case SyntaxKind.SetAccessor:
120+
case SyntaxKind.TypeAliasDeclaration:
121+
case SyntaxKind.TypeAssertionExpression:
122+
case SyntaxKind.VariableDeclaration:
123+
return true;
124+
default:
125+
return false;
126+
}
127+
}
128+
129+
function getTypeParameters(node: JSDocTypedefTag) {
130+
if (node.typeExpression && isJSDocTypeLiteral(node.typeExpression)) {
131+
node.typeExpression.jsDocPropertyTags?.map((tag) => {
132+
return factory.createTypeParameterDeclaration(
133+
[],
134+
tag.name.getFullText()
135+
);
136+
});
137+
}
138+
139+
return [];
140+
}
141+
142+
export function _containsJSDocTypedef(node: Node): node is HasJSDoc {
143+
if (hasJSDocNodes(node)) {
144+
const jsDocNodes = node.jsDoc || [];
145+
return jsDocNodes.some((node) => {
146+
const tags = node.tags || [];
147+
return tags.some((tag) => isJSDocTypedefTag(tag));
148+
});
149+
}
150+
return false;
151+
}
152+
153+
export function getJSDocTypedefNode(node: HasJSDoc): JSDocTypedefTag {
154+
const jsDocNodes = node.jsDoc || [];
155+
156+
return flatMap(jsDocNodes, (node) => {
157+
const tags = node.tags || [];
158+
return tags.filter((tag) => isJSDocTypedefTag(tag));
159+
})[0] as unknown as JSDocTypedefTag;
160+
}
161+
162+
export function containsTypeDefTag(node: Node): node is JSDocTypedefTag {
163+
return isInJSDoc(node) && isJSDocTypedefTag(node);
164+
}
165+
}

src/services/suggestionDiagnostics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ namespace ts {
5454
}
5555
}
5656

57+
if (codefix._containsJSDocTypedef(node)) {
58+
const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
59+
diags.push(createDiagnosticForNode(jsdocTypedefNode.name || jsdocTypedefNode, Diagnostics.Convert_typedef_to_type));
60+
}
61+
5762
if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
5863
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
5964
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"codefixes/convertConstToLet.ts",
117117
"codefixes/fixExpectedComma.ts",
118118
"codefixes/fixAddVoidToPromise.ts",
119+
"codefixes/convertTypedefToType.ts",
119120
"refactors/convertExport.ts",
120121
"refactors/convertImport.ts",
121122
"refactors/convertToOptionalChainExpression.ts",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
/////**
4+
//// * @typedef {Object} Foo
5+
//// * @property {Number} bar
6+
//// */
7+
8+
verify.codeFix({
9+
description: ts.Diagnostics.Convert_typedef_to_type.message,
10+
errorCode: ts.Diagnostics.Convert_typedef_to_type.code,
11+
newFileContent: `type Foo = {
12+
bar: number
13+
};`,
14+
});

0 commit comments

Comments
 (0)