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
+1
View File
@@ -67,6 +67,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph upgrade` now upgrades a global npm install when run from your home folder, which holds npm's global folder on Windows and with tools like nvm: before, it ran `npm install` without `-g` there, which left the global copy on the old version and put a second copy in your home folder. A copy installed in a project is now upgraded in that project wherever you run the command, with a warning if npm left it on the old version, and a copy installed with pnpm, yarn or bun gets the command that upgrades it instead of npm. Thanks @timlincool for the report and @maxmilian for the first fix. (#2477)
- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
- In CFML, a `<cfquery>` or `<cfscript>` block the parser can't read no longer marks its whole file as contributing nothing to the graph: `codegraph init` no longer warns about the file, and `codegraph status` no longer lists it among files whose symbols are missing, since its component, functions and calls are all indexed. The file's recorded errors now name the block and its line instead. Thanks @HarryMuc for the report. (#2654)
- In Erlang, a comment inside an argument or parameter list, like the one in `add_to_includes( % give access to .hrl in app's src/` before its two arguments, no longer counts as an argument. The call now links to the function of its real arity, and a function whose parameter list holds a comment keeps its own arity. Before, the comment raised the arity by one, so the call linked to nothing. Re-index Erlang projects after upgrading. Thanks @Dshuishui. (#1619)
- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `<script setup>` or of a TypeScript or JavaScript file (`const { t } = useI18n()`) now link to the function they call, so `callers` and impact for a composable or helper used this way are no longer mostly empty. A `v-for` item or slot prop is never taken for a project function, a handler like `@click="open = false"` no longer links to an unrelated function of the same name, and a component's own function named like a Nuxt helper, such as `clearError`, is now the one its calls reach. Thanks @deniskern for the report, and @drakeo338 and @L0garithmic. (#2340)
+33
View File
@@ -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(
+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,