Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Inheritance relationships no longer attach external Rust or npm supertypes to unrelated local symbols with the same name, including in Svelte, Vue and Astro components; re-index after upgrading to clear existing false relationships. Thanks @ctype-lab. (#1536)

- Spring's dependency-injection resolution patterns no longer capture inheritance references or references from other languages. In a polyglot repository where Spring is detected (a sibling Java module is enough), a Scala or Kotlin `extends`/`implements` reference could be resolved by Spring's directory-convention heuristics to an unrelated same-named class — for example a test fixture — corrupting the inheritance graph that `codegraph_impact` and `codegraph_explore` walk. Inheritance now always resolves through imports and name matching, and the Spring DI patterns only apply to Java/Kotlin references. Re-index after upgrading. (#1825)

- PHP static calls through imported class aliases now reach the correct class when services and repositories share method names, so callers and impact analysis show the right dependencies after re-indexing. (#1545)
- TypeScript/JavaScript: a call through a field of the enclosing class — `this.mailer.send()` — now resolves on the field's declared type, so a delegating wrapper that shares the method's name no longer records itself as its own callee and `callers`, `impact` and trace stop lying on that shape. A field whose type is external or a builtin stays unresolved rather than guessed. Re-index after upgrading. (#1496)
- TypeScript and JavaScript collection calls through local variables and their nested properties no longer link to unrelated project methods; re-index after upgrading. (#1566)
Expand Down
71 changes: 71 additions & 0 deletions __tests__/frameworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,77 @@ class OwnerController {
});
});

describe('springResolver.resolve — DI heuristics are gated to Java/Kotlin non-inheritance refs', () => {
// A polyglot repo (Scala + a sibling Java module) detects Spring globally.
// The DI/convention patterns (bare-name Pattern 4 especially) must then not
// hijack a Scala `extends X` to a same-named class found via directory
// heuristics — inheritance must resolve through imports/name matching.
const decoyNode: Node = {
id: 'class:src/test/model/ExtCustomer.java:ExtCustomer:3',
kind: 'class',
name: 'ExtCustomer',
qualifiedName: 'src/test/model/ExtCustomer.java::ExtCustomer',
filePath: 'src/test/model/ExtCustomer.java',
language: 'java',
startLine: 3,
endLine: 10,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
};
const context = {
getNodesInFile: () => [],
getNodesByName: (n: string) => (n === 'ExtCustomer' ? [decoyNode] : []),
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => false,
readFile: () => null,
getProjectRoot: () => '/test',
getAllFiles: () => [],
getNodesByLowerName: () => [],
getImportMappings: () => [],
};
const baseRef = {
fromNodeId: 'class:A.scala:MExtCustomer:5',
referenceName: 'ExtCustomer',
line: 5,
column: 10,
filePath: 'A.scala',
};

it('does NOT resolve a Scala extends reference (Pattern 4 bare-name fallback)', () => {
const ref = { ...baseRef, referenceKind: 'extends' as const, language: 'scala' as const };
expect(springResolver.resolve(ref, context as any)).toBeNull();
});

it('does NOT resolve a non-Java/Kotlin plain reference either', () => {
const ref = { ...baseRef, referenceKind: 'references' as const, language: 'scala' as const };
expect(springResolver.resolve(ref, context as any)).toBeNull();
});

it('does NOT resolve a Java extends reference — inheritance is never a DI pattern', () => {
const ref = {
...baseRef,
filePath: 'B.java',
referenceKind: 'extends' as const,
language: 'java' as const,
};
expect(springResolver.resolve(ref, context as any)).toBeNull();
});

it('still resolves a Java DI reference through the entity pattern', () => {
const ref = {
...baseRef,
filePath: 'B.java',
referenceKind: 'references' as const,
language: 'java' as const,
};
const result = springResolver.resolve(ref, context as any);
expect(result?.targetNodeId).toBe(decoyNode.id);
expect(result?.resolvedBy).toBe('framework');
});
});

import { playResolver } from '../src/resolution/frameworks/play';
import { isSourceFile, isPlayRoutesFile } from '../src/extraction/grammars';

Expand Down
9 changes: 9 additions & 0 deletions src/resolution/frameworks/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ export const springResolver: FrameworkResolver = {
}
}

// The DI/convention patterns below (1–5) are Spring-specific heuristics:
// they must only fire on Java/Kotlin refs, and never on inheritance refs.
// Without these gates a Scala `extends ExtCustomer` (Spring detected via a
// sibling Java module) was hijacked by Pattern 4's bare-name fallback to an
// unrelated same-named test class — `extends`/`implements` must resolve via
// imports/name matching, not directory heuristics.
if (ref.language !== 'java' && ref.language !== 'kotlin') return null;
if (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') return null;

// Pattern 1: Service references (dependency injection)
if (ref.referenceName.endsWith('Service')) {
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
Expand Down