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)

- Scala inheritance is no longer cut short by the companion-object idiom. A Scala `object` sharing its name with a trait or class (the ubiquitous trait + companion pattern) could win the resolution of `extends`/`with` references, detaching every subtype from the inheritance chain — so `codegraph_impact` on a widely-used trait stopped at depth 1. Scala `object` definitions are now indexed as modules and inheritance references prefer the actual type, so impact analysis follows the full subtype tree. Re-index after upgrading. (#1824)

- 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
6 changes: 3 additions & 3 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4409,7 +4409,7 @@ class APXCharacter { // the one real definition

const scala = extractFromSource('M.scala', 'trait Marker\ncase object Red\nclass Foo\n');
const scalaNames = scala.nodes
.filter((n) => ['class', 'trait', 'interface'].includes(n.kind))
.filter((n) => ['class', 'trait', 'interface', 'module'].includes(n.kind))
.map((n) => n.name);
expect(scalaNames).toEqual(expect.arrayContaining(['Marker', 'Red', 'Foo']));
});
Expand Down Expand Up @@ -8296,14 +8296,14 @@ class UserService(private val repo: UserRepository) {
expect(cls?.language).toBe('scala');
});

it('should extract object definitions as class kind', () => {
it('should extract object definitions as module kind', () => {
const code = `
object DatabaseConfig {
val url = "jdbc:postgresql://localhost/mydb"
}
`;
const result = extractFromSource('Config.scala', code);
const obj = result.nodes.find((n) => n.kind === 'class' && n.name === 'DatabaseConfig');
const obj = result.nodes.find((n) => n.kind === 'module' && n.name === 'DatabaseConfig');
expect(obj).toBeDefined();
});

Expand Down
45 changes: 45 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4860,6 +4860,51 @@ object Main {
});
});

describe('Scala companion object vs extends resolution', () => {
it('resolves `extends X` to the trait, not the same-named companion object', async () => {
// The trait+companion idiom: both nodes share name AND file. Without a
// kind-aware tie-breaker the winner is arbitrary, and when the companion
// wins, every subtype is detached from the trait's inheritance chain.
fs.writeFileSync(
path.join(tempDir, 'ExtAgreement.scala'),
`trait ExtAgreement {
def extId: String = "x"
}
object ExtAgreement {
val Kind = "agreement"
}
`
);
fs.writeFileSync(
path.join(tempDir, 'MExtAgreement.scala'),
`class MExtAgreement extends ExtAgreement {
def render(): String = extId
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });

const traitNode = cg.getNodesByKind('trait').find((n) => n.name === 'ExtAgreement');
const moduleNode = cg.getNodesByKind('module').find((n) => n.name === 'ExtAgreement');
expect(traitNode).toBeDefined();
expect(moduleNode).toBeDefined();

const traitExtends = cg
.getIncomingEdges(traitNode!.id)
.filter((e) => e.kind === 'extends');
const moduleExtends = cg
.getIncomingEdges(moduleNode!.id)
.filter((e) => e.kind === 'extends');
expect(traitExtends.length).toBe(1);
expect(moduleExtends.length).toBe(0);

// Impact must now traverse THROUGH the trait to the subtype.
const impact = cg.getImpactRadius(traitNode!.id, 5);
const impactNames = [...impact.nodes.values()].map((n) => n.name);
expect(impactNames).toContain('MExtAgreement');
});
});

describe('Dart chained static-factory / factory-constructor call resolution (#645/#608 mechanism)', () => {
function callerNamesOf(qualifiedName: string): string[] {
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
Expand Down
5 changes: 5 additions & 0 deletions src/extraction/languages/scala.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export const scalaExtractor: LanguageExtractor = {

classifyClassNode: (node: SyntaxNode) => {
if (node.type === 'trait_definition') return 'trait';
// A Scala `object` is a singleton (the companion-object idiom), not a
// type: `extends X` can never target it. Classifying it as `module`
// keeps it distinguishable from the same-named trait/class so the
// resolver can prefer the type node for extends/implements refs.
if (node.type === 'object_definition') return 'module';
return 'class';
},

Expand Down
2 changes: 1 addition & 1 deletion src/extraction/tree-sitter-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export interface LanguageExtractor {
* Classify a class_declaration node when the grammar reuses one node type
* for multiple concepts (e.g. Swift uses class_declaration for classes, structs, and enums).
*/
classifyClassNode?: (node: SyntaxNode) => 'class' | 'struct' | 'enum' | 'interface' | 'trait';
classifyClassNode?: (node: SyntaxNode) => 'class' | 'struct' | 'enum' | 'interface' | 'trait' | 'module';

/**
* Classify a methodTypes node when the grammar reuses one node type for
Expand Down
3 changes: 3 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,8 @@ export class TreeSitterExtractor {
this.extractInterface(node);
} else if (classification === 'trait') {
this.extractClass(node, 'trait');
} else if (classification === 'module') {
this.extractClass(node, 'module');
} else {
this.extractClass(node);
}
Expand Down Expand Up @@ -5750,6 +5752,7 @@ export class TreeSitterExtractor {
else if (classification === 'enum') this.extractEnum(node);
else if (classification === 'interface') this.extractInterface(node);
else if (classification === 'trait') this.extractClass(node, 'trait');
else if (classification === 'module') this.extractClass(node, 'module');
else this.extractClass(node);
return;
}
Expand Down
11 changes: 11 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,17 @@ function findBestMatch(
}
}

// For inheritance references (`extends X` / `implements X`), penalize
// `module` candidates — a Scala companion `object` shares its name (and
// file) with the trait/class it accompanies, but `extends` can never
// target a singleton. `module` stays in SUPERTYPE_TARGET_KINDS (Ruby
// `include`, TS namespaces), so it is still eligible; without this the
// trait and its companion tie and the winner is arbitrary, which detaches
// subtypes from the inheritance chain (impact analysis breaks).
if (isInheritanceRef(ref) && candidate.kind === 'module') {
score -= 50;
}

// For decorator references (`@Foo`), prefer functions. Class
// decorators (Python `@SomeClass`, Java annotation interfaces)
// also resolve here, hence the smaller class bonus.
Expand Down