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
33 changes: 33 additions & 0 deletions __tests__/erlang-arity-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,39 @@ go(Args) ->
expect(edges.some((e) => e.sq === 'spawner::go/1' && e.tq.startsWith('multi::job'))).toBe(false);
});

it('counts arity past comments written inside a parameter or argument list', async () => {
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(dir, 'src', 'noted.erl'),
`-module(noted).
-export([run/0, plain/2, spaced/2]).

plain(X, Y) -> X + Y.

spaced(X, % why this one is special
Y) ->
X * Y.

run() ->
A = plain( % leading note
1, 2),
B = plain(1, % separating note
2),
C = spaced(1, 2),
D = erlang:spawn(noted, plain, [1, % note inside the MFA list
2]),
{A, B, C, D}.
`
);
const edges = await callEdges(dir);
// A comment is a NAMED child, but it is not an argument: every call below
// is arity 2, and `spaced` is defined with arity 2.
expect(edges).toContainEqual({ sq: 'noted::run/0', tq: 'noted::plain/2' });
expect(edges).toContainEqual({ sq: 'noted::run/0', tq: 'noted::spaced/2' });
// Nothing resolved to a phantom arity the comments would have produced.
expect(edges.some((e) => /::(plain|spaced)\/[^2]$/.test(e.tq))).toBe(false);
});

it('lands `fun mod:f/1` references on the written arity', async () => {
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
fs.writeFileSync(
Expand Down
12 changes: 8 additions & 4 deletions src/extraction/languages/erlang.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText, getChildByField, getPrecedingDocstring } from '../tree-sitter-helpers';
import {
getNodeText,
getChildByField,
getPrecedingDocstring,
namedArgCount,
} from '../tree-sitter-helpers';
import type { LanguageExtractor, ExtractorContext } from '../tree-sitter-types';

// Node names follow the vendored WhatsApp/tree-sitter-erlang grammar (0.19,
Expand Down Expand Up @@ -95,10 +100,9 @@ function moduleExports(node: SyntaxNode, source: string, filePath: string): Set<
return result;
}

/** Argument count of a clause/sig: the `args` (expr_args) field's named-child count. */
/** Argument count of a clause/sig: the `args` (expr_args) field's arguments. */
function nodeArity(withArgs: SyntaxNode): number {
const args = getChildByField(withArgs, 'args');
return args ? args.namedChildCount : 0;
return namedArgCount(getChildByField(withArgs, 'args'));
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/extraction/tree-sitter-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ function cleanCommentMarkers(comment: string): string {
.trim();
}

/**
* Number of ARGUMENTS in an `args` node — its named children minus comments.
*
* tree-sitter reports `comment` as a NAMED child, so a comment written at the
* top level of a parameter or argument list (`f(A, % why\n B)`) inflates a
* plain `namedChildCount`. Anywhere that count is an arity — and arity is part
* of an Erlang function's identity (#1610) — the comment must not be counted.
*/
export function namedArgCount(argsNode: SyntaxNode | null | undefined): number {
if (!argsNode) return 0;
return argsNode.namedChildren.filter((c) => c.type !== 'comment').length;
}

/**
* Get the docstring/comment preceding a node
*/
Expand Down
13 changes: 9 additions & 4 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
UnresolvedReference,
} from '../types';
import { getParser, detectLanguage, isLanguageSupported, isFileLevelOnlyLanguage } from './grammars';
import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers';
import {
generateNodeId,
getNodeText,
getChildByField,
getPrecedingDocstring,
namedArgCount,
} from './tree-sitter-helpers';
import { FN_REF_SPECS, captureFnRefCandidates, type FnRefSpec, type FnRefCandidate } from './function-ref';
import { isGeneratedFile } from './generated-detection';
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
Expand Down Expand Up @@ -3805,8 +3811,7 @@ export class TreeSitterExtractor {
}
// Arity from the call site's own argument list — part of the callee's
// identity, and what disambiguates `f/1` from `f/2` (#1610).
const callArgsNode = getChildByField(node, 'args');
const callArity = callArgsNode ? callArgsNode.namedChildCount : 0;
const callArity = namedArgCount(getChildByField(node, 'args'));
this.unresolvedReferences.push({
fromNodeId: callerId,
referenceName: `${calleeName}/${callArity}`,
Expand Down Expand Up @@ -3873,7 +3878,7 @@ export class TreeSitterExtractor {
// qualified matcher then resolves it only when the module
// defines exactly one arity of that name.
const mfaList = argExprs[i + 2];
const arityTail = mfaList?.type === 'list' ? `/${mfaList.namedChildCount}` : '';
const arityTail = mfaList?.type === 'list' ? `/${namedArgCount(mfaList)}` : '';
this.unresolvedReferences.push({
fromNodeId: callerId,
referenceName: (isLocalModule ? erlAtom(f) : `${erlAtom(m)}::${erlAtom(f)}`) + arityTail,
Expand Down