Skip to content

convert JSDoc typedef to type, issue 50644 #51430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6694,7 +6694,15 @@
"category": "Suggestion",
"code": 80008
},

"JSDoc typedef may be converted to TypeScript type.": {
"category": "Suggestion",
"code": 80009
},
"JSDoc typedefs may be converted to TypeScript types.": {
"category": "Suggestion",
"code": 80010
},

"Add missing 'super()' call": {
"category": "Message",
"code": 90001
Expand Down Expand Up @@ -7552,6 +7560,14 @@
"category": "Message",
"code": 95175
},
"Convert typedef to TypeScript type.": {
"category": "Message",
"code": 95176
},
"Convert all typedef to TypeScript types.": {
"category": "Message",
"code": 95177
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
1 change: 1 addition & 0 deletions src/services/_namespaces/ts.codefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from "../codefixes/convertToEsModule";
export * from "../codefixes/correctQualifiedNameToIndexedAccessType";
export * from "../codefixes/convertToTypeOnlyExport";
export * from "../codefixes/convertToTypeOnlyImport";
export * from "../codefixes/convertTypedefToType";
export * from "../codefixes/convertLiteralTypeToMappedType";
export * from "../codefixes/fixClassIncorrectlyImplementsInterface";
export * from "../codefixes/importFixes";
Expand Down
178 changes: 178 additions & 0 deletions src/services/codefixes/convertTypedefToType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
Diagnostics,
factory,
forEach,
getSynthesizedDeepClone,
getTokenAtPosition,
hasJSDocNodes,
InterfaceDeclaration,
isJSDocTypedefTag,
isJSDocTypeLiteral,
JSDocPropertyLikeTag,
JSDocTypedefTag,
JSDocTypeExpression,
JSDocTypeLiteral,
mapDefined,
Node,
PropertySignature,
some,
SourceFile,
SyntaxKind,
textChanges,
TypeAliasDeclaration,
} from "../_namespaces/ts";
import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix";

const fixId = "convertTypedefToType";
const errorCodes = [Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type.code];
registerCodeFix({
fixIds: [fixId],
errorCodes,
getCodeActions(context) {
const node = getTokenAtPosition(
context.sourceFile,
context.span.start
);
if (!node) return;
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile));

if (changes.length > 0) {
return [
createCodeFixAction(
fixId,
changes,
Diagnostics.Convert_typedef_to_TypeScript_type,
fixId,
Diagnostics.Convert_all_typedef_to_TypeScript_types,
),
];
}
},
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const node = getTokenAtPosition(diag.file, diag.start);
if (node) doChange(changes, node, diag.file);
})
});

function doChange(changes: textChanges.ChangeTracker, node: Node, sourceFile: SourceFile) {
if (isJSDocTypedefTag(node)) {
fixSingleTypeDef(changes, node, sourceFile);
}
}

function fixSingleTypeDef(
changes: textChanges.ChangeTracker,
typeDefNode: JSDocTypedefTag | undefined,
sourceFile: SourceFile,
) {
if (!typeDefNode) return;

const declaration = createDeclaration(typeDefNode);
if (!declaration) return;

const comment = typeDefNode.parent;

changes.replaceNode(
sourceFile,
comment,
declaration
);
}

function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined {
const { typeExpression } = tag;
if (!typeExpression) return;
const typeName = tag.name?.getText();
if (!typeName) return;

// For use case @typedef {object}Foo @property{bar}number
// But object type can be nested, meaning the value in the k/v pair can be object itself
if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) {
return createInterfaceForTypeLiteral(typeName, typeExpression);
}
// for use case @typedef {(number|string|undefined)} Foo or @typedef {number|string|undefined} Foo
// In this case, we reach the leaf node of AST.
if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) {
return createTypeAliasForTypeExpression(typeName, typeExpression);
}
}

function createInterfaceForTypeLiteral(
typeName: string,
typeLiteral: JSDocTypeLiteral
): InterfaceDeclaration | undefined {
const propertySignatures = createSignatureFromTypeLiteral(typeLiteral);
if (!some(propertySignatures)) return;
const interfaceDeclaration = factory.createInterfaceDeclaration(
/*modifiers*/ undefined,
typeName,
/*typeParameters*/ undefined,
/*heritageClauses*/ undefined,
propertySignatures,
);
return interfaceDeclaration;
}

function createTypeAliasForTypeExpression(
typeName: string,
typeExpression: JSDocTypeExpression
): TypeAliasDeclaration | undefined {
const typeReference = getSynthesizedDeepClone(typeExpression.type);
if (!typeReference) return;
const declaration = factory.createTypeAliasDeclaration(
/*modifiers*/ undefined,
factory.createIdentifier(typeName),
/*typeParameters*/ undefined,
typeReference
);
return declaration;
}

function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): PropertySignature[] | undefined {
const propertyTags = typeLiteral.jsDocPropertyTags;
if (!some(propertyTags)) return;

const getSignature = (tag: JSDocPropertyLikeTag) => {
const name = getPropertyName(tag);
const type = tag.typeExpression?.type;
const isOptional = tag.isBracketed;
let typeReference;

// Recursively handle nested object type
if (type && isJSDocTypeLiteral(type)) {
const signatures = createSignatureFromTypeLiteral(type);
typeReference = factory.createTypeLiteralNode(signatures);
}
// Leaf node, where type.kind === SyntaxKind.JSDocTypeExpression
else if (type) {
typeReference = getSynthesizedDeepClone(type);
}

if (typeReference && name) {
const questionToken = isOptional ? factory.createToken(SyntaxKind.QuestionToken) : undefined;
const prop = factory.createPropertySignature(
/*modifiers*/ undefined,
name,
questionToken,
typeReference
);

return prop;
}
};

const props = mapDefined(propertyTags, getSignature);
return props;
}

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

/** @internal */
export function getJSDocTypedefNode(node: Node): JSDocTypedefTag | undefined {
if (hasJSDocNodes(node)) {
return forEach(node.jsDoc, (node) => node.tags?.find(isJSDocTypedefTag));
}
return undefined;
}
5 changes: 5 additions & 0 deletions src/services/suggestionDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Pr
}
}

const jsdocTypedefNode = codefix.getJSDocTypedefNode(node);
if (jsdocTypedefNode) {
diags.push(createDiagnosticForNode(jsdocTypedefNode, Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type));
}

if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ Info 32 [00:01:13.000] response:
"2713",
"1205",
"1371",
"80009",
"2690",
"2420",
"2720",
Expand Down
18 changes: 18 additions & 0 deletions tests/cases/fourslash/codeFixConvertTypedefToType1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />

////
//// /**
//// * @typedef {Object}Foo
//// * @property {number}bar
//// */
////

verify.codeFix({
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
index: 0,
newFileContent: `
interface Foo {
bar: number;
}
`,
});
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixConvertTypedefToType2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

////
//// /**
//// * @typedef {(number|string|undefined)} Foo
//// */
////

verify.codeFix({
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
index: 0,
newFileContent: `
type Foo = (number | string | undefined);
`,
});
23 changes: 23 additions & 0 deletions tests/cases/fourslash/codeFixConvertTypedefToType3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts' />

////
//// /**
//// * @typedef Foo
//// * type {object}
//// * @property {string} id - person's ID
//// * @property name {string} // person's name
//// * @property {number|undefined} age - person's age
//// */
////

verify.codeFix({
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
index: 0,
newFileContent: `
interface Foo {
id: string;
name: string;
age: number | undefined;
}
`,
});
30 changes: 30 additions & 0 deletions tests/cases/fourslash/codeFixConvertTypedefToType4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// <reference path='fourslash.ts' />

////
//// /**
//// * @typedef {object} Person
//// * @property {object} data
//// * @property {string} data.name
//// * @property {number} data.age
//// * @property {object} data.contact
//// * @property {string} data.contact.address
//// * @property {string} [data.contact.phone]
//// */
////

verify.codeFix({
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
index: 0,
newFileContent: `
interface Person {
data: {
name: string;
age: number;
contact: {
address: string;
phone?: string;
};
};
}
`,
});
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixConvertTypedefToType5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

////
//// /**
//// * @typedef {number} Foo
//// */
////

verify.codeFix({
description: ts.Diagnostics.Convert_typedef_to_TypeScript_type.message,
index: 0,
newFileContent: `
type Foo = number;
`,
});