Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions libs/openant-core/parsers/javascript/dependency_resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class DependencyResolver {
// Skip 'this' (handled above) and common built-ins
if (objectName === 'this' || this._isBuiltIn(objectName)) continue;

const resolved = this._resolveMethodCall(objectName, methodName, callerFile);
const resolved = this._resolveMethodCall(objectName, methodName, callerFile, callerFuncId);
if (resolved && !seenCalls.has(resolved)) {
seenCalls.add(resolved);
calls.push(resolved);
Expand Down Expand Up @@ -240,23 +240,55 @@ class DependencyResolver {

/**
* Resolve an object.method call
*
* Supports two resolution strategies:
* 1. Direct class name match: objectName === className
* 2. DI-aware resolution: objectName is a constructor-injected parameter,
* use its type annotation to find the target class
*/
_resolveMethodCall(objectName, methodName, callerFile) {
// Check if objectName matches a class name
const qualifiedName = `${objectName}.${methodName}`;
_resolveMethodCall(objectName, methodName, callerFile, callerFuncId = null) {
const candidates = this.functionsByName[methodName];

if (!candidates || !Array.isArray(candidates)) {
return null;
}

// 1. Exact class name match (existing behavior)
for (const funcId of candidates) {
const funcData = this.functions[funcId];
if (funcData && funcData.className === objectName) {
return funcId;
}
}

// 2. DI-aware resolution: look up objectName in caller's constructorDeps
// e.g., this.callService.getById() -> constructorDeps says callService: CallService
// -> resolve to CallService.getById
if (callerFuncId) {
const callerFunc = this.functions[callerFuncId];
if (callerFunc && callerFunc.constructorDeps) {
const typeName = callerFunc.constructorDeps[objectName];
if (typeName) {
// 2a. Exact type match
for (const funcId of candidates) {
const funcData = this.functions[funcId];
if (funcData && funcData.className === typeName) {
return funcId;
}
}

// 2b. Implementation class match: type is often an interface/abstract class
// and the implementation has a suffix (e.g., CallService -> CallServiceV1, CallServiceImpl)
for (const funcId of candidates) {
const funcData = this.functions[funcId];
if (funcData && funcData.className && funcData.className.startsWith(typeName)) {
return funcId;
}
}
}
}
}

return null;
}

Expand Down
32 changes: 32 additions & 0 deletions libs/openant-core/parsers/javascript/typescript_analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,38 @@ class TypeScriptAnalyzer {
className: className,
};
}

// Extract constructor DI metadata for this class
// In NestJS/Angular, constructor parameters with type annotations
// declare injected services: constructor(private callService: CallService)
const constructors = classDecl.getConstructors();
if (constructors.length > 0) {
const ctor = constructors[0];
const injections = {}; // paramName -> typeName

for (const param of ctor.getParameters()) {
const paramName = param.getName();
const typeNode = param.getTypeNode();
if (typeNode) {
const typeName = typeNode.getText();
// Only store simple PascalCase type names (skip union types, generics, primitives)
if (/^[A-Z][a-zA-Z0-9_$]*$/.test(typeName)) {
injections[paramName] = typeName;
}
}
}

if (Object.keys(injections).length > 0) {
// Store DI metadata on each method of this class
for (const method of classDecl.getMethods()) {
const methodName = method.getName();
const functionId = `${relativePath}:${className}.${methodName}`;
if (this.functions[functionId]) {
this.functions[functionId].constructorDeps = injections;
}
}
}
}
}

// Extract methods from object literals in export default
Expand Down
Loading
Loading