From 81509b08736b092d95815b807d57842c7e32d381 Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Thu, 6 Nov 2025 19:07:55 +0800 Subject: [PATCH 1/6] fix: use variable symbol instead of arrow function symbol for default export check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed an issue where arrow function default export detection was checking the arrow function's symbol instead of the variable declaration's symbol. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ts-parser/src/parser/FunctionParser.ts | 3 ++- ts-parser/test-repo/src/test-export-default.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 ts-parser/test-repo/src/test-export-default.ts diff --git a/ts-parser/src/parser/FunctionParser.ts b/ts-parser/src/parser/FunctionParser.ts index 78057f7..7050f09 100644 --- a/ts-parser/src/parser/FunctionParser.ts +++ b/ts-parser/src/parser/FunctionParser.ts @@ -386,7 +386,8 @@ export class FunctionParser { isExported = true; } else { const parent = varDecl.getVariableStatement(); - isExported = parent ? (parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === arrowFunc.getSymbol() && this.defaultExportSymbol !== undefined)) : false; + const varSymbol = varDecl.getSymbol(); + isExported = parent ? (parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === varSymbol && this.defaultExportSymbol !== undefined)) : false; } return { diff --git a/ts-parser/test-repo/src/test-export-default.ts b/ts-parser/test-repo/src/test-export-default.ts new file mode 100644 index 0000000..78ef9f1 --- /dev/null +++ b/ts-parser/test-repo/src/test-export-default.ts @@ -0,0 +1,11 @@ +// 测试用例:箭头函数先声明,然后作为默认导出 +const foo = () => { + console.log('bar') +} + +export default foo; + +// 对比:直接导出的箭头函数 +export const bar = () => { + console.log('baz') +} From 7dc9a9c10eec8051e79557de425b19ebba8eb33a Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Thu, 6 Nov 2025 19:10:25 +0800 Subject: [PATCH 2/6] upd version --- ts-parser/package.json | 2 +- ts-parser/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts-parser/package.json b/ts-parser/package.json index d50ec11..2c22c5e 100644 --- a/ts-parser/package.json +++ b/ts-parser/package.json @@ -1,6 +1,6 @@ { "name": "abcoder-ts-parser", - "version": "0.0.9", + "version": "0.0.13", "description": "TypeScript AST parser for UNIAST specification", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/ts-parser/src/index.ts b/ts-parser/src/index.ts index 7316435..b0e01dd 100644 --- a/ts-parser/src/index.ts +++ b/ts-parser/src/index.ts @@ -11,7 +11,7 @@ const program = new Command(); program .name('abcoder-ts-parser') .description('TypeScript AST parser for UNIAST specification') - .version('0.0.9'); + .version('0.0.13'); program .command('parse') From e4f2bf9639e16e03d3d2158f1a6c987ceaa8794b Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Mon, 1 Dec 2025 19:10:08 +0800 Subject: [PATCH 3/6] fix: correct type alias symbol extraction from TypeReference nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed type dependency recognition for type aliases (especially union types) across all parsers by extracting symbols from TypeReference node's typeName child instead of the node itself. Changes: - DependencyUtils: capture union/intersection type aliases before recursing into members - TypeParser: extract symbols from typeName for TypeReference nodes, handle ExpressionWithTypeArguments, put type alias dependencies in InlineStruct instead of Implements - FunctionParser: extract symbols from TypeReference typeName for function parameter/return types - VarParser: extract symbols from TypeReference typeName for variable/property type annotations - Added comprehensive unit tests for all three parsers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ts-parser/src/parser/FunctionParser.ts | 57 ++++- ts-parser/src/parser/TypeParser.ts | 88 +++++++- ts-parser/src/parser/VarParser.ts | 32 ++- .../src/parser/test/FunctionParser.test.ts | 151 +++++++++++++ ts-parser/src/parser/test/TypeParser.test.ts | 187 ++++++++++++++-- ts-parser/src/parser/test/VarParser.test.ts | 202 +++++++++++++++++- ts-parser/src/utils/dependency-utils.ts | 16 +- 7 files changed, 695 insertions(+), 38 deletions(-) diff --git a/ts-parser/src/parser/FunctionParser.ts b/ts-parser/src/parser/FunctionParser.ts index 7050f09..1176be4 100644 --- a/ts-parser/src/parser/FunctionParser.ts +++ b/ts-parser/src/parser/FunctionParser.ts @@ -676,7 +676,62 @@ export class FunctionParser { } for (const typeNode of typeNodes) { - // Handle union and intersection types by extracting individual type references + // First, try to extract the direct type reference from the typeNode itself + // This handles type aliases like "Status" which reference union types + let directSymbol: Symbol | undefined; + + // For TypeReferenceNode, get the symbol from the type name + if (Node.isTypeReference(typeNode)) { + const typeName = typeNode.getTypeName(); + if (Node.isIdentifier(typeName)) { + directSymbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + directSymbol = typeName.getRight().getSymbol(); + } + } else { + // For other type nodes, try to get symbol from the type itself + const typeObj = typeNode.getType(); + directSymbol = typeObj.getSymbol() || typeNode.getSymbol(); + } + + if (directSymbol) { + const directTypeName = directSymbol.getName(); + if (!this.isPrimitiveType(directTypeName)) { + const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(directSymbol, typeNode); + if (resolvedSymbol && !resolvedSymbol.isExternal) { + const decls = resolvedRealSymbol?.getDeclarations() || []; + if (decls.length > 0) { + const defStartOffset = decls[0].getStart(); + const defEndOffset = decls[0].getEnd(); + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; + + // Check if this is not a self-reference within the same function + const isSelfReference = ( + resolvedSymbol.moduleName === moduleName && + this.getPkgPath(resolvedSymbol.packagePath || packagePath) === packagePath && + defEndOffset <= node.getEnd() && + defStartOffset >= node.getStart() + ); + + if (!visited.has(key) && !isSelfReference) { + visited.add(key); + const dep: Dependency = { + ModPath: resolvedSymbol.moduleName || moduleName, + PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), + Name: resolvedSymbol.name, + File: resolvedSymbol.filePath, + Line: resolvedSymbol.line, + StartOffset: resolvedSymbol.startOffset, + EndOffset: resolvedSymbol.endOffset + }; + types.push(dep); + } + } + } + } + } + + // Then handle union and intersection types by extracting individual type references const typeReferences = this.dependencyUtils.extractAtomicTypeReferences(typeNode); for (const typeRef of typeReferences) { diff --git a/ts-parser/src/parser/TypeParser.ts b/ts-parser/src/parser/TypeParser.ts index e257002..ff4f7fe 100644 --- a/ts-parser/src/parser/TypeParser.ts +++ b/ts-parser/src/parser/TypeParser.ts @@ -7,7 +7,8 @@ import { SyntaxKind, TypeNode, ClassExpression, - Symbol + Symbol, + Node } from 'ts-morph'; import { Type as UniType, Dependency } from '../types/uniast'; import { assignSymbolName, SymbolResolver } from '../utils/symbol-resolver'; @@ -248,9 +249,9 @@ export class TypeParser { TypeKind: 'typedef', Content: content, Methods: {}, - Implements: typeDependencies, + Implements: [], SubStruct: [], - InlineStruct: [] + InlineStruct: typeDependencies }; } @@ -293,19 +294,82 @@ export class TypeParser { const dependencies: Dependency[] = []; const visited = new Set(); - // Extract from identifiers and find their definitions - const types = this.dependencyUtils.extractAtomicTypeReferences(typeNode); + // Collect all type reference nodes (including the root typeNode itself if it's a TypeReference) + const typeReferences: TypeNode[] = []; - for (const t of types) { - const symbol = t.getSymbol(); - if (!symbol) { - continue; + // Handle ExpressionWithTypeArguments (used in extends/implements clauses) + if (Node.isExpressionWithTypeArguments(typeNode)) { + const expression = typeNode.getExpression(); + let symbol: Symbol | undefined; + + if (Node.isIdentifier(expression)) { + symbol = expression.getSymbol(); + } else if (Node.isPropertyAccessExpression(expression)) { + symbol = expression.getSymbol(); + } + + if (symbol) { + const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeNode); + if (resolvedSymbol && !resolvedSymbol.isExternal) { + const decls = resolvedRealSymbol?.getDeclarations() || []; + if (decls.length > 0) { + const defStartOffset = decls[0].getStart(); + const defEndOffset = decls[0].getEnd(); + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; + + // Check if this is not a self-reference + const isSelfReference = ( + resolvedSymbol.moduleName === moduleName && + this.getPkgPath(resolvedSymbol.packagePath || packagePath) === packagePath && + defStartOffset <= resolvedSymbol.startOffset && + resolvedSymbol.endOffset <= defEndOffset + ); + + if (!visited.has(key) && !isSelfReference) { + visited.add(key); + dependencies.push({ + ModPath: resolvedSymbol.moduleName || moduleName, + PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), + Name: resolvedSymbol.name, + File: resolvedSymbol.filePath, + Line: resolvedSymbol.line, + StartOffset: resolvedSymbol.startOffset, + EndOffset: resolvedSymbol.endOffset + }); + } + } + } } - const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeNode); - // if symbol is not external, add it to dependencies + } + + // Handle TypeReference nodes + if (Node.isTypeReference(typeNode)) { + typeReferences.push(typeNode); + } + + // Also get all descendant type references + typeReferences.push(...typeNode.getDescendantsOfKind(SyntaxKind.TypeReference)); + + // Process each type reference + for (const typeRef of typeReferences) { + if (!Node.isTypeReference(typeRef)) continue; + + const typeName = typeRef.getTypeName(); + let symbol: Symbol | undefined; + + if (Node.isIdentifier(typeName)) { + symbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + symbol = typeName.getRight().getSymbol(); + } + + if (!symbol) continue; + + const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeRef); if (!resolvedSymbol || resolvedSymbol.isExternal) { continue; } + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; if (visited.has(key)) { continue; @@ -329,6 +393,8 @@ export class TypeParser { StartOffset: resolvedSymbol.startOffset, EndOffset: resolvedSymbol.endOffset }; + + // Skip self-references if ( dep.ModPath === moduleName && dep.PkgPath === packagePath && diff --git a/ts-parser/src/parser/VarParser.ts b/ts-parser/src/parser/VarParser.ts index f8ceb54..840e21f 100644 --- a/ts-parser/src/parser/VarParser.ts +++ b/ts-parser/src/parser/VarParser.ts @@ -140,7 +140,21 @@ export class VarParser { const typeNode = varDecl.getTypeNode(); let type: Dependency | undefined; if (typeNode) { - const typeSymbol = typeNode.getSymbol(); + let typeSymbol: Symbol | undefined; + + // For TypeReferenceNode, get the symbol from the type name + if (Node.isTypeReference(typeNode)) { + const typeName = typeNode.getTypeName(); + if (Node.isIdentifier(typeName)) { + typeSymbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + typeSymbol = typeName.getRight().getSymbol(); + } + } else { + // For other type nodes, try to get symbol from the type itself + typeSymbol = typeNode.getSymbol(); + } + if (typeSymbol) { const [resolvedSymbol, ] = this.symbolResolver.resolveSymbol(typeSymbol, varDecl); if (resolvedSymbol && !resolvedSymbol.isExternal) { @@ -214,7 +228,21 @@ export class VarParser { const typeNode = prop.getTypeNode(); let type: Dependency | undefined; if (typeNode) { - const typeSymbol = typeNode.getSymbol(); + let typeSymbol: Symbol | undefined; + + // For TypeReferenceNode, get the symbol from the type name + if (Node.isTypeReference(typeNode)) { + const typeName = typeNode.getTypeName(); + if (Node.isIdentifier(typeName)) { + typeSymbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + typeSymbol = typeName.getRight().getSymbol(); + } + } else { + // For other type nodes, try to get symbol from the type itself + typeSymbol = typeNode.getSymbol(); + } + if (typeSymbol) { const [resolvedSymbol, ] = this.symbolResolver.resolveSymbol(typeSymbol, prop); if (resolvedSymbol && !resolvedSymbol.isExternal) { diff --git a/ts-parser/src/parser/test/FunctionParser.test.ts b/ts-parser/src/parser/test/FunctionParser.test.ts index 6497ea8..e48e989 100644 --- a/ts-parser/src/parser/test/FunctionParser.test.ts +++ b/ts-parser/src/parser/test/FunctionParser.test.ts @@ -599,4 +599,155 @@ describe('FunctionParser', () => { cleanup(); }); }); + + describe('type alias dependencies in function parameters and return types', () => { + it('should extract union type alias dependencies from function parameters', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Status = 'normal' | 'abnormal'; + + export const flipStatus = (s: Status): Status => { + return s === 'normal' ? 'abnormal' : 'normal'; + }; + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + // flipStatus function should exist + const flipStatus = expectToBeDefined(functions['flipStatus']); + expect(flipStatus.Exported).toBe(true); + + // Should have Status in Types array + expect(flipStatus.Types).toBeDefined(); + expect(expectToBeDefined(flipStatus.Types).length).toBeGreaterThan(0); + + const typeNames = expectToBeDefined(flipStatus.Types).map(dep => dep.Name); + expect(typeNames).toContain('Status'); + + cleanup(); + }); + + it('should extract type alias dependencies from complex function signatures', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserId = string; + export type UserRole = 'admin' | 'user' | 'guest'; + + export type User = { + id: UserId; + role: UserRole; + name: string; + }; + + export function createUser(id: UserId, role: UserRole, name: string): User { + return { id, role, name }; + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const createUser = expectToBeDefined(functions['createUser']); + + // Should have all type aliases in Types array + expect(createUser.Types).toBeDefined(); + const typeNames = expectToBeDefined(createUser.Types).map(dep => dep.Name); + + expect(typeNames).toContain('UserId'); + expect(typeNames).toContain('UserRole'); + expect(typeNames).toContain('User'); + + cleanup(); + }); + + it('should not include primitive types in Types array', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export function processData(name: string, age: number, active: boolean): void { + console.log(name, age, active); + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const processData = expectToBeDefined(functions['processData']); + + // Should not have primitive types + const typeNames = (processData.Types || []).map(dep => dep.Name); + expect(typeNames).not.toContain('string'); + expect(typeNames).not.toContain('number'); + expect(typeNames).not.toContain('boolean'); + expect(typeNames).not.toContain('void'); + + cleanup(); + }); + + it('should extract type aliases from arrow function parameters and return types', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Result = { success: true; data: T } | { success: false; error: string }; + export type UserData = { name: string; email: string }; + + export const fetchUser = async (id: string): Promise> => { + return { success: true, data: { name: 'John', email: 'john@example.com' } }; + }; + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const fetchUser = expectToBeDefined(functions['fetchUser']); + + // Should have type aliases in Types array + expect(fetchUser.Types).toBeDefined(); + const typeNames = expectToBeDefined(fetchUser.Types).map(dep => dep.Name); + + expect(typeNames).toContain('Result'); + expect(typeNames).toContain('UserData'); + + cleanup(); + }); + + it('should handle multiple occurrences of the same type alias', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Status = 'active' | 'inactive'; + + export function updateStatus(oldStatus: Status, newStatus: Status): Status { + return newStatus; + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const updateStatus = expectToBeDefined(functions['updateStatus']); + + // Should have Status only once (deduplication) + expect(updateStatus.Types).toBeDefined(); + const typeNames = expectToBeDefined(updateStatus.Types).map(dep => dep.Name); + const statusCount = typeNames.filter(name => name === 'Status').length; + + expect(statusCount).toBe(1); + + cleanup(); + }); + }); }); \ No newline at end of file diff --git a/ts-parser/src/parser/test/TypeParser.test.ts b/ts-parser/src/parser/test/TypeParser.test.ts index 85cbea6..460072f 100644 --- a/ts-parser/src/parser/test/TypeParser.test.ts +++ b/ts-parser/src/parser/test/TypeParser.test.ts @@ -299,7 +299,7 @@ describe('TypeParser', () => { const { project, sourceFile, cleanup } = createTestProject(` class CustomType {} interface CustomInterface {} - + type SimpleAlias = CustomType; type ComplexAlias = { prop: CustomType; @@ -313,37 +313,38 @@ describe('TypeParser', () => { }; }; `); - + const parser = new TypeParser(process.cwd()); let pkgPathAbsFile : string = sourceFile.getFilePath() pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) - + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); - + const simpleAlias = expectToBeDefined(types['SimpleAlias']); const complexAlias = expectToBeDefined(types['ComplexAlias']); const unionAlias = expectToBeDefined(types['UnionAlias']); const genericAlias = expectToBeDefined(types['GenericAlias']); const nestedAlias = expectToBeDefined(types['NestedAlias']); - - expect(expectToBeDefined(simpleAlias.Implements).length).toBeGreaterThan(0); - expect(expectToBeDefined(complexAlias.Implements).length).toBeGreaterThan(0); - expect(expectToBeDefined(unionAlias.Implements).length).toBeGreaterThan(0); - expect(expectToBeDefined(genericAlias.Implements).length).toBeGreaterThan(0); - expect(expectToBeDefined(nestedAlias.Implements).length).toBeGreaterThan(0); - + + // Type aliases should have dependencies in InlineStruct, not Implements + expect(expectToBeDefined(simpleAlias.InlineStruct).length).toBeGreaterThan(0); + expect(expectToBeDefined(complexAlias.InlineStruct).length).toBeGreaterThan(0); + expect(expectToBeDefined(unionAlias.InlineStruct).length).toBeGreaterThan(0); + expect(expectToBeDefined(genericAlias.InlineStruct).length).toBeGreaterThan(0); + expect(expectToBeDefined(nestedAlias.InlineStruct).length).toBeGreaterThan(0); + const allTypeNames = [ - ...expectToBeDefined(simpleAlias.Implements), - ...expectToBeDefined(complexAlias.Implements), - ...expectToBeDefined(unionAlias.Implements), - ...expectToBeDefined(genericAlias.Implements), - ...expectToBeDefined(nestedAlias.Implements) + ...expectToBeDefined(simpleAlias.InlineStruct), + ...expectToBeDefined(complexAlias.InlineStruct), + ...expectToBeDefined(unionAlias.InlineStruct), + ...expectToBeDefined(genericAlias.InlineStruct), + ...expectToBeDefined(nestedAlias.InlineStruct) ].map(dep => expectToBeDefined(dep).Name); - + expect(allTypeNames).toContain('CustomType'); expect(allTypeNames).toContain('CustomInterface'); - + cleanup(); }); @@ -521,4 +522,154 @@ describe('TypeParser', () => { cleanup(); }); }); + + describe('type alias dependencies in InlineStruct', () => { + it('should extract union type alias dependencies into InlineStruct', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Status = 'normal' | 'abnormal'; + + export type ServerStatus = { + code: number; + status: Status; + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile : string = sourceFile.getFilePath() + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // Status type should exist + expect(types['Status']).toBeDefined(); + expect(types['Status'].TypeKind).toBe('typedef'); + + // ServerStatus should exist + const serverStatus = expectToBeDefined(types['ServerStatus']); + expect(serverStatus.TypeKind).toBe('typedef'); + + // ServerStatus should have Status in InlineStruct, not Implements + expect(serverStatus.Implements).toEqual([]); + expect(expectToBeDefined(serverStatus.InlineStruct).length).toBeGreaterThan(0); + + const inlineStructNames = expectToBeDefined(serverStatus.InlineStruct).map(dep => dep.Name); + expect(inlineStructNames).toContain('Status'); + + cleanup(); + }); + + it('should extract complex type alias dependencies into InlineStruct', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserId = string; + export type UserRole = 'admin' | 'user' | 'guest'; + + export type User = { + id: UserId; + role: UserRole; + name: string; + }; + + export type UserWithMetadata = User & { + createdAt: Date; + updatedAt: Date; + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile : string = sourceFile.getFilePath() + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // User type should have dependencies in InlineStruct + const user = expectToBeDefined(types['User']); + expect(user.Implements).toEqual([]); + expect(expectToBeDefined(user.InlineStruct).length).toBeGreaterThan(0); + + const userInlineNames = expectToBeDefined(user.InlineStruct).map(dep => dep.Name); + expect(userInlineNames).toContain('UserId'); + expect(userInlineNames).toContain('UserRole'); + + // UserWithMetadata should have User in InlineStruct + const userWithMetadata = expectToBeDefined(types['UserWithMetadata']); + expect(userWithMetadata.Implements).toEqual([]); + expect(expectToBeDefined(userWithMetadata.InlineStruct).length).toBeGreaterThan(0); + + const metadataInlineNames = expectToBeDefined(userWithMetadata.InlineStruct).map(dep => dep.Name); + expect(metadataInlineNames).toContain('User'); + + cleanup(); + }); + + it('should not include primitive types in InlineStruct', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Config = { + host: string; + port: number; + enabled: boolean; + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile : string = sourceFile.getFilePath() + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const config = expectToBeDefined(types['Config']); + + // Should not have primitive types in InlineStruct + const inlineStructNames = (config.InlineStruct || []).map(dep => dep.Name); + expect(inlineStructNames).not.toContain('string'); + expect(inlineStructNames).not.toContain('number'); + expect(inlineStructNames).not.toContain('boolean'); + + cleanup(); + }); + + it('should handle nested type references in InlineStruct', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Address = { + street: string; + city: string; + }; + + export type ContactInfo = { + email: string; + address: Address; + }; + + export type Person = { + name: string; + contact: ContactInfo; + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile : string = sourceFile.getFilePath() + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // ContactInfo should reference Address + const contactInfo = expectToBeDefined(types['ContactInfo']); + expect(expectToBeDefined(contactInfo.InlineStruct).length).toBeGreaterThan(0); + + const contactInfoInlineNames = expectToBeDefined(contactInfo.InlineStruct).map(dep => dep.Name); + expect(contactInfoInlineNames).toContain('Address'); + + // Person should reference ContactInfo + const person = expectToBeDefined(types['Person']); + expect(expectToBeDefined(person.InlineStruct).length).toBeGreaterThan(0); + + const personInlineNames = expectToBeDefined(person.InlineStruct).map(dep => dep.Name); + expect(personInlineNames).toContain('ContactInfo'); + + cleanup(); + }); + }); }); \ No newline at end of file diff --git a/ts-parser/src/parser/test/VarParser.test.ts b/ts-parser/src/parser/test/VarParser.test.ts index ffa41d1..4b9f7ad 100644 --- a/ts-parser/src/parser/test/VarParser.test.ts +++ b/ts-parser/src/parser/test/VarParser.test.ts @@ -276,16 +276,210 @@ describe('VarParser', () => { export { someVar } from './other-module'; export * as namespace from './namespace-module'; `); - + const parser = new VarParser(project, process.cwd()); let pkgPathAbsFile : string = sourceFile.getFilePath() pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/') const pkgPath = path.relative(process.cwd(), pkgPathAbsFile) - + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); - + expect(vars).toBeDefined(); - + + cleanup(); + }); + }); + + describe('type alias dependencies in variable type annotations', () => { + it('should extract union type alias dependencies from variable declarations', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Status = 'normal' | 'abnormal'; + + export const currentStatus: Status = 'normal'; + export let mutableStatus: Status = 'abnormal'; + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // currentStatus should have Status as type dependency + const currentStatus = expectToBeDefined(vars['currentStatus']); + expect(currentStatus.Type).toBeDefined(); + expect(currentStatus.Type?.Name).toBe('Status'); + expect(currentStatus.IsExported).toBe(true); + + // mutableStatus should also have Status as type dependency + const mutableStatus = expectToBeDefined(vars['mutableStatus']); + expect(mutableStatus.Type).toBeDefined(); + expect(mutableStatus.Type?.Name).toBe('Status'); + + cleanup(); + }); + + it('should extract complex type alias dependencies from variable declarations', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserId = string; + export type UserRole = 'admin' | 'user' | 'guest'; + + export type User = { + id: UserId; + role: UserRole; + name: string; + }; + + export const adminUser: User = { + id: 'admin-001', + role: 'admin', + name: 'Admin' + }; + + export const userId: UserId = 'user-123'; + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // adminUser should reference User type + const adminUser = expectToBeDefined(vars['adminUser']); + expect(adminUser.Type).toBeDefined(); + expect(adminUser.Type?.Name).toBe('User'); + + // userId should reference UserId type + const userId = expectToBeDefined(vars['userId']); + expect(userId.Type).toBeDefined(); + expect(userId.Type?.Name).toBe('UserId'); + + cleanup(); + }); + + it('should not include primitive types as type dependencies', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export const name: string = 'John'; + export const age: number = 30; + export const active: boolean = true; + export const nothing: null = null; + export const undef: undefined = undefined; + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // None of these should have Type set (primitive types are not tracked) + expect(vars['name'].Type).toBeUndefined(); + expect(vars['age'].Type).toBeUndefined(); + expect(vars['active'].Type).toBeUndefined(); + expect(vars['nothing'].Type).toBeUndefined(); + expect(vars['undef'].Type).toBeUndefined(); + + cleanup(); + }); + + it('should extract type aliases from destructured variables', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Config = { + host: string; + port: number; + }; + + export type Status = 'running' | 'stopped'; + + export const config: Config = { host: 'localhost', port: 8080 }; + export const status: Status = 'running'; + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // config should reference Config type + const config = expectToBeDefined(vars['config']); + expect(config.Type).toBeDefined(); + expect(config.Type?.Name).toBe('Config'); + + // status should reference Status type + const statusVar = expectToBeDefined(vars['status']); + expect(statusVar.Type).toBeDefined(); + expect(statusVar.Type?.Name).toBe('Status'); + + cleanup(); + }); + + it('should handle array and generic type aliases', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type StringArray = Array; + export type NumberList = number[]; + + export const names: StringArray = ['Alice', 'Bob']; + export const ages: NumberList = [25, 30]; + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // names should reference StringArray type + const names = expectToBeDefined(vars['names']); + expect(names.Type).toBeDefined(); + expect(names.Type?.Name).toBe('StringArray'); + + // ages should reference NumberList type + const ages = expectToBeDefined(vars['ages']); + expect(ages.Type).toBeDefined(); + expect(ages.Type?.Name).toBe('NumberList'); + + cleanup(); + }); + + it('should extract type aliases from interface and class types', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export interface UserInterface { + name: string; + age: number; + } + + export class UserClass { + constructor(public name: string, public age: number) {} + } + + export const user1: UserInterface = { name: 'Alice', age: 25 }; + export const user2: UserClass = new UserClass('Bob', 30); + `); + + const parser = new VarParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const vars = parser.parseVars(sourceFile, 'parser-tests', pkgPath); + + // user1 should reference UserInterface + const user1 = expectToBeDefined(vars['user1']); + expect(user1.Type).toBeDefined(); + expect(user1.Type?.Name).toBe('UserInterface'); + + // user2 should reference UserClass + const user2 = expectToBeDefined(vars['user2']); + expect(user2.Type).toBeDefined(); + expect(user2.Type?.Name).toBe('UserClass'); + cleanup(); }); }); diff --git a/ts-parser/src/utils/dependency-utils.ts b/ts-parser/src/utils/dependency-utils.ts index 0bddc0f..35c03d1 100644 --- a/ts-parser/src/utils/dependency-utils.ts +++ b/ts-parser/src/utils/dependency-utils.ts @@ -36,13 +36,25 @@ export class DependencyUtils { if (t.isTypeParameter()) { return; } - + if(t.isUnion()) { + // If the union type has a symbol (i.e., it's a type alias), add it first + const symbol = t.getSymbol(); + if (symbol) { + results.push(t); + } + // Then recursively process union members t.getUnionTypes().forEach(visit); return; } if (t.isIntersection()) { + // If the intersection type has a symbol (i.e., it's a type alias), add it first + const symbol = t.getSymbol(); + if (symbol) { + results.push(t); + } + // Then recursively process intersection members t.getIntersectionTypes().forEach(visit); return; } @@ -70,7 +82,7 @@ export class DependencyUtils { console.error('Error processing type:', t, error); } } - + visit(type); return results; } From 27cb8c7174f50d10cb078801366602e15dec00d9 Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Mon, 1 Dec 2025 19:50:56 +0800 Subject: [PATCH 4/6] fix type self-reference problem --- ts-parser/package.json | 2 +- ts-parser/src/index.ts | 2 +- ts-parser/src/parser/FunctionParser.ts | 11 +++--- ts-parser/src/parser/PackageParser.ts | 2 +- ts-parser/src/parser/TypeParser.ts | 27 +++++---------- .../src/parser/test/FunctionParser.test.ts | 34 +++++++++++++++++++ ts-parser/src/parser/test/TypeParser.test.ts | 33 ++++++++++++++++++ .../test-repo/src/test-export-default.ts | 11 ++++++ 8 files changed, 95 insertions(+), 27 deletions(-) diff --git a/ts-parser/package.json b/ts-parser/package.json index 2c22c5e..0258387 100644 --- a/ts-parser/package.json +++ b/ts-parser/package.json @@ -1,6 +1,6 @@ { "name": "abcoder-ts-parser", - "version": "0.0.13", + "version": "0.0.20", "description": "TypeScript AST parser for UNIAST specification", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/ts-parser/src/index.ts b/ts-parser/src/index.ts index b0e01dd..bdbd0ab 100644 --- a/ts-parser/src/index.ts +++ b/ts-parser/src/index.ts @@ -11,7 +11,7 @@ const program = new Command(); program .name('abcoder-ts-parser') .description('TypeScript AST parser for UNIAST specification') - .version('0.0.13'); + .version('0.0.20'); program .command('parse') diff --git a/ts-parser/src/parser/FunctionParser.ts b/ts-parser/src/parser/FunctionParser.ts index 1176be4..fc5346c 100644 --- a/ts-parser/src/parser/FunctionParser.ts +++ b/ts-parser/src/parser/FunctionParser.ts @@ -705,12 +705,12 @@ export class FunctionParser { const defEndOffset = decls[0].getEnd(); const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; - // Check if this is not a self-reference within the same function + // Check if this is a self-reference (type reference within its own definition) const isSelfReference = ( resolvedSymbol.moduleName === moduleName && this.getPkgPath(resolvedSymbol.packagePath || packagePath) === packagePath && - defEndOffset <= node.getEnd() && - defStartOffset >= node.getStart() + defStartOffset <= resolvedSymbol.startOffset && + resolvedSymbol.endOffset <= defEndOffset ); if (!visited.has(key) && !isSelfReference) { @@ -775,11 +775,12 @@ export class FunctionParser { EndOffset: resolvedSymbol.endOffset }; + // Check if this is a self-reference (type reference within its own definition) if ( dep.ModPath === moduleName && dep.PkgPath === packagePath && - defEndOffset <= node.getEnd() && - defStartOffset >= node.getStart() + defStartOffset <= resolvedSymbol.startOffset && + resolvedSymbol.endOffset <= defEndOffset ) { continue; } diff --git a/ts-parser/src/parser/PackageParser.ts b/ts-parser/src/parser/PackageParser.ts index e436783..de4aa3a 100644 --- a/ts-parser/src/parser/PackageParser.ts +++ b/ts-parser/src/parser/PackageParser.ts @@ -29,7 +29,7 @@ export class PackageParser { // eslint-disable-next-line @typescript-eslint/no-explicit-any const vars: Record = {}; - for (const sourceFile of sourceFiles) { + for (const sourceFile of sourceFiles) { // Parse functions const fileFunctions = this.functionParser.parseFunctions(sourceFile, moduleName, packagePath); Object.assign(functions, fileFunctions); diff --git a/ts-parser/src/parser/TypeParser.ts b/ts-parser/src/parser/TypeParser.ts index ff4f7fe..81de1db 100644 --- a/ts-parser/src/parser/TypeParser.ts +++ b/ts-parser/src/parser/TypeParser.ts @@ -313,19 +313,12 @@ export class TypeParser { if (resolvedSymbol && !resolvedSymbol.isExternal) { const decls = resolvedRealSymbol?.getDeclarations() || []; if (decls.length > 0) { - const defStartOffset = decls[0].getStart(); - const defEndOffset = decls[0].getEnd(); const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; - // Check if this is not a self-reference - const isSelfReference = ( - resolvedSymbol.moduleName === moduleName && - this.getPkgPath(resolvedSymbol.packagePath || packagePath) === packagePath && - defStartOffset <= resolvedSymbol.startOffset && - resolvedSymbol.endOffset <= defEndOffset - ); + // Check if this is a self-reference: the type reference is within its own definition + const isSelfRef = typeNode.getAncestors().some(ancestor => ancestor === decls[0]); - if (!visited.has(key) && !isSelfReference) { + if (!visited.has(key) && !isSelfRef) { visited.add(key); dependencies.push({ ModPath: resolvedSymbol.moduleName || moduleName, @@ -380,8 +373,11 @@ export class TypeParser { continue; } - const defStartOffset = decls[0].getStart(); - const defEndOffset = decls[0].getEnd(); + // Check if this is a self-reference: the type reference is within its own definition + // If typeRef's ancestors include decls[0], it's a self-reference + const isSelfRef = typeRef.getAncestors().some(ancestor => ancestor === decls[0]); + + if (isSelfRef) continue; visited.add(key); const dep: Dependency = { @@ -394,13 +390,6 @@ export class TypeParser { EndOffset: resolvedSymbol.endOffset }; - // Skip self-references - if ( - dep.ModPath === moduleName && - dep.PkgPath === packagePath && - defStartOffset <= resolvedSymbol.startOffset && - resolvedSymbol.endOffset <= defEndOffset - ) continue; dependencies.push(dep); } diff --git a/ts-parser/src/parser/test/FunctionParser.test.ts b/ts-parser/src/parser/test/FunctionParser.test.ts index e48e989..d5793d7 100644 --- a/ts-parser/src/parser/test/FunctionParser.test.ts +++ b/ts-parser/src/parser/test/FunctionParser.test.ts @@ -749,5 +749,39 @@ describe('FunctionParser', () => { cleanup(); }); + + it('should filter out self-referencing recursive types', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type TreeNode = { + value: string; + children: TreeNode[]; + }; + + export function processTree(node: TreeNode): void { + console.log(node.value); + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const processTree = expectToBeDefined(functions['processTree']); + + // Should have TreeNode in Types array + expect(processTree.Types).toBeDefined(); + const typeNames = expectToBeDefined(processTree.Types).map(dep => dep.Name); + + expect(typeNames).toContain('TreeNode'); + + // TreeNode should only appear once (the self-reference in TreeNode definition should be filtered) + const treeNodeCount = typeNames.filter(name => name === 'TreeNode').length; + expect(treeNodeCount).toBe(1); + + cleanup(); + }); }); }); \ No newline at end of file diff --git a/ts-parser/src/parser/test/TypeParser.test.ts b/ts-parser/src/parser/test/TypeParser.test.ts index 460072f..7ea0476 100644 --- a/ts-parser/src/parser/test/TypeParser.test.ts +++ b/ts-parser/src/parser/test/TypeParser.test.ts @@ -671,5 +671,38 @@ describe('TypeParser', () => { cleanup(); }); + + it('should filter out self-referencing recursive types in InlineStruct', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type TreeNode = { + value: string; + children: TreeNode[]; + }; + + export type LinkedListNode = { + data: number; + next: LinkedListNode | null; + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // TreeNode should not include itself in InlineStruct (self-reference should be filtered) + const treeNode = expectToBeDefined(types['TreeNode']); + const treeNodeInlineNames = (treeNode.InlineStruct || []).map(dep => dep.Name); + expect(treeNodeInlineNames).not.toContain('TreeNode'); + + // LinkedListNode should not include itself in InlineStruct + const linkedListNode = expectToBeDefined(types['LinkedListNode']); + const linkedListInlineNames = (linkedListNode.InlineStruct || []).map(dep => dep.Name); + expect(linkedListInlineNames).not.toContain('LinkedListNode'); + + cleanup(); + }); }); }); \ No newline at end of file diff --git a/ts-parser/test-repo/src/test-export-default.ts b/ts-parser/test-repo/src/test-export-default.ts index 78ef9f1..64ce8e1 100644 --- a/ts-parser/test-repo/src/test-export-default.ts +++ b/ts-parser/test-repo/src/test-export-default.ts @@ -9,3 +9,14 @@ export default foo; export const bar = () => { console.log('baz') } + +export type Status = 'normal' | 'abnormal' + +export type ServerStatus = { + code: number; + status: Status; +} + +export const flipStatus = (s: Status): Status => { + return s === 'normal' ? 'abnormal' : 'normal'; +} From 715c8907a44f92a7ff568098be6e5682546ea802 Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Mon, 1 Dec 2025 21:31:37 +0800 Subject: [PATCH 5/6] filtering type parameters --- ts-parser/src/parser/FunctionParser.ts | 15 ++++ ts-parser/src/parser/TypeParser.ts | 80 +++++++++++++------ .../test-repo/src/test-export-default.ts | 12 ++- 3 files changed, 83 insertions(+), 24 deletions(-) diff --git a/ts-parser/src/parser/FunctionParser.ts b/ts-parser/src/parser/FunctionParser.ts index fc5346c..3631899 100644 --- a/ts-parser/src/parser/FunctionParser.ts +++ b/ts-parser/src/parser/FunctionParser.ts @@ -655,6 +655,12 @@ export class FunctionParser { const types: Dependency[] = []; const visited = new Set(); + // Collect all type parameter names from this node to filter them out + const typeParamNames = new Set(); + for (const typeParam of node.getTypeParameters()) { + typeParamNames.add(typeParam.getName()); + } + // Extract from type references and find their definitions const typeNodes: TypeNode[] = node.getDescendantsOfKind(SyntaxKind.TypeReference) @@ -696,6 +702,10 @@ export class FunctionParser { if (directSymbol) { const directTypeName = directSymbol.getName(); + // Skip if this is a type parameter + if (typeParamNames.has(directTypeName)) { + continue; + } if (!this.isPrimitiveType(directTypeName)) { const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(directSymbol, typeNode); if (resolvedSymbol && !resolvedSymbol.isExternal) { @@ -740,6 +750,11 @@ export class FunctionParser { continue; } + // Skip if this is a type parameter + if (typeParamNames.has(typeName)) { + continue; + } + const symbol = typeRef.getSymbol(); if (!symbol) { continue; diff --git a/ts-parser/src/parser/TypeParser.ts b/ts-parser/src/parser/TypeParser.ts index 81de1db..e9105bb 100644 --- a/ts-parser/src/parser/TypeParser.ts +++ b/ts-parser/src/parser/TypeParser.ts @@ -105,6 +105,12 @@ export class TypeParser { const content = cls.getFullText(); const isExported = cls.isExported() || cls.isDefaultExport() || (sym === this.defaultExported && sym !== undefined); + // Collect type parameter names + const typeParamNames = new Set(); + for (const typeParam of cls.getTypeParameters()) { + typeParamNames.add(typeParam.getName()); + } + // Parse methods // eslint-disable-next-line @typescript-eslint/no-explicit-any const methods: Record = {}; @@ -128,7 +134,7 @@ export class TypeParser { const typeNodes = clause.getTypeNodes(); for (const typeNode of typeNodes) { - const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath); + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); if (clauseType === SyntaxKind.ImplementsKeyword) { implementsInterfaces.push(...dependencies); } else if (clauseType === SyntaxKind.ExtendsKeyword) { @@ -170,6 +176,12 @@ export class TypeParser { const content = iface.getFullText(); const isExported = iface.isExported() || iface.isDefaultExport() || (sym === this.defaultExported && sym !== undefined); + // Collect type parameter names + const typeParamNames = new Set(); + for (const typeParam of iface.getTypeParameters()) { + typeParamNames.add(typeParam.getName()); + } + // Parse methods // eslint-disable-next-line @typescript-eslint/no-explicit-any const methods: Record = {}; @@ -190,7 +202,7 @@ export class TypeParser { if (clause.getToken() === SyntaxKind.ExtendsKeyword) { const typeNodes = clause.getTypeNodes(); for (const typeNode of typeNodes) { - const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath); + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); extendsInterfaces.push(...dependencies); } } @@ -229,11 +241,17 @@ export class TypeParser { const content = typeAlias.getFullText(); const isExported = typeAlias.isExported() || typeAlias.isDefaultExport() || (sym === this.defaultExported && sym !== undefined); + // Collect type parameter names + const typeParamNames = new Set(); + for (const typeParam of typeAlias.getTypeParameters()) { + typeParamNames.add(typeParam.getName()); + } + // Extract type dependencies from the type alias const typeDependencies: Dependency[] = []; const typeNode = typeAlias.getTypeNode(); if (typeNode) { - const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath); + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); typeDependencies.push(...dependencies); } @@ -290,7 +308,7 @@ export class TypeParser { * This handles union types, intersection types, generics, arrays, etc. * Uses SymbolResolver for consistent dependency resolution, similar to extractTypeReferences */ - private extractTypeDependencies(typeNode: TypeNode, moduleName: string, packagePath: string): Dependency[] { + private extractTypeDependencies(typeNode: TypeNode, moduleName: string, packagePath: string, typeParamNames?: Set): Dependency[] { const dependencies: Dependency[] = []; const visited = new Set(); @@ -311,24 +329,29 @@ export class TypeParser { if (symbol) { const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeNode); if (resolvedSymbol && !resolvedSymbol.isExternal) { - const decls = resolvedRealSymbol?.getDeclarations() || []; - if (decls.length > 0) { - const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; - - // Check if this is a self-reference: the type reference is within its own definition - const isSelfRef = typeNode.getAncestors().some(ancestor => ancestor === decls[0]); - - if (!visited.has(key) && !isSelfRef) { - visited.add(key); - dependencies.push({ - ModPath: resolvedSymbol.moduleName || moduleName, - PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), - Name: resolvedSymbol.name, - File: resolvedSymbol.filePath, - Line: resolvedSymbol.line, - StartOffset: resolvedSymbol.startOffset, - EndOffset: resolvedSymbol.endOffset - }); + // Skip if this is a type parameter + if (typeParamNames && typeParamNames.has(resolvedSymbol.name)) { + // Skip this type parameter + } else { + const decls = resolvedRealSymbol?.getDeclarations() || []; + if (decls.length > 0) { + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; + + // Check if this is a self-reference: the type reference is within its own definition + const isSelfRef = typeNode.getAncestors().some(ancestor => ancestor === decls[0]); + + if (!visited.has(key) && !isSelfRef) { + visited.add(key); + dependencies.push({ + ModPath: resolvedSymbol.moduleName || moduleName, + PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), + Name: resolvedSymbol.name, + File: resolvedSymbol.filePath, + Line: resolvedSymbol.line, + StartOffset: resolvedSymbol.startOffset, + EndOffset: resolvedSymbol.endOffset + }); + } } } } @@ -363,6 +386,11 @@ export class TypeParser { continue; } + // Skip if this is a type parameter + if (typeParamNames && typeParamNames.has(resolvedSymbol.name)) { + continue; + } + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; if (visited.has(key)) { continue; @@ -424,6 +452,12 @@ export class TypeParser { const endOffset = classExpr.getEnd(); const content = classExpr.getFullText(); + // Collect type parameter names + const typeParamNames = new Set(); + for (const typeParam of classExpr.getTypeParameters()) { + typeParamNames.add(typeParam.getName()); + } + // Parse methods // eslint-disable-next-line @typescript-eslint/no-explicit-any const methods: Record = {}; @@ -447,7 +481,7 @@ export class TypeParser { const typeNodes = clause.getTypeNodes(); for (const typeNode of typeNodes) { - const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath); + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); if (clauseType === SyntaxKind.ImplementsKeyword) { implementsInterfaces.push(...dependencies); diff --git a/ts-parser/test-repo/src/test-export-default.ts b/ts-parser/test-repo/src/test-export-default.ts index 64ce8e1..06a5783 100644 --- a/ts-parser/test-repo/src/test-export-default.ts +++ b/ts-parser/test-repo/src/test-export-default.ts @@ -12,11 +12,21 @@ export const bar = () => { export type Status = 'normal' | 'abnormal' +export type Result = T | Status + export type ServerStatus = { code: number; status: Status; } -export const flipStatus = (s: Status): Status => { +export const convert = (s: T): Result => { + // 如果输入是字符串,返回 'normal',否则返回输入本身 + if (typeof s === 'string') { + return 'normal'; + } + return s; +}; + +export const flipStatus = (s: Status): Result => { return s === 'normal' ? 'abnormal' : 'normal'; } From 9cd54c2bbf2765cf28442209bcd207e80bd871b5 Mon Sep 17 00:00:00 2001 From: Pu Junsong Date: Tue, 2 Dec 2025 16:26:23 +0800 Subject: [PATCH 6/6] support class parsing --- ts-parser/package.json | 2 +- ts-parser/src/index.ts | 2 +- ts-parser/src/parser/FunctionParser.ts | 387 +++++++++++++++- ts-parser/src/parser/TypeParser.ts | 132 +++++- ts-parser/src/parser/VarParser.ts | 16 + .../src/parser/test/FunctionParser.test.ts | 398 +++++++++++++++++ ts-parser/src/parser/test/TypeParser.test.ts | 418 ++++++++++++++++++ ts-parser/src/utils/symbol-resolver.ts | 14 +- ts-parser/test-repo/src/middleware/Test.ts | 3 + ts-parser/test-repo/src/middleware/Test2.ts | 7 + .../test-repo/src/test-class-method-deps.ts | 31 ++ 11 files changed, 1384 insertions(+), 26 deletions(-) create mode 100644 ts-parser/test-repo/src/middleware/Test.ts create mode 100644 ts-parser/test-repo/src/middleware/Test2.ts create mode 100644 ts-parser/test-repo/src/test-class-method-deps.ts diff --git a/ts-parser/package.json b/ts-parser/package.json index 0258387..1e7064b 100644 --- a/ts-parser/package.json +++ b/ts-parser/package.json @@ -1,6 +1,6 @@ { "name": "abcoder-ts-parser", - "version": "0.0.20", + "version": "0.0.21", "description": "TypeScript AST parser for UNIAST specification", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/ts-parser/src/index.ts b/ts-parser/src/index.ts index bdbd0ab..2da247d 100644 --- a/ts-parser/src/index.ts +++ b/ts-parser/src/index.ts @@ -11,7 +11,7 @@ const program = new Command(); program .name('abcoder-ts-parser') .description('TypeScript AST parser for UNIAST specification') - .version('0.0.20'); + .version('0.0.21'); program .command('parse') diff --git a/ts-parser/src/parser/FunctionParser.ts b/ts-parser/src/parser/FunctionParser.ts index 3631899..d73b760 100644 --- a/ts-parser/src/parser/FunctionParser.ts +++ b/ts-parser/src/parser/FunctionParser.ts @@ -7,6 +7,8 @@ import { ArrowFunction, FunctionExpression, MethodSignature, + GetAccessorDeclaration, + SetAccessorDeclaration, Node, SyntaxKind, ParameterDeclaration, @@ -98,6 +100,28 @@ export class FunctionParser { console.error('Error processing static method:', staticMethod, error); } } + + // Parse getters + const getAccessors = cls.getGetAccessors(); + for (const getter of getAccessors) { + try { + const getterObj = this.parseGetAccessor(getter, moduleName, packagePath, sourceFile, className); + functions[getterObj.Name] = getterObj; + } catch (error) { + console.error('Error processing getter:', getter, error); + } + } + + // Parse setters + const setAccessors = cls.getSetAccessors(); + for (const setter of setAccessors) { + try { + const setterObj = this.parseSetAccessor(setter, moduleName, packagePath, sourceFile, className); + functions[setterObj.Name] = setterObj; + } catch (error) { + console.error('Error processing setter:', setter, error); + } + } } // Parse arrow functions assigned to variables @@ -222,6 +246,15 @@ export class FunctionParser { let isExported = false; if (Node.isClassDeclaration(parent)) { isExported = parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === parentSym && parentSym !== undefined); + } else if (Node.isClassExpression(parent)) { + // ClassExpression can be exported if assigned to an exported variable or used in default export + // For now, we check if the parent's parent is an exported variable statement + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (this.defaultExportSymbol === varSymbol && varSymbol !== undefined)) : false; + } } // Parse receiver @@ -237,6 +270,9 @@ export class FunctionParser { // Parse parameters const params = this.parseParameters(method.getParameters(), moduleName, packagePath, sourceFile); + // Parse return types + const results = this.parseReturnTypes(method, moduleName, packagePath, sourceFile); + // Parse function calls const functionCalls = this.extractFunctionCalls(method, moduleName, packagePath, sourceFile); const methodCalls = this.extractMethodCalls(method, moduleName, packagePath, sourceFile); @@ -260,7 +296,7 @@ export class FunctionParser { Signature: signature, Receiver: receiver, Params: params, - Results: [], + Results: results, FunctionCalls: functionCalls, MethodCalls: methodCalls, Types: types, @@ -282,6 +318,37 @@ export class FunctionParser { const content = method.getFullText(); const signature = method.getText(); + // Get interface name for receiver + const parent = method.getParent(); + let interfaceName = ""; + if (Node.isInterfaceDeclaration(parent)) { + const interfaceSym = parent.getSymbol(); + if (interfaceSym) { + interfaceName = assignSymbolName(interfaceSym); + } else { + interfaceName = parent.getName() || "anonymous_" + parent.getStart(); + } + } + + // Parse receiver + const receiver: Receiver = { + IsPointer: false, + Type: { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: interfaceName + } + }; + + // Parse parameters + const params = this.parseParameters(method.getParameters(), moduleName, packagePath, sourceFile); + + // Parse return types + const results = this.parseReturnTypes(method, moduleName, packagePath, sourceFile); + + // Extract type references from method signature (including generic type parameters) + const types = this.extractTypeReferences(method, moduleName, packagePath, sourceFile); + return { ModPath: moduleName, PkgPath: this.getPkgPath(packagePath), @@ -295,11 +362,12 @@ export class FunctionParser { IsInterfaceMethod: true, Content: content, Signature: signature, - Params: [], - Results: [], + Receiver: receiver, + Params: params, + Results: results, FunctionCalls: [], MethodCalls: [], - Types: [], + Types: types, GlobalVars: [] }; } @@ -323,11 +391,33 @@ export class FunctionParser { if (Node.isClassDeclaration(parent)) { const parentSym = parent.getSymbol() isExported = parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === parentSym && parentSym !== undefined); + } else if (Node.isClassExpression(parent)) { + // ClassExpression can be exported if assigned to an exported variable or used in default export + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (this.defaultExportSymbol === varSymbol && varSymbol !== undefined)) : false; + } } + // Parse receiver + const receiver: Receiver = { + IsPointer: false, + Type: { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: className + } + }; + // Parse parameters const params = this.parseParameters(ctor.getParameters(), moduleName, packagePath, sourceFile); + // Parse function calls + const functionCalls = this.extractFunctionCalls(ctor, moduleName, packagePath, sourceFile); + const methodCalls = this.extractMethodCalls(ctor, moduleName, packagePath, sourceFile); + // Extract type references and global variables from constructor body const types = this.extractTypeReferences(ctor, moduleName, packagePath, sourceFile); const globalVars = this.extractGlobalVarReferences(ctor, moduleName, packagePath, sourceFile); @@ -345,10 +435,11 @@ export class FunctionParser { IsInterfaceMethod: false, Content: content, Signature: signature, + Receiver: receiver, Params: params, Results: [], - FunctionCalls: [], - MethodCalls: [], + FunctionCalls: functionCalls, + MethodCalls: methodCalls, Types: types, GlobalVars: globalVars }; @@ -371,6 +462,9 @@ export class FunctionParser { // Parse parameters const params = this.parseParameters(arrowFunc.getParameters(), moduleName, packagePath, sourceFile); + // Parse return types + const results = this.parseReturnTypes(arrowFunc, moduleName, packagePath, sourceFile); + // Parse function calls const functionCalls = this.extractFunctionCalls(arrowFunc, moduleName, packagePath, sourceFile); const methodCalls = this.extractMethodCalls(arrowFunc, moduleName, packagePath, sourceFile); @@ -404,6 +498,150 @@ export class FunctionParser { Content: content, Signature: signature, Params: params, + Results: results, + FunctionCalls: functionCalls, + MethodCalls: methodCalls, + Types: types, + GlobalVars: globalVars + }; + } + + private parseGetAccessor(getter: GetAccessorDeclaration, moduleName: string, packagePath: string, sourceFile: SourceFile, className: string): UniFunction { + const symbol = getter.getSymbol(); + let accessorName = "" + if (symbol) { + accessorName = assignSymbolName(symbol) + } else { + accessorName = "anonymous_" + getter.getStart() + } + const startLine = getter.getStartLineNumber(); + const startOffset = getter.getStart(); + const endOffset = getter.getEnd(); + const content = getter.getFullText(); + const signature = this.extractSignature(getter); + + const parent = getter.getParent(); + const parentSym = parent.getSymbol() + let isExported = false; + if (Node.isClassDeclaration(parent)) { + isExported = parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === parentSym && parentSym !== undefined); + } else if (Node.isClassExpression(parent)) { + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (this.defaultExportSymbol === varSymbol && varSymbol !== undefined)) : false; + } + } + + // Parse receiver + const receiver: Receiver = { + IsPointer: false, + Type: { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: className + } + }; + + // Parse return types + const results = this.parseReturnTypes(getter, moduleName, packagePath, sourceFile); + + // Parse function calls + const functionCalls = this.extractFunctionCalls(getter, moduleName, packagePath, sourceFile); + const methodCalls = this.extractMethodCalls(getter, moduleName, packagePath, sourceFile); + + // Extract type references and global variables + const types = this.extractTypeReferences(getter, moduleName, packagePath, sourceFile); + const globalVars = this.extractGlobalVarReferences(getter, moduleName, packagePath, sourceFile); + + return { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: accessorName, + File: this.getRelativePath(sourceFile.getFilePath()), + Line: startLine, + StartOffset: startOffset, + EndOffset: endOffset, + Exported: isExported, + IsMethod: true, + IsInterfaceMethod: false, + Content: content, + Signature: signature, + Receiver: receiver, + Params: [], + Results: results, + FunctionCalls: functionCalls, + MethodCalls: methodCalls, + Types: types, + GlobalVars: globalVars + }; + } + + private parseSetAccessor(setter: SetAccessorDeclaration, moduleName: string, packagePath: string, sourceFile: SourceFile, className: string): UniFunction { + const symbol = setter.getSymbol(); + let accessorName = "" + if (symbol) { + accessorName = assignSymbolName(symbol) + } else { + accessorName = "anonymous_" + setter.getStart() + } + const startLine = setter.getStartLineNumber(); + const startOffset = setter.getStart(); + const endOffset = setter.getEnd(); + const content = setter.getFullText(); + const signature = this.extractSignature(setter); + + const parent = setter.getParent(); + const parentSym = parent.getSymbol() + let isExported = false; + if (Node.isClassDeclaration(parent)) { + isExported = parent.isExported() || parent.isDefaultExport() || (this.defaultExportSymbol === parentSym && parentSym !== undefined); + } else if (Node.isClassExpression(parent)) { + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (this.defaultExportSymbol === varSymbol && varSymbol !== undefined)) : false; + } + } + + // Parse receiver + const receiver: Receiver = { + IsPointer: false, + Type: { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: className + } + }; + + // Parse parameters + const params = this.parseParameters(setter.getParameters(), moduleName, packagePath, sourceFile); + + // Parse function calls + const functionCalls = this.extractFunctionCalls(setter, moduleName, packagePath, sourceFile); + const methodCalls = this.extractMethodCalls(setter, moduleName, packagePath, sourceFile); + + // Extract type references and global variables + const types = this.extractTypeReferences(setter, moduleName, packagePath, sourceFile); + const globalVars = this.extractGlobalVarReferences(setter, moduleName, packagePath, sourceFile); + + return { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: accessorName, + File: this.getRelativePath(sourceFile.getFilePath()), + Line: startLine, + StartOffset: startOffset, + EndOffset: endOffset, + Exported: isExported, + IsMethod: true, + IsInterfaceMethod: false, + Content: content, + Signature: signature, + Receiver: receiver, + Params: params, Results: [], FunctionCalls: functionCalls, MethodCalls: methodCalls, @@ -412,21 +650,136 @@ export class FunctionParser { }; } - // TODO: parse parameters - private parseParameters(_parameters: ParameterDeclaration[], _moduleName: string, _packagePath: string, _sourceFile: SourceFile): Dependency[] { + // Parse parameters and extract type dependencies + private parseParameters(parameters: ParameterDeclaration[], moduleName: string, packagePath: string, _sourceFile: SourceFile): Dependency[] { const dependencies: Dependency[] = []; + const visited = new Set(); + + for (const param of parameters) { + const typeNode = param.getTypeNode(); + if (!typeNode) continue; + + // Extract type references from parameter type + const typeReferences: Node[] = []; + + // Handle direct type reference + if (Node.isTypeReference(typeNode)) { + typeReferences.push(typeNode); + } + + // Also get all descendant type references + typeReferences.push(...typeNode.getDescendantsOfKind(SyntaxKind.TypeReference)); + + for (const typeRef of typeReferences) { + if (!Node.isTypeReference(typeRef)) continue; + + const typeName = typeRef.getTypeName(); + let symbol: Symbol | undefined; + + if (Node.isIdentifier(typeName)) { + symbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + symbol = typeName.getRight().getSymbol(); + } + + if (!symbol) continue; + + const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeRef); + if (!resolvedSymbol || resolvedSymbol.isExternal) { + continue; + } + + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; + if (visited.has(key)) { + continue; + } + + const decls = resolvedRealSymbol?.getDeclarations() || []; + if (decls.length === 0) { + continue; + } + + visited.add(key); + dependencies.push({ + ModPath: resolvedSymbol.moduleName || moduleName, + PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), + Name: resolvedSymbol.name, + File: resolvedSymbol.filePath, + Line: resolvedSymbol.line, + StartOffset: resolvedSymbol.startOffset, + EndOffset: resolvedSymbol.endOffset + }); + } + } return dependencies; } - // TODO: parse return types - private parseReturnTypes(_func: FunctionDeclaration | MethodSignature, _moduleName: string, _packagePath: string, _sourceFile: SourceFile): Dependency[] { + // Parse return types and extract type dependencies + private parseReturnTypes(func: FunctionDeclaration | MethodDeclaration | MethodSignature | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, moduleName: string, packagePath: string, _sourceFile: SourceFile): Dependency[] { const results: Dependency[] = []; + const visited = new Set(); + + const returnTypeNode = func.getReturnTypeNode(); + if (!returnTypeNode) return results; + + // Extract type references from return type + const typeReferences: Node[] = []; + + // Handle direct type reference + if (Node.isTypeReference(returnTypeNode)) { + typeReferences.push(returnTypeNode); + } + + // Also get all descendant type references + typeReferences.push(...returnTypeNode.getDescendantsOfKind(SyntaxKind.TypeReference)); + + for (const typeRef of typeReferences) { + if (!Node.isTypeReference(typeRef)) continue; + + const typeName = typeRef.getTypeName(); + let symbol: Symbol | undefined; + + if (Node.isIdentifier(typeName)) { + symbol = typeName.getSymbol(); + } else if (Node.isQualifiedName(typeName)) { + symbol = typeName.getRight().getSymbol(); + } + + if (!symbol) continue; + + const [resolvedSymbol, resolvedRealSymbol] = this.symbolResolver.resolveSymbol(symbol, typeRef); + if (!resolvedSymbol || resolvedSymbol.isExternal) { + continue; + } + + const key = `${resolvedSymbol.moduleName}?${resolvedSymbol.packagePath}#${resolvedSymbol.name}`; + if (visited.has(key)) { + continue; + } + + const decls = resolvedRealSymbol?.getDeclarations() || []; + if (decls.length === 0) { + continue; + } + + visited.add(key); + results.push({ + ModPath: resolvedSymbol.moduleName || moduleName, + PkgPath: this.getPkgPath(resolvedSymbol.packagePath || packagePath), + Name: resolvedSymbol.name, + File: resolvedSymbol.filePath, + Line: resolvedSymbol.line, + StartOffset: resolvedSymbol.startOffset, + EndOffset: resolvedSymbol.endOffset + }); + } + return results; } private extractFunctionCalls( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, moduleName: string, packagePath: string, _sourceFile: SourceFile @@ -492,7 +845,7 @@ export class FunctionParser { } private extractMethodCalls( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, moduleName: string, packagePath: string, sourceFile: SourceFile @@ -532,7 +885,7 @@ export class FunctionParser { } private processNewCall( - callerNode: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + callerNode: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, newExpr: Identifier, moduleName: string, packagePath: string, @@ -589,7 +942,7 @@ export class FunctionParser { } private processMethodCall( - callerNode: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + callerNode: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, propAccess: PropertyAccessExpression, moduleName: string, packagePath: string, @@ -647,7 +1000,7 @@ export class FunctionParser { private extractTypeReferences( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration, moduleName: string, packagePath: string, _sourceFile: SourceFile @@ -807,7 +1160,7 @@ export class FunctionParser { } private extractGlobalVarReferences( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression, + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration, moduleName: string, packagePath: string, _sourceFile: SourceFile @@ -928,7 +1281,7 @@ export class FunctionParser { } private extractSignature( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | ArrowFunction | FunctionExpression | GetAccessorDeclaration | SetAccessorDeclaration ): string { if (Node.isArrowFunction(node)) { const equalsGreaterThanToken = node.getEqualsGreaterThan(); diff --git a/ts-parser/src/parser/TypeParser.ts b/ts-parser/src/parser/TypeParser.ts index e9105bb..577d5c6 100644 --- a/ts-parser/src/parser/TypeParser.ts +++ b/ts-parser/src/parser/TypeParser.ts @@ -114,6 +114,8 @@ export class TypeParser { // Parse methods // eslint-disable-next-line @typescript-eslint/no-explicit-any const methods: Record = {}; + + // Parse instance methods const classMethods = cls.getMethods(); for (const method of classMethods) { const methodName = method.getName() || 'anonymous'; @@ -124,6 +126,51 @@ export class TypeParser { }; } + // Parse constructors + const constructors = cls.getConstructors(); + for (const ctor of constructors) { + // Constructors don't have symbols, so we use 'constructor' as the key + const ctorName = 'constructor'; + methods[ctorName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${ctorName}` + }; + } + + // Parse static methods + const staticMethods = cls.getStaticMethods(); + for (const staticMethod of staticMethods) { + const methodName = staticMethod.getName() || 'anonymous'; + methods[methodName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${methodName}` + }; + } + + // Parse getters + const getAccessors = cls.getGetAccessors(); + for (const getter of getAccessors) { + const getterName = getter.getName() || 'anonymous'; + methods[getterName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${getterName}` + }; + } + + // Parse setters + const setAccessors = cls.getSetAccessors(); + for (const setter of setAccessors) { + const setterName = setter.getName() || 'anonymous'; + methods[setterName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${setterName}` + }; + } + // Parse implemented interfaces and extended classes const implementsInterfaces: Dependency[] = []; const extendsClasses: Dependency[] = []; @@ -146,6 +193,17 @@ export class TypeParser { // Combine implements and extends into Implements, but filter out external symbols const allImplements = [...implementsInterfaces, ...extendsClasses]; + // Extract property type dependencies + const propertyTypes: Dependency[] = []; + const properties = cls.getProperties(); + for (const prop of properties) { + const typeNode = prop.getTypeNode(); + if (typeNode) { + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); + propertyTypes.push(...dependencies); + } + } + return { ModPath: moduleName, PkgPath: this.getPkgPath(packagePath), @@ -159,7 +217,7 @@ export class TypeParser { Content: content, Methods: methods, Implements: allImplements, - SubStruct: [], + SubStruct: propertyTypes, InlineStruct: [] }; } @@ -210,6 +268,17 @@ export class TypeParser { // Combine extends interfaces and other dependencies into Implements, but filter out external symbols const allImplements = [...extendsInterfaces]; + // Extract property type dependencies + const propertyTypes: Dependency[] = []; + const properties = iface.getProperties(); + for (const prop of properties) { + const typeNode = prop.getTypeNode(); + if (typeNode) { + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); + propertyTypes.push(...dependencies); + } + } + return { ModPath: moduleName, PkgPath: this.getPkgPath(packagePath), @@ -223,7 +292,7 @@ export class TypeParser { Content: content, Methods: methods, Implements: allImplements, - SubStruct: [], + SubStruct: propertyTypes, InlineStruct: [] }; } @@ -461,6 +530,8 @@ export class TypeParser { // Parse methods // eslint-disable-next-line @typescript-eslint/no-explicit-any const methods: Record = {}; + + // Parse instance methods const classMethods = classExpr.getMethods(); for (const method of classMethods) { const methodName = method.getName() || 'anonymous'; @@ -471,6 +542,51 @@ export class TypeParser { }; } + // Parse constructors + const constructors = classExpr.getConstructors(); + for (const ctor of constructors) { + // Constructors don't have symbols, so we use 'constructor' as the key + const ctorName = 'constructor'; + methods[ctorName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${ctorName}` + }; + } + + // Parse static methods + const staticMethods = classExpr.getStaticMethods(); + for (const staticMethod of staticMethods) { + const methodName = staticMethod.getName() || 'anonymous'; + methods[methodName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${methodName}` + }; + } + + // Parse getters + const getAccessors = classExpr.getGetAccessors(); + for (const getter of getAccessors) { + const getterName = getter.getName() || 'anonymous'; + methods[getterName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${getterName}` + }; + } + + // Parse setters + const setAccessors = classExpr.getSetAccessors(); + for (const setter of setAccessors) { + const setterName = setter.getName() || 'anonymous'; + methods[setterName] = { + ModPath: moduleName, + PkgPath: this.getPkgPath(packagePath), + Name: `${name}.${setterName}` + }; + } + // Parse implemented interfaces and extended classes const implementsInterfaces: Dependency[] = []; const extendsClasses: Dependency[] = []; @@ -491,6 +607,16 @@ export class TypeParser { } } + // Extract property type dependencies + const propertyTypes: Dependency[] = []; + const properties = classExpr.getProperties(); + for (const prop of properties) { + const typeNode = prop.getTypeNode(); + if (typeNode) { + const dependencies = this.extractTypeDependencies(typeNode, moduleName, packagePath, typeParamNames); + propertyTypes.push(...dependencies); + } + } return { ModPath: moduleName, @@ -505,7 +631,7 @@ export class TypeParser { Content: content, Methods: methods, Implements: [...implementsInterfaces, ...extendsClasses], - SubStruct: [], + SubStruct: propertyTypes, InlineStruct: [] }; } diff --git a/ts-parser/src/parser/VarParser.ts b/ts-parser/src/parser/VarParser.ts index 840e21f..6110cf8 100644 --- a/ts-parser/src/parser/VarParser.ts +++ b/ts-parser/src/parser/VarParser.ts @@ -219,6 +219,14 @@ export class VarParser { let isExported = false; if (Node.isClassDeclaration(parent)) { isExported = parent.isExported() || parent.isDefaultExport() || (parent.getSymbol() === this.defaultExportedSym && this.defaultExportedSym !== undefined); + } else if (Node.isClassExpression(parent)) { + // ClassExpression can be exported if assigned to an exported variable + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (varSymbol === this.defaultExportedSym && varSymbol !== undefined)) : false; + } } const isConst = false; @@ -401,6 +409,14 @@ export class VarParser { const parent = parentNode.getParent(); if (Node.isClassDeclaration(parent)) { isExported = parent.isExported() || parent.isDefaultExport() || (parent.getSymbol() === this.defaultExportedSym && this.defaultExportedSym !== undefined); + } else if (Node.isClassExpression(parent)) { + // ClassExpression can be exported if assigned to an exported variable + const grandParent = parent.getParent(); + if (Node.isVariableDeclaration(grandParent)) { + const varStatement = grandParent.getVariableStatement(); + const varSymbol = grandParent.getSymbol(); + isExported = varStatement ? (varStatement.isExported() || varStatement.isDefaultExport() || (varSymbol === this.defaultExportedSym && varSymbol !== undefined)) : false; + } } } diff --git a/ts-parser/src/parser/test/FunctionParser.test.ts b/ts-parser/src/parser/test/FunctionParser.test.ts index d5793d7..6b10051 100644 --- a/ts-parser/src/parser/test/FunctionParser.test.ts +++ b/ts-parser/src/parser/test/FunctionParser.test.ts @@ -783,5 +783,403 @@ describe('FunctionParser', () => { cleanup(); }); + + it('should extract function calls from constructors', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export function TestMiddleware() { + console.log('Test middleware'); + } + + export default class TestMiddleware2 { + constructor() { + TestMiddleware() + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + // Find the constructor function + const constructorFunc = Object.values(functions).find(f => f.Name.includes('constructor')); + expect(constructorFunc).toBeDefined(); + + const ctor = expectToBeDefined(constructorFunc); + + // Should extract TestMiddleware function call + expect(ctor.FunctionCalls).toBeDefined(); + const functionCalls = expectToBeDefined(ctor.FunctionCalls); + expect(functionCalls.length).toBeGreaterThan(0); + + const callNames = functionCalls.map(call => call.Name); + expect(callNames).toContain('TestMiddleware'); + + cleanup(); + }); + }); + + describe('parameter and return type dependencies', () => { + it('should extract parameter type dependencies', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserType = { + id: number; + name: string; + }; + + export type ResultType = { + success: boolean; + data: any; + }; + + export function processUser(user: UserType): ResultType { + return { success: true, data: user }; + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const processUser = expectToBeDefined(functions['processUser']); + + // Should extract UserType from parameters + expect(processUser.Params).toBeDefined(); + const params = expectToBeDefined(processUser.Params); + expect(params.length).toBeGreaterThan(0); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('UserType'); + + // Should extract ResultType from return type + expect(processUser.Results).toBeDefined(); + const results = expectToBeDefined(processUser.Results); + expect(results.length).toBeGreaterThan(0); + const resultNames = results.map(r => r.Name); + expect(resultNames).toContain('ResultType'); + + cleanup(); + }); + + it('should have Receiver field for constructors', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserData = { + id: string; + name: string; + }; + + export class UserService { + private data: UserData; + + constructor(userData: UserData) { + this.data = userData; + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + // Find the constructor + const constructorFunc = Object.values(functions).find(f => f.Name.includes('constructor')); + expect(constructorFunc).toBeDefined(); + + const ctor = expectToBeDefined(constructorFunc); + + // Should have Receiver field + expect(ctor.Receiver).toBeDefined(); + expect(ctor.Receiver?.Type.Name).toBe('UserService'); + expect(ctor.IsMethod).toBe(true); + + // Should extract UserData from parameters + expect(ctor.Params).toBeDefined(); + const params = expectToBeDefined(ctor.Params); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('UserData'); + + cleanup(); + }); + + it('should have Receiver field for interface methods', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type RequestType = { + url: string; + }; + + export type ResponseType = { + status: number; + }; + + export interface HttpClient { + request(req: RequestType): ResponseType; + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const requestMethod = expectToBeDefined(functions['HttpClient.request']); + + // Should have Receiver field + expect(requestMethod.Receiver).toBeDefined(); + expect(requestMethod.Receiver?.Type.Name).toBe('HttpClient'); + expect(requestMethod.IsInterfaceMethod).toBe(true); + + // Should extract RequestType from parameters + const params = expectToBeDefined(requestMethod.Params); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('RequestType'); + + // Should extract ResponseType from return type + const results = expectToBeDefined(requestMethod.Results); + const resultNames = results.map(r => r.Name); + expect(resultNames).toContain('ResponseType'); + + cleanup(); + }); + + it('should extract type dependencies from interface methods', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type RequestType = { + url: string; + }; + + export type ResponseType = { + status: number; + }; + + export interface HttpClient { + request(req: RequestType): ResponseType; + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const requestMethod = Object.values(functions).find(f => f.Name.includes('request')); + expect(requestMethod).toBeDefined(); + + const method = expectToBeDefined(requestMethod); + + // Should extract RequestType from parameters + const params = expectToBeDefined(method.Params); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('RequestType'); + + // Should extract ResponseType from return type + const results = expectToBeDefined(method.Results); + const resultNames = results.map(r => r.Name); + expect(resultNames).toContain('ResponseType'); + + cleanup(); + }); + + it('should extract type dependencies from arrow functions', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type InputType = string; + export type OutputType = number; + + export const convert = (input: InputType): OutputType => { + return parseInt(input); + }; + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const convert = expectToBeDefined(functions['convert']); + + // Should extract InputType from parameters + const params = expectToBeDefined(convert.Params); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('InputType'); + + // Should extract OutputType from return type + const results = expectToBeDefined(convert.Results); + const resultNames = results.map(r => r.Name); + expect(resultNames).toContain('OutputType'); + + cleanup(); + }); + }); + + describe('getter and setter support', () => { + it('should parse getters with type dependencies', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserData = { + id: string; + name: string; + }; + + export class UserService { + private data: UserData; + + constructor(userData: UserData) { + this.data = userData; + } + + get userData(): UserData { + return this.data; + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + // Should parse the getter + const getter = expectToBeDefined(functions['UserService.userData']); + expect(getter.IsMethod).toBe(true); + expect(getter.Receiver).toBeDefined(); + expect(getter.Receiver?.Type.Name).toBe('UserService'); + + // Should extract return type + expect(getter.Results).toBeDefined(); + const results = expectToBeDefined(getter.Results); + const resultNames = results.map(r => r.Name); + expect(resultNames).toContain('UserData'); + + // Should extract global variable reference + expect(getter.GlobalVars).toBeDefined(); + + cleanup(); + }); + + it('should parse setters with type dependencies', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserData = { + id: string; + name: string; + }; + + export class UserService { + private data: UserData; + + set userData(value: UserData) { + this.data = value; + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + // Should parse the setter + const setter = expectToBeDefined(functions['UserService.userData']); + expect(setter.IsMethod).toBe(true); + expect(setter.Receiver).toBeDefined(); + expect(setter.Receiver?.Type.Name).toBe('UserService'); + + // Should extract parameter type + expect(setter.Params).toBeDefined(); + const params = expectToBeDefined(setter.Params); + const paramNames = params.map(p => p.Name); + expect(paramNames).toContain('UserData'); + + cleanup(); + }); + + it('should parse getters with function calls', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export function validateData(data: string): boolean { + return data.length > 0; + } + + export class DataService { + private _data: string = ''; + + get isValid(): boolean { + return validateData(this._data); + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const getter = expectToBeDefined(functions['DataService.isValid']); + + // Should extract function call + expect(getter.FunctionCalls).toBeDefined(); + const functionCalls = expectToBeDefined(getter.FunctionCalls); + const callNames = functionCalls.map(c => c.Name); + expect(callNames).toContain('validateData'); + + cleanup(); + }); + + it('should parse setters with method calls', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export class Logger { + log(message: string): void { + console.log(message); + } + } + + export class DataService { + private logger: Logger; + private _data: string = ''; + + constructor() { + this.logger = new Logger(); + } + + set data(value: string) { + this._data = value; + this.logger.log('Data updated'); + } + } + `); + + const parser = new FunctionParser(project, process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const functions = parser.parseFunctions(sourceFile, 'parser-tests', pkgPath); + + const setter = expectToBeDefined(functions['DataService.data']); + + // Should extract method call + expect(setter.MethodCalls).toBeDefined(); + const methodCalls = expectToBeDefined(setter.MethodCalls); + const callNames = methodCalls.map(c => c.Name); + expect(callNames).toContain('Logger.log'); + + cleanup(); + }); }); }); \ No newline at end of file diff --git a/ts-parser/src/parser/test/TypeParser.test.ts b/ts-parser/src/parser/test/TypeParser.test.ts index 7ea0476..ff1aacc 100644 --- a/ts-parser/src/parser/test/TypeParser.test.ts +++ b/ts-parser/src/parser/test/TypeParser.test.ts @@ -704,5 +704,423 @@ describe('TypeParser', () => { cleanup(); }); + + it('should parse class constructors and static methods in Methods field', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export class TestClass { + private value: number; + + constructor(initialValue: number) { + this.value = initialValue; + } + + // Instance method + getValue(): number { + return this.value; + } + + // Static method + static createDefault(): TestClass { + return new TestClass(0); + } + + // Another static method + static fromString(str: string): TestClass { + return new TestClass(parseInt(str, 10)); + } + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const testClass = expectToBeDefined(types['TestClass']); + expect(testClass.Methods).toBeDefined(); + + const methods = expectToBeDefined(testClass.Methods); + + // Should include instance methods + expect(methods['getValue']).toBeDefined(); + expect(methods['getValue'].Name).toBe('TestClass.getValue'); + + // Should include constructor + expect(methods['constructor']).toBeDefined(); + expect(methods['constructor'].Name).toBe('TestClass.constructor'); + + // Should include static methods + expect(methods['createDefault']).toBeDefined(); + expect(methods['createDefault'].Name).toBe('TestClass.createDefault'); + + expect(methods['fromString']).toBeDefined(); + expect(methods['fromString'].Name).toBe('TestClass.fromString'); + + cleanup(); + }); + + it('should parse class expression with constructors and static methods', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export const ClassExpr = class MyClassExpr { + private name: string; + + constructor(name: string) { + this.name = name; + } + + getName(): string { + return this.name; + } + + static create(name: string): MyClassExpr { + return new MyClassExpr(name); + } + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const classExpr = expectToBeDefined(types['MyClassExpr']); + expect(classExpr.Methods).toBeDefined(); + + const methods = expectToBeDefined(classExpr.Methods); + + // Should include instance methods + expect(methods['getName']).toBeDefined(); + + // Should include constructor + expect(methods['constructor']).toBeDefined(); + + // Should include static methods + expect(methods['create']).toBeDefined(); + + cleanup(); + }); + }); + + describe('property type dependencies', () => { + it('should extract property type dependencies from classes', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserRole = 'admin' | 'user'; + + export type UserSettings = { + theme: string; + }; + + export class User { + role: UserRole; + settings: UserSettings; + active: boolean; + + constructor() { + this.active = true; + } + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const userClass = expectToBeDefined(types['User']); + + // Should have property type dependencies in SubStruct + expect(userClass.SubStruct).toBeDefined(); + const subStruct = expectToBeDefined(userClass.SubStruct); + const subStructNames = subStruct.map(dep => dep.Name); + + expect(subStructNames).toContain('UserRole'); + expect(subStructNames).toContain('UserSettings'); + + cleanup(); + }); + + it('should extract property type dependencies from interfaces', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Address = { + street: string; + city: string; + }; + + export type PhoneNumber = string; + + export interface Contact { + address: Address; + phone: PhoneNumber; + email: string; + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const contactInterface = expectToBeDefined(types['Contact']); + + // Should have property type dependencies in SubStruct + expect(contactInterface.SubStruct).toBeDefined(); + const subStruct = expectToBeDefined(contactInterface.SubStruct); + const subStructNames = subStruct.map(dep => dep.Name); + + expect(subStructNames).toContain('Address'); + expect(subStructNames).toContain('PhoneNumber'); + + cleanup(); + }); + + it('should extract property type dependencies from class expressions', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type ConfigType = { + timeout: number; + }; + + export const MyClass = class { + config: ConfigType; + + constructor() { + this.config = { timeout: 5000 }; + } + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // Find the class expression (it may have a generated name) + const classType = Object.values(types).find(t => t.TypeKind === 'struct'); + expect(classType).toBeDefined(); + + const myClass = expectToBeDefined(classType); + + // Should have property type dependencies in SubStruct + expect(myClass.SubStruct).toBeDefined(); + const subStruct = expectToBeDefined(myClass.SubStruct); + const subStructNames = subStruct.map(dep => dep.Name); + + expect(subStructNames).toContain('ConfigType'); + + cleanup(); + }); + }); + + describe('getter and setter support in Methods field', () => { + it('should parse getters in class Methods field', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserData = { + id: string; + name: string; + }; + + export class UserService { + private data: UserData; + + constructor(userData: UserData) { + this.data = userData; + } + + get userData(): UserData { + return this.data; + } + + get userId(): string { + return this.data.id; + } + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const userService = expectToBeDefined(types['UserService']); + expect(userService.Methods).toBeDefined(); + + const methods = expectToBeDefined(userService.Methods); + + // Should include getters + expect(methods['userData']).toBeDefined(); + expect(methods['userData'].Name).toBe('UserService.userData'); + expect(methods['userId']).toBeDefined(); + expect(methods['userId'].Name).toBe('UserService.userId'); + + cleanup(); + }); + + it('should parse setters in class Methods field', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type UserData = { + id: string; + name: string; + }; + + export class UserService { + private data: UserData; + + set userData(value: UserData) { + this.data = value; + } + + set userId(value: string) { + this.data.id = value; + } + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const userService = expectToBeDefined(types['UserService']); + expect(userService.Methods).toBeDefined(); + + const methods = expectToBeDefined(userService.Methods); + + // Should include setters + expect(methods['userData']).toBeDefined(); + expect(methods['userData'].Name).toBe('UserService.userData'); + expect(methods['userId']).toBeDefined(); + expect(methods['userId'].Name).toBe('UserService.userId'); + + cleanup(); + }); + + it('should parse both getter and setter with same name', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export class Counter { + private _count: number = 0; + + get count(): number { + return this._count; + } + + set count(value: number) { + this._count = value; + } + } + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + const counter = expectToBeDefined(types['Counter']); + expect(counter.Methods).toBeDefined(); + + const methods = expectToBeDefined(counter.Methods); + + // Should include the count accessor (getter/setter share the same name) + expect(methods['count']).toBeDefined(); + expect(methods['count'].Name).toBe('Counter.count'); + + cleanup(); + }); + + it('should parse getters in class expressions', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Config = { + timeout: number; + }; + + export const ConfigService = class { + private _config: Config; + + constructor() { + this._config = { timeout: 5000 }; + } + + get config(): Config { + return this._config; + } + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // Find the class expression (it will have a name like '__class' or contain 'AnonymousClass') + const configServiceClass = Object.values(types).find(t => + t.TypeKind === 'struct' && t.Methods && 'config' in t.Methods + ); + expect(configServiceClass).toBeDefined(); + + const classType = expectToBeDefined(configServiceClass); + expect(classType.Methods).toBeDefined(); + + const methods = expectToBeDefined(classType.Methods); + + // Should include getter + expect(methods['config']).toBeDefined(); + + cleanup(); + }); + + it('should parse setters in class expressions', () => { + const { project, sourceFile, cleanup } = createTestProject(` + export type Config = { + timeout: number; + }; + + export const ConfigService = class { + private _config: Config; + + set config(value: Config) { + this._config = value; + } + }; + `); + + const parser = new TypeParser(process.cwd()); + let pkgPathAbsFile: string = sourceFile.getFilePath(); + pkgPathAbsFile = pkgPathAbsFile.split('/').slice(0, -1).join('/'); + const pkgPath = path.relative(process.cwd(), pkgPathAbsFile); + + const types = parser.parseTypes(sourceFile, 'parser-tests', pkgPath); + + // Find the class expression (it will have a name like '__class' or contain 'AnonymousClass') + const configServiceClass = Object.values(types).find(t => + t.TypeKind === 'struct' && t.Methods && 'config' in t.Methods + ); + expect(configServiceClass).toBeDefined(); + + const classType = expectToBeDefined(configServiceClass); + expect(classType.Methods).toBeDefined(); + + const methods = expectToBeDefined(classType.Methods); + + // Should include setter + expect(methods['config']).toBeDefined(); + + cleanup(); + }); }); }); \ No newline at end of file diff --git a/ts-parser/src/utils/symbol-resolver.ts b/ts-parser/src/utils/symbol-resolver.ts index b14224b..792589e 100644 --- a/ts-parser/src/utils/symbol-resolver.ts +++ b/ts-parser/src/utils/symbol-resolver.ts @@ -347,12 +347,18 @@ export function assignSymbolName(symbol: Symbol): string { // Handle methods, properties, constructors, and functions with proper naming // Handle class/interface members with parent prefix - if(Node.isMethodDeclaration(firstDecl) || Node.isMethodSignature(firstDecl) || + if(Node.isMethodDeclaration(firstDecl) || Node.isMethodSignature(firstDecl) || Node.isPropertyDeclaration(firstDecl) || Node.isPropertySignature(firstDecl) || - Node.isConstructorDeclaration(firstDecl)) { + Node.isConstructorDeclaration(firstDecl) || Node.isGetAccessorDeclaration(firstDecl) || Node.isSetAccessorDeclaration(firstDecl)) { const parent = firstDecl.getParent(); - if(Node.isClassDeclaration(parent) || Node.isInterfaceDeclaration(parent)) { - const parentName = parent.getName() || 'AnonymousClass'; + if(Node.isClassDeclaration(parent) || Node.isInterfaceDeclaration(parent) || Node.isClassExpression(parent)) { + const parentSym = parent.getSymbol(); + let parentName = 'AnonymousClass'; + if(parentSym) { + parentName = assignSymbolName(parentSym); + } else if (Node.isClassDeclaration(parent) || Node.isInterfaceDeclaration(parent)) { + parentName = parent.getName() || 'AnonymousClass'; + } rawName = parentName + "." + rawName } } diff --git a/ts-parser/test-repo/src/middleware/Test.ts b/ts-parser/test-repo/src/middleware/Test.ts new file mode 100644 index 0000000..ed95fda --- /dev/null +++ b/ts-parser/test-repo/src/middleware/Test.ts @@ -0,0 +1,3 @@ +export function TestMiddleware() { + console.log('Test middleware'); +} \ No newline at end of file diff --git a/ts-parser/test-repo/src/middleware/Test2.ts b/ts-parser/test-repo/src/middleware/Test2.ts new file mode 100644 index 0000000..cec2d70 --- /dev/null +++ b/ts-parser/test-repo/src/middleware/Test2.ts @@ -0,0 +1,7 @@ +import {TestMiddleware} from "./Test"; + +export default class TestMiddleware2 { + constructor() { + TestMiddleware() + } +} \ No newline at end of file diff --git a/ts-parser/test-repo/src/test-class-method-deps.ts b/ts-parser/test-repo/src/test-class-method-deps.ts new file mode 100644 index 0000000..ef82406 --- /dev/null +++ b/ts-parser/test-repo/src/test-class-method-deps.ts @@ -0,0 +1,31 @@ +export type UserData = { + id: string; + name: string; +}; + +export function validateUser(user: UserData): boolean { + return user.id.length > 0; +} + +export class UserService { + private data: UserData; + + constructor(userData: UserData) { + this.data = userData; + } + + // 方法中应该能识别: + // 1. 参数类型 UserData + // 2. 返回类型 boolean + // 3. 函数调用 validateUser + checkValid(user: UserData): boolean { + return validateUser(user); + } + + // 方法中应该能识别: + // 1. 返回类型 UserData + // 2. 全局变量引用 this.data + getUserData(): UserData { + return this.data; + } +}