Skip to content

Commit 7a1f97a

Browse files
egorioeverbits
andauthored
fix(54666): Codefix `convertTypedefToType to work for multiple typedefs in a row (#54667)
Co-authored-by: everbits <[email protected]>
1 parent a86be55 commit 7a1f97a

File tree

5 files changed

+382
-37
lines changed

5 files changed

+382
-37
lines changed

src/services/codefixes/convertTypedefToType.ts

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {
22
Diagnostics,
33
factory,
4-
forEach,
4+
flatMap,
5+
getNewLineOrDefaultFromHost,
56
getSynthesizedDeepClone,
67
getTokenAtPosition,
78
hasJSDocNodes,
89
InterfaceDeclaration,
910
isJSDocTypedefTag,
1011
isJSDocTypeLiteral,
12+
JSDoc,
1113
JSDocPropertyLikeTag,
14+
JSDocTag,
1215
JSDocTypedefTag,
1316
JSDocTypeExpression,
1417
JSDocTypeLiteral,
@@ -29,12 +32,14 @@ registerCodeFix({
2932
fixIds: [fixId],
3033
errorCodes,
3134
getCodeActions(context) {
35+
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
3236
const node = getTokenAtPosition(
3337
context.sourceFile,
3438
context.span.start
3539
);
3640
if (!node) return;
37-
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile));
41+
42+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile, newLineCharacter));
3843

3944
if (changes.length > 0) {
4045
return [
@@ -48,35 +53,102 @@ registerCodeFix({
4853
];
4954
}
5055
},
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-
})
56+
getAllCodeActions: context => codeFixAll(
57+
context,
58+
errorCodes,
59+
(changes, diag) => {
60+
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
61+
const node = getTokenAtPosition(diag.file, diag.start);
62+
const fixAll = true;
63+
if (node) doChange(changes, node, diag.file, newLineCharacter, fixAll);
64+
}
65+
)
5566
});
5667

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(
68+
function doChange(
6469
changes: textChanges.ChangeTracker,
65-
typeDefNode: JSDocTypedefTag | undefined,
70+
node: Node,
6671
sourceFile: SourceFile,
72+
newLine: string,
73+
fixAll = false
6774
) {
68-
if (!typeDefNode) return;
75+
if (!isJSDocTypedefTag(node)) return;
6976

70-
const declaration = createDeclaration(typeDefNode);
77+
const declaration = createDeclaration(node);
7178
if (!declaration) return;
7279

73-
const comment = typeDefNode.parent;
80+
const commentNode = node.parent;
81+
82+
const { leftSibling, rightSibling } = getLeftAndRightSiblings(node);
83+
84+
let pos = commentNode.getStart();
85+
let prefix = "";
86+
87+
// the first @typedef is the comment block with a text comment above
88+
if (!leftSibling && commentNode.comment) {
89+
pos = findEndOfTextBetween(commentNode, commentNode.getStart(), node.getStart());
90+
prefix = `${newLine} */${newLine}`;
91+
}
92+
93+
if (leftSibling) {
94+
if (fixAll && isJSDocTypedefTag(leftSibling)) {
95+
// Don't need to keep empty comment clock between created interfaces
96+
pos = node.getStart();
97+
prefix = "";
98+
}
99+
else {
100+
pos = findEndOfTextBetween(commentNode, leftSibling.getStart(), node.getStart());
101+
prefix = `${newLine} */${newLine}`;
102+
}
103+
}
104+
105+
let end = commentNode.getEnd();
106+
let suffix = "";
107+
108+
if (rightSibling) {
109+
if (fixAll && isJSDocTypedefTag(rightSibling)) {
110+
// Don't need to keep empty comment clock between created interfaces
111+
end = rightSibling.getStart();
112+
suffix = `${newLine}${newLine}`;
113+
}
114+
else {
115+
end = rightSibling.getStart();
116+
suffix = `${newLine}/**${newLine} * `;
117+
}
118+
}
74119

75-
changes.replaceNode(
76-
sourceFile,
77-
comment,
78-
declaration
120+
changes.replaceRange(sourceFile, { pos, end }, declaration, { prefix, suffix });
121+
}
122+
123+
function getLeftAndRightSiblings(typedefNode: JSDocTypedefTag): { leftSibling?: Node, rightSibling?: Node } {
124+
125+
const commentNode = typedefNode.parent;
126+
const maxChildIndex = commentNode.getChildCount() - 1;
127+
128+
const currentNodeIndex = commentNode.getChildren().findIndex(
129+
(n) => n.getStart() === typedefNode.getStart() && n.getEnd() === typedefNode.getEnd()
79130
);
131+
132+
const leftSibling = currentNodeIndex > 0 ? commentNode.getChildAt(currentNodeIndex - 1) : undefined;
133+
const rightSibling = currentNodeIndex < maxChildIndex ? commentNode.getChildAt(currentNodeIndex + 1) : undefined;
134+
135+
return { leftSibling, rightSibling };
136+
}
137+
138+
/**
139+
* Finds the index of the last meaningful symbol (except empty spaces, * and /) in the comment
140+
* between start and end positions
141+
*/
142+
function findEndOfTextBetween(jsDocComment: JSDoc, from: number, to: number): number {
143+
const comment = jsDocComment.getText().substring(from - jsDocComment.getStart(), to - jsDocComment.getStart());
144+
145+
for (let i = comment.length; i > 0; i--) {
146+
if(!/[*\/\s]/g.test(comment.substring(i - 1, i))) {
147+
return from + i;
148+
}
149+
}
150+
151+
return to;
80152
}
81153

82154
function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined {
@@ -86,7 +158,7 @@ function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAli
86158
if (!typeName) return;
87159

88160
// 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
161+
// But object type can be nested, meaning the value in the k/v pair can be the object itself
90162
if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
91163
return createInterfaceForTypeLiteral(typeName, typeExpression);
92164
}
@@ -103,14 +175,14 @@ function createInterfaceForTypeLiteral(
103175
): InterfaceDeclaration | undefined {
104176
const propertySignatures = createSignatureFromTypeLiteral(typeLiteral);
105177
if (!some(propertySignatures)) return;
106-
const interfaceDeclaration = factory.createInterfaceDeclaration(
178+
179+
return factory.createInterfaceDeclaration(
107180
/*modifiers*/ undefined,
108181
typeName,
109182
/*typeParameters*/ undefined,
110183
/*heritageClauses*/ undefined,
111184
propertySignatures,
112185
);
113-
return interfaceDeclaration;
114186
}
115187

116188
function createTypeAliasForTypeExpression(
@@ -119,13 +191,13 @@ function createTypeAliasForTypeExpression(
119191
): TypeAliasDeclaration | undefined {
120192
const typeReference = getSynthesizedDeepClone(typeExpression.type);
121193
if (!typeReference) return;
122-
const declaration = factory.createTypeAliasDeclaration(
194+
195+
return factory.createTypeAliasDeclaration(
123196
/*modifiers*/ undefined,
124197
factory.createIdentifier(typeName),
125198
/*typeParameters*/ undefined,
126199
typeReference
127200
);
128-
return declaration;
129201
}
130202

131203
function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): PropertySignature[] | undefined {
@@ -150,29 +222,28 @@ function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): Property
150222

151223
if (typeReference && name) {
152224
const questionToken = isOptional ? factory.createToken(SyntaxKind.QuestionToken) : undefined;
153-
const prop = factory.createPropertySignature(
225+
226+
return factory.createPropertySignature(
154227
/*modifiers*/ undefined,
155228
name,
156229
questionToken,
157230
typeReference
158231
);
159-
160-
return prop;
161232
}
162233
};
163234

164-
const props = mapDefined(propertyTags, getSignature);
165-
return props;
235+
return mapDefined(propertyTags, getSignature);
166236
}
167237

168238
function getPropertyName(tag: JSDocPropertyLikeTag): string | undefined {
169239
return tag.name.kind === SyntaxKind.Identifier ? tag.name.text : tag.name.right.text;
170240
}
171241

172242
/** @internal */
173-
export function getJSDocTypedefNode(node: Node): JSDocTypedefTag | undefined {
243+
export function getJSDocTypedefNodes(node: Node): readonly JSDocTag[] {
174244
if (hasJSDocNodes(node)) {
175-
return forEach(node.jsDoc, (node) => node.tags?.find(isJSDocTypedefTag));
245+
return flatMap(node.jsDoc, (doc) => doc.tags?.filter((tag) => isJSDocTypedefTag(tag)));
176246
}
177-
return undefined;
247+
248+
return [];
178249
}

src/services/suggestionDiagnostics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Pr
116116
}
117117
}
118118

119-
const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
120-
if (jsdocTypedefNode) {
119+
const jsdocTypedefNodes = codefix.getJSDocTypedefNodes(node);
120+
for (const jsdocTypedefNode of jsdocTypedefNodes) {
121121
diags.push(createDiagnosticForNode(jsdocTypedefNode, Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type));
122122
}
123123

tests/cases/fourslash/codeFixConvertTypedefToType4.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ interface Person {
2727
};
2828
}
2929
`,
30-
});
30+
});

0 commit comments

Comments
 (0)