Vendored WhatsApp/tree-sitter-erlang 0.19 (the ELP grammar, ABI 14) with an Erlang-shaped extractor: multi-clause/multi-arity functions merged into one symbol, -spec signatures, records with fields, -type/-opaque aliases, -define macros, -include/-include_lib file edges, and -export-driven visibility. Modules wrap in a namespace so remote mod:fn(...) calls resolve through the existing qualified-name matcher as mod::fn with zero resolver changes. -behaviour declarations link to the behaviour module — gated to namespace targets only (bare-name fallthrough linked -behaviour(supervisor) to an unrelated macro constant on emqx). OTP indirection with static targets is followed: spawn/apply/proc_lib/timer/rpc MFA-argument callees, and gen_server:call/cast(?MODULE | ?SERVER) to the module's own handle_call/handle_cast. Var-module dispatch and message sends stay deliberately unlinked. codegraph_explore also normalizes Erlang-native query spelling (mod:fn/3, init/2) so named symbols resolve as typed. Benchmarked on cowboy (189 files), ejabberd (414), emqx (2,447): extraction PASS on all three; with-codegraph arms reached 2/2/0 file Reads vs 10/5+/19 without, fastest on the largest repo. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
63e1b5a23a
commit
6511722250
@@ -83,6 +83,67 @@ describe('Resolution Module', () => {
|
||||
expect(result?.resolvedBy).toBe('exact-match');
|
||||
});
|
||||
|
||||
it('should resolve Erlang -behaviour refs only to module namespaces', () => {
|
||||
// On emqx, `-behaviour(supervisor)` (OTP behaviour, not in the repo)
|
||||
// fell through to bare-name matching and resolved to a
|
||||
// `-define(supervisor, ...)` macro constant in an unrelated app.
|
||||
const macroConstant: Node = {
|
||||
id: 'constant:apps/bridge/src/impl.erl:supervisor:61',
|
||||
kind: 'constant',
|
||||
name: 'supervisor',
|
||||
qualifiedName: 'impl::supervisor',
|
||||
filePath: 'apps/bridge/src/impl.erl',
|
||||
language: 'erlang',
|
||||
startLine: 61,
|
||||
endLine: 61,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const behaviourModule: Node = {
|
||||
id: 'namespace:src/my_behaviour.erl:my_behaviour:1',
|
||||
kind: 'namespace',
|
||||
name: 'my_behaviour',
|
||||
qualifiedName: 'my_behaviour',
|
||||
filePath: 'src/my_behaviour.erl',
|
||||
language: 'erlang',
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const nodes = [macroConstant, behaviourModule];
|
||||
const context: ResolutionContext = {
|
||||
getNodesInFile: () => [],
|
||||
getNodesByName: (name) => nodes.filter((n) => n.name === name),
|
||||
getNodesByQualifiedName: () => [],
|
||||
getNodesByKind: () => [],
|
||||
fileExists: () => false,
|
||||
readFile: () => null,
|
||||
getProjectRoot: () => '/test',
|
||||
getAllFiles: () => [],
|
||||
getNodesByLowerName: () => [],
|
||||
getImportMappings: () => [],
|
||||
};
|
||||
const mkRef = (name: string) => ({
|
||||
fromNodeId: 'namespace:src/worker.erl:worker:1',
|
||||
referenceName: name,
|
||||
referenceKind: 'implements' as const,
|
||||
line: 2,
|
||||
column: 0,
|
||||
filePath: 'src/worker.erl',
|
||||
language: 'erlang' as const,
|
||||
});
|
||||
|
||||
// Out-of-repo behaviour whose name collides with a macro constant:
|
||||
// stays unresolved instead of linking the constant.
|
||||
expect(matchReference(mkRef('supervisor'), context)).toBeNull();
|
||||
// In-repo behaviour module resolves to its namespace.
|
||||
const resolved = matchReference(mkRef('my_behaviour'), context);
|
||||
expect(resolved?.targetNodeId).toBe(behaviourModule.id);
|
||||
});
|
||||
|
||||
it('should prefer same-module candidates over cross-module matches', () => {
|
||||
// Simulates a Python monorepo where multiple apps define navigate()
|
||||
const candidateA: Node = {
|
||||
|
||||
Reference in New Issue
Block a user