Skip to content
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
7 changes: 7 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@
"avatar_url": "https://avatars.githubusercontent.com/u/172711?v=4",
"profile": "https://tverdohleb.com/",
"contributions": ["bug", "code", "ideas"]
},
{
"login": "hayawata3626",
"name": "Isco",
"avatar_url": "https://avatars.githubusercontent.com/u/15213369?v=4",
"profile": "https://zenn.dev/watahaya",
"contributions": ["code"]
}
],
"contributorsPerLine": 7
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tverdohleb.com/"><img src="https://avatars.githubusercontent.com/u/172711?v=4?s=100" width="100px;" alt="Valeriy"/><br /><sub><b>Valeriy</b></sub></a><br /><a href="https://github.com/fabien0102/openapi-codegen/issues?q=author%3Atverdohleb" title="Bug reports">🐛</a> <a href="https://github.com/fabien0102/openapi-codegen/commits?author=tverdohleb" title="Code">💻</a> <a href="#ideas-tverdohleb" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://zenn.dev/watahaya"><img src="https://avatars.githubusercontent.com/u/15213369?v=4?s=100" width="100px;" alt="Isco"/><br /><sub><b>Isco</b></sub></a><br /><a href="https://github.com/fabien0102/openapi-codegen/commits?author=hayawata3626" title="Code">💻</a></td>
</tr>
</tbody>
</table>
Expand Down
346 changes: 346 additions & 0 deletions plugins/typescript/src/core/analyzeImportUsage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
import { describe, it, expect } from "vitest";
import ts, { factory as f } from "typescript";
import { analyzeImportUsage } from "./analyzeImportUsage";

// Helper to set parent references for TypeScript AST nodes
const setParentReferences = <T extends ts.Node>(node: T): T => {
function setParent(child: ts.Node, parent: ts.Node) {
Object.defineProperty(child, "parent", {
value: parent,
writable: true,
configurable: true,
});
child.forEachChild((grandChild) => setParent(grandChild, child));
}
node.forEachChild((child) => setParent(child, node));
return node;
};

describe("analyzeImportUsage", () => {
describe("type-only usage", () => {
it("should detect type reference usage", () => {
const nodes = [
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("MyAlias"),
undefined,
f.createTypeReferenceNode(
f.createIdentifier("TestImport"),
undefined
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
});

it("should detect usage in type alias declaration", () => {
const nodes = [
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("TestImport"),
undefined,
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
});

it("should detect usage in interface declaration", () => {
const nodes = [
setParentReferences(
f.createInterfaceDeclaration(
undefined,
f.createIdentifier("TestImport"),
undefined,
undefined,
[]
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
});

it("should detect indexed access type usage", () => {
const typeRef = f.createTypeReferenceNode(
f.createIdentifier("TestImport"),
undefined
);
const nodes = [
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("MyType"),
undefined,
f.createIndexedAccessTypeNode(
typeRef,
f.createLiteralTypeNode(f.createStringLiteral("prop"))
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
});
});

describe("value usage", () => {
it("should detect call expression usage", () => {
const nodes = [
setParentReferences(
f.createExpressionStatement(
f.createCallExpression(
f.createIdentifier("TestImport"),
undefined,
[]
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});

it("should detect property access usage", () => {
const nodes = [
setParentReferences(
f.createExpressionStatement(
f.createPropertyAccessExpression(
f.createIdentifier("TestImport"),
f.createIdentifier("prop")
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});

it("should detect binary expression usage", () => {
const nodes = [
setParentReferences(
f.createExpressionStatement(
f.createBinaryExpression(
f.createIdentifier("TestImport"),
ts.SyntaxKind.EqualsEqualsToken,
f.createStringLiteral("test")
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});

it("should detect new expression usage", () => {
const nodes = [
setParentReferences(
f.createExpressionStatement(
f.createNewExpression(
f.createIdentifier("TestImport"),
undefined,
[]
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});

it("should detect array literal usage", () => {
const nodes = [
setParentReferences(
f.createVariableStatement(
undefined,
f.createVariableDeclarationList([
f.createVariableDeclaration(
f.createIdentifier("arr"),
undefined,
undefined,
f.createArrayLiteralExpression([
f.createIdentifier("TestImport"),
])
),
])
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});

it("should detect object literal usage", () => {
const objectLiteral = f.createObjectLiteralExpression([
f.createPropertyAssignment("key", f.createIdentifier("TestImport")),
]);
const nodes = [
setParentReferences(
f.createVariableStatement(
undefined,
f.createVariableDeclarationList([
f.createVariableDeclaration(
f.createIdentifier("obj"),
undefined,
undefined,
objectLiteral
),
])
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});
});

describe("mixed usage", () => {
it("should detect mixed type and value usage", () => {
const nodes = [
// Type usage
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("MyType"),
undefined,
f.createTypeReferenceNode(
f.createIdentifier("TestImport"),
undefined
)
)
),
// Value usage
setParentReferences(
f.createExpressionStatement(
f.createCallExpression(
f.createIdentifier("TestImport"),
undefined,
[]
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});
});

describe("unused imports", () => {
it("should default to type-only for unused imports", () => {
const nodes = [
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("MyType"),
undefined,
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
)
),
];

expect(analyzeImportUsage(nodes, "UnusedImport")).toBe(true);
});

it("should handle empty node array", () => {
expect(analyzeImportUsage([], "TestImport")).toBe(true);
});
});

describe("edge cases", () => {
it("should handle nodes without parents", () => {
const identifier = f.createIdentifier("TestImport");
// Explicitly don't set parent references
expect(analyzeImportUsage([identifier], "TestImport")).toBe(true);
});

it("should be case sensitive", () => {
const nodes = [
setParentReferences(
f.createExpressionStatement(
f.createCallExpression(
f.createIdentifier("testImport"), // lowercase
undefined,
[]
)
)
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(true); // Should default to type-only
});

it("should handle deeply nested usage", () => {
const nodes = [
setParentReferences(
f.createBlock([
f.createIfStatement(
f.createBinaryExpression(
f.createTrue(),
ts.SyntaxKind.EqualsEqualsToken,
f.createTrue()
),
f.createBlock([
f.createExpressionStatement(
f.createCallExpression(
f.createIdentifier("TestImport"),
undefined,
[]
)
),
])
),
])
),
];

expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
});
});

describe("performance optimization", () => {
it("should return false for mixed usage", () => {
const nodes = [
// Type usage
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("MyType"),
undefined,
f.createTypeReferenceNode(
f.createIdentifier("TestImport"),
undefined
)
)
),
// Value usage
setParentReferences(
f.createExpressionStatement(
f.createCallExpression(
f.createIdentifier("TestImport"),
undefined,
[]
)
)
),
// Additional nodes - should stop before processing these
setParentReferences(
f.createTypeAliasDeclaration(
undefined,
f.createIdentifier("AnotherType"),
undefined,
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
)
),
];

const result = analyzeImportUsage(nodes, "TestImport");
expect(result).toBe(false);
});
});
});
Loading