fix(cpp): a function is named by its declarator, never by its noexcept, throw or requires clause (#2727)

Both C++ extractors name a function by the first qualified identifier
found breadth-first in its declarator, which is how an out-of-line
`Foo::bar` is named. The walk skipped only the parameter list and the
trailing return type, so a qualified name in the clauses after them
became the name of a function whose own name is plain. nlohmann/json's
constructor `basic_json(CompatibleType&& val) noexcept(noexcept(
JSONSerializer<U>::to_json(…)))` was indexed as a method
`JSONSerializer::to_json` that doesn't exist, with no node for the
constructor; `throw(std::bad_alloc)` named a method `std::bad_alloc`,
and a trailing `requires std::integral<T>` a function `std::integral<T>`.

The walk now also skips `noexcept`, `throw_specifier` and
`requires_clause`, in the TS extractor and the kernel alike; the parity
fixture covers all three.

Measured on main 226ac930 with fresh indexes, and byte-identical again
after syncing onto e97012eb: json 42, abseil 28 and
protobuf 1 phantom nodes become the real functions at the same lines;
fmt, leveldb, rocksdb, redis and three controls in other languages are
byte-identical. Kernel parity sweeps on json, fmt, abseil, leveldb and
rocksdb find no file with diffs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 04:20:15 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e97012eb31
commit f66d064279
5 changed files with 172 additions and 6 deletions
+10 -3
View File
@@ -100,9 +100,11 @@ function findDeclaratorQualifiedId(declarator: SyntaxNode): SyntaxNode | undefin
if (current.type === 'qualified_identifier') return current;
for (let i = 0; i < current.namedChildCount; i++) {
const child = current.namedChild(i);
// Don't descend into parameters or the trailing return type — their types
// (`const std::string&`, `-> std::string`) aren't the function name.
if (child && child.type !== 'parameter_list' && child.type !== 'trailing_return_type') {
// Don't descend into parameters, the trailing return type or the
// clauses after them — `const std::string&`, `-> std::string`,
// nlohmann/json's `noexcept(noexcept(JSONSerializer<U>::to_json(…)))`,
// `throw(std::bad_alloc)`, `requires std::integral<T>` name no function.
if (child && !DECLARATOR_NON_NAME_PARTS.has(child.type)) {
queue.push(child);
}
}
@@ -110,6 +112,11 @@ function findDeclaratorQualifiedId(declarator: SyntaxNode): SyntaxNode | undefin
return undefined;
}
/** The parts of a function declarator that hold types or expressions, never its name. */
const DECLARATOR_NON_NAME_PARTS: ReadonlySet<string> = new Set([
'parameter_list', 'trailing_return_type', 'noexcept', 'throw_specifier', 'requires_clause',
]);
/**
* Single-argument macros are ambiguous without their definition (#1373).
* Require a preceding local #define whose replacement uses its sole parameter