escripts (.escript) index like any module — the ELP grammar has a
first-class shebang node, so no source transform is needed; main/1 and its
helpers get full function/call extraction.
OTP application resource files (<app>.app.src and compiled <app>.app) join
the graph as Erlang terms the grammar parses natively. They route by full
suffix (their last-dot extension, .src, is far too generic for the
extension map). The application tuple yields structure: {mod, {Mod, _}}
links the app to its callback module — the app's entry point — and
{applications, [...]} / {included_applications, [...]} connect umbrella
sibling apps, resolving through the OTP app-name == module-name convention;
kernel/stdlib and other out-of-repo apps stay unresolved.
App-file refs resolve only ever to MODULES: validation on emqx caught the
ssl OTP-app dependency resolving to a test helper FUNCTION named ssl (the
same defect class as the earlier -behaviour gate), so the matchReference
module-only gate now covers every ref an .app/.app.src file emits.
Validated on emqx: 2 app.src + 6 escripts indexed, entry-module and
umbrella-dependency edges all namespace-targeted post-gate, escript
functions extracted; a stray legacy/module.src stays unknown.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a5b8cd8e25
commit
a0208feaac
@@ -110,6 +110,14 @@ describe('Language Detection', () => {
|
||||
it('should detect Erlang files', () => {
|
||||
expect(detectLanguage('src/my_server.erl')).toBe('erlang');
|
||||
expect(detectLanguage('include/records.hrl')).toBe('erlang');
|
||||
expect(detectLanguage('bin/release_tool.escript')).toBe('erlang');
|
||||
// OTP app resource files route by full suffix — `.src` alone is too generic.
|
||||
expect(detectLanguage('src/myapp.app.src')).toBe('erlang');
|
||||
expect(detectLanguage('ebin/myapp.app')).toBe('erlang');
|
||||
expect(detectLanguage('legacy/module.src')).toBe('unknown');
|
||||
expect(isSourceFile('src/myapp.app.src')).toBe(true);
|
||||
expect(isSourceFile('ebin/myapp.app')).toBe(true);
|
||||
expect(isSourceFile('legacy/module.src')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return unknown for unsupported extensions', () => {
|
||||
@@ -9134,6 +9142,54 @@ second(X) -> X.
|
||||
});
|
||||
});
|
||||
|
||||
describe('escript and app resource files', () => {
|
||||
it('should extract functions and calls from an escript behind a shebang', () => {
|
||||
const code = `#!/usr/bin/env escript
|
||||
%%! -smp enable
|
||||
|
||||
main([Path]) ->
|
||||
Result = analyze(Path),
|
||||
io:format("~p~n", [Result]).
|
||||
|
||||
analyze(Path) ->
|
||||
{ok, Bin} = file:read_file(Path),
|
||||
byte_size(Bin).
|
||||
`;
|
||||
const result = extractFromSource('bin/tool.escript', code);
|
||||
const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
|
||||
expect(fns).toContain('main');
|
||||
expect(fns).toContain('analyze');
|
||||
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
|
||||
expect(calls).toContain('analyze');
|
||||
expect(calls).toContain('io::format');
|
||||
});
|
||||
|
||||
it('should link an app resource file to its callback module and dependency apps', () => {
|
||||
const code = `{application, sample, [
|
||||
{description, "Sample application"},
|
||||
{vsn, "1.0.0"},
|
||||
{registered, [sample_server]},
|
||||
{mod, {sample_app, []}},
|
||||
{applications, [kernel, stdlib, sample_core]},
|
||||
{included_applications, [sample_extra]},
|
||||
{env, [{limit, 100}]},
|
||||
{modules, []}
|
||||
]}.
|
||||
`;
|
||||
const result = extractFromSource('src/sample.app.src', code);
|
||||
const refs = result.unresolvedReferences.map((r) => `${r.referenceKind}:${r.referenceName}`);
|
||||
// The application-callback module is the app's entry point.
|
||||
expect(refs).toContain('references:sample_app');
|
||||
// Dependencies resolve to umbrella siblings; kernel/stdlib just drop.
|
||||
expect(refs).toContain('imports:kernel');
|
||||
expect(refs).toContain('imports:sample_core');
|
||||
expect(refs).toContain('imports:sample_extra');
|
||||
// Registered names, env values, and the like carry no graph structure.
|
||||
expect(refs.filter((r) => r.endsWith(':sample_server'))).toHaveLength(0);
|
||||
expect(refs.filter((r) => r.endsWith(':limit'))).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Macro linkage', () => {
|
||||
it('should attribute macro-body calls to the macro and link function-like uses into the chain', () => {
|
||||
const code = `-module(m).
|
||||
|
||||
@@ -142,6 +142,35 @@ describe('Resolution Module', () => {
|
||||
// In-repo behaviour module resolves to its namespace.
|
||||
const resolved = matchReference(mkRef('my_behaviour'), context);
|
||||
expect(resolved?.targetNodeId).toBe(behaviourModule.id);
|
||||
|
||||
// The same module-only rule covers refs emitted by .app/.app.src
|
||||
// resource files: on emqx, the `ssl` OTP app dependency resolved to a
|
||||
// test helper FUNCTION named ssl. A colliding non-module name stays
|
||||
// unresolved; a real umbrella-sibling module resolves.
|
||||
nodes.push({
|
||||
id: 'function:test/ldap_SUITE.erl:ssl:12',
|
||||
kind: 'function',
|
||||
name: 'ssl',
|
||||
qualifiedName: 'ldap_SUITE::ssl',
|
||||
filePath: 'test/ldap_SUITE.erl',
|
||||
language: 'erlang',
|
||||
startLine: 12,
|
||||
endLine: 14,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
const appRef = (name: string) => ({
|
||||
fromNodeId: 'file:src/myapp.app.src',
|
||||
referenceName: name,
|
||||
referenceKind: 'imports' as const,
|
||||
line: 6,
|
||||
column: 0,
|
||||
filePath: 'src/myapp.app.src',
|
||||
language: 'erlang' as const,
|
||||
});
|
||||
expect(matchReference(appRef('ssl'), context)).toBeNull();
|
||||
expect(matchReference(appRef('my_behaviour'), context)?.targetNodeId).toBe(behaviourModule.id);
|
||||
});
|
||||
|
||||
it('should prefer same-module candidates over cross-module matches', () => {
|
||||
|
||||
Reference in New Issue
Block a user