fix(erlang): a comment in an argument list no longer counts as an argument

Since PR #1615 made arity part of an Erlang function's identity, arity
is counted from the argument list's named children. tree-sitter reports
a comment as a named child, so a comment at the top level of a
parameter or argument list raised the count by one. A call written
plain( % note followed by two arguments was looked up as plain/3 and
linked to nothing, a definition with a comment between its parameters
was indexed one arity too high, and the MFA list of spawn(M, f, [A, B])
with a comment between A and B gave f/3. A comment nested deeper,
inside a tuple, a record or a list argument, was never affected.

The three counts now go through one namedArgCount helper that leaves
comments out: a clause's and a spec's arity, a call site's arity, and
the MFA list's.

Conflict resolution: the import block of tree-sitter.ts moved on main
since the PR; main's imports are kept, with namedArgCount added.

Validation: the new test fails on main (its fixture gets no call edge
at all) and passes here. A/B against main: rebar3 97e9367 gains 2 edges
(rebar_compiler.erl:403 and rebar_release_SUITE.erl:247, both calls
whose argument lists hold a comment, both checked against the source),
0 removed, nodes unchanged; cowboy 6475600 and ranch 7ec436d are
byte-identical, as are the controls express and flask. Erlang has no
kernel extractor, so there is no parity to run. Index CPU on rebar3,
median of 3 interleaved: 9.9 s on main, 9.3 s here.

Original PR #1619 by @Dshuishui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dshuishui
2026-10-23 19:53:00 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 90c5dc6fc9
commit 44adef821b
5 changed files with 58 additions and 8 deletions
+8 -4
View File
@@ -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,
@@ -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'));
}
/**
+13
View File
@@ -130,6 +130,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
*
+3 -4
View File
@@ -16,7 +16,7 @@ import {
UnresolvedReference,
} from '../types';
import { getParser, detectLanguage, isLanguageSupported, isFileLevelOnlyLanguage } from './grammars';
import { NodeIdAllocator, getNodeText, getChildByField, getPrecedingDocstring, BUILTIN_TYPE_NAMES } from './tree-sitter-helpers';
import { NodeIdAllocator, getNodeText, getChildByField, getPrecedingDocstring, BUILTIN_TYPE_NAMES, namedArgCount } from './tree-sitter-helpers';
import { FN_REF_SPECS, captureFnRefCandidates, goScopedFnRefScopes, type FnRefSpec, type FnRefCandidate } from './function-ref';
import { isGeneratedFile, isMinifiedContent } from './generated-detection';
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
@@ -5405,8 +5405,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}`,
@@ -5473,7 +5472,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,