import type { Node as SyntaxNode } from 'web-tree-sitter'; import { getChildByField, getNodeText } from '../tree-sitter-helpers'; import type { LanguageExtractor } from '../tree-sitter-types'; /** * Find the function NAME's `qualified_identifier` (`Foo::bar`) inside a * declarator, skipping the `parameter_list` — a parameter with a qualified type * (`const std::string& x`) must NOT be mistaken for the method name. Without the * skip, a plain free function `std::string TableFileName(const std::string&...)` * was named `string` (from the parameter type), so calls to it never resolved * and its file looked like nothing depended on it. */ function findDeclaratorQualifiedId(declarator: SyntaxNode): SyntaxNode | undefined { const queue: SyntaxNode[] = [declarator]; while (queue.length > 0) { const current = queue.shift()!; 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') { queue.push(child); } } } return undefined; } /** * Recover the real function name from the macro-definition idiom * `MACRO_NAME(real_name, typed args…) { body }` — flash-attention's * `DEFINE_FLASH_FORWARD_KERNEL(flash_fwd_kernel, bool Is_dropout, …) { … }` * being the motivating case: tree-sitter parses the invocation as a * function_definition NAMED after the macro, so every such kernel shared one * name (`DEFINE_FLASH_FORWARD_KERNEL`) and the launch sites' calls to the real * names (`flash_fwd_kernel<…><<<…>>>`) could never resolve. * * Deliberately narrow so name-in-first-arg is unambiguous — ALL of: * - the parsed name is macro-shaped: ALL-CAPS with at least one underscore * (`TEST` never matches; K&R C definitions have lowercase names); * - the first "parameter" is a LONE identifier (no type, no declarator) * containing a lowercase letter — the name being defined; * - at least one more parameter follows and NONE of them is another lone * identifier — a second bare arg means the first isn't the name (gtest's * `TEST_F(Fixture, Name)`, `PYBIND11_MODULE(ext, m)`, * google-benchmark's `BENCHMARK_DEFINE_F(Fix, name)` all bail here). */ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string | undefined { if (node.type !== 'function_definition') return undefined; const declarator = getChildByField(node, 'declarator'); if (declarator?.type !== 'function_declarator') return undefined; const inner = getChildByField(declarator, 'declarator'); if (inner?.type !== 'identifier') return undefined; const macroName = getNodeText(inner, source); if (!/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(macroName)) return undefined; const params = getChildByField(declarator, 'parameters'); if (!params || params.namedChildCount < 2) return undefined; const loneIdentText = (p: SyntaxNode): string | null => p.type === 'parameter_declaration' && p.namedChildCount === 1 && p.namedChild(0)?.type === 'type_identifier' ? getNodeText(p.namedChild(0)!, source) : null; const first = params.namedChild(0); const name = first ? loneIdentText(first) : null; if (!name || !/[a-z]/.test(name)) return undefined; for (let i = 1; i < params.namedChildCount; i++) { const p = params.namedChild(i); if (p && loneIdentText(p) !== null) return undefined; } return name; } function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined { const macroDefined = recoverCppMacroDefinedName(node, source); if (macroDefined) return macroDefined; const declarator = getChildByField(node, 'declarator'); if (!declarator) return undefined; const qid = findDeclaratorQualifiedId(declarator); if (!qid) return undefined; const parts = getNodeText(qid, source).trim().split('::').filter(Boolean); return parts[parts.length - 1]; } function extractCppReceiverType(node: SyntaxNode, source: string): string | undefined { const declarator = getChildByField(node, 'declarator'); if (!declarator) return undefined; const qid = findDeclaratorQualifiedId(declarator); if (!qid) return undefined; const parts = getNodeText(qid, source).trim().split('::').filter(Boolean); if (parts.length <= 1) return undefined; // An out-of-line template method definition carries the class's template // parameter list in the qualifier (`template T Box::get()`), // but the class node is indexed as bare `Box` — strip `<…>` so the receiver // matches it, the same normalization #1043 applies to base-class refs. // Multi-line parameter lists otherwise leak whole `<…>` blocks (newlines // included) into qualified_name, which can exceed NAME_MAX (#1286). const receiver = stripCppTemplateArgs(parts.slice(0, -1).join('::')); return receiver || undefined; } /** * Built-in / non-class return types that can never be a method receiver. We * store no `returnType` for these so resolution never tries to resolve a method * on `void` / `int` / etc. */ const CPP_NON_CLASS_RETURN = new Set([ 'void', 'bool', 'char', 'short', 'int', 'long', 'float', 'double', 'unsigned', 'signed', 'size_t', 'ssize_t', 'auto', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'intptr_t', 'uintptr_t', 'nullptr_t', ]); /** * Normalize a C++ return type to the bare class name a method could be called * on. Unwraps smart-pointer / optional wrappers to their element type * (`std::unique_ptr` → `Widget`) so a factory's `->method()` resolves on * the pointee. Strips cv-qualifiers, `&`/`*`, namespace qualifiers, and other * template args. Returns undefined for primitives / void / `auto` / empty. */ export function normalizeCppReturnType(raw: string): string | undefined { let t = raw.trim(); if (!t) return undefined; // Unwrap smart pointers / optional to their pointee (the thing you call `->` on). const wrapper = t.match(/\b(?:std\s*::\s*)?(?:unique_ptr|shared_ptr|weak_ptr|optional)\s*<\s*([^,>]+?)\s*>/); if (wrapper && wrapper[1]) t = wrapper[1]; t = t .replace(/\b(?:const|volatile|typename|struct|class|enum)\b/g, ' ') .replace(/<[^>]*>/g, ' ') .replace(/[*&]+/g, ' ') .replace(/\s+/g, ' ') .trim(); if (!t) return undefined; const last = t.split('::').filter(Boolean).pop(); if (!last) return undefined; if (CPP_NON_CLASS_RETURN.has(last)) return undefined; if (!/^[A-Za-z_]\w*$/.test(last)) return undefined; return last; } /** * Strip C++ template arguments from a base-type reference name so it matches the * bare class/struct the template was DEFINED as. `template class * Base { … }` is indexed as a node named `Base`, but a derived class * `class D : public Base` records its base as the full `Base` (and * `class Q : public ns::Tpl` as `ns::Tpl`) — neither name-matches * `Base` / `ns::Tpl`, so the `extends` edge never resolves and the derived class * looks like it inherits from nothing (#1043). * * Removes every balanced `<…>` group regardless of nesting or position, so * `Base` → `Base`, `ns::Tpl>` → `ns::Tpl`, and the rare * `Outer::Inner` → `Outer::Inner`. The remaining qualified head is exactly * what the non-templated base case already produces, so resolution treats them * identically. A name with no template args passes through unchanged. */ export function stripCppTemplateArgs(name: string): string { if (!name.includes('<')) return name; let out = ''; let depth = 0; for (const ch of name) { if (ch === '<') depth++; else if (ch === '>') { if (depth > 0) depth--; } else if (depth === 0) out += ch; } return out.trim(); } /** * A function/method's return type lives in the `function_definition`'s `type` * field (`Metrics& Metrics::instance()` → `Metrics`). Constructors, destructors, * and conversion operators have no `type` field → undefined. */ function extractCppReturnType(node: SyntaxNode, source: string): string | undefined { const typeNode = getChildByField(node, 'type'); if (!typeNode) return undefined; return normalizeCppReturnType(getNodeText(typeNode, source)); } export const cExtractor: LanguageExtractor = { // CUDA in C-detected headers (content-gated blank; see preParseCSource). preParse: preParseCSource, // Universal net: recover a real name from any macro-mangled function name. recoverMangledName: recoverMangledCppName, functionTypes: ['function_definition'], classTypes: [], methodTypes: [], interfaceTypes: [], structTypes: ['struct_specifier'], // A bodiless `union U;` is a forward declaration; the aggregate extractor // applies the same body requirement it uses for C structs. unionTypes: ['union_specifier'], enumTypes: ['enum_specifier'], enumMemberTypes: ['enumerator'], typeAliasTypes: ['type_definition'], // typedef importTypes: ['preproc_include'], callTypes: ['call_expression'], variableTypes: ['declaration'], nameField: 'declarator', bodyField: 'body', paramsField: 'parameters', // A `const`/`static const` file-scope declaration carries a `type_qualifier` // child reading "const" — extract those as `constant`, plain globals as // `variable`. isConst: (node) => node.namedChildren.some( (c: SyntaxNode) => c.type === 'type_qualifier' && c.text === 'const' ), getReturnType: extractCppReturnType, resolveTypeAliasKind: (node, _source) => { // C typedef: `typedef enum { ... } name;` or `typedef struct { ... } name;` // The inner enum_specifier/struct_specifier is anonymous, but we want the typedef name // to become the enum/struct node name. `typedef union { ... } name;` takes the // same route — otherwise the union body would mint a second, `` node // beside the alias. for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum'; if ( child.type === 'struct_specifier' && getChildByField(child, 'body') ) return 'struct'; if (child.type === 'union_specifier' && getChildByField(child, 'body')) return 'union'; } return undefined; }, extractImport: (node, source) => { const importText = source.substring(node.startIndex, node.endIndex).trim(); // C includes: #include , #include "myheader.h" const systemLib = node.namedChildren.find((c: SyntaxNode) => c.type === 'system_lib_string'); if (systemLib) { return { moduleName: getNodeText(systemLib, source).replace(/^<|>$/g, ''), signature: importText }; } const stringLiteral = node.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal'); if (stringLiteral) { const stringContent = stringLiteral.namedChildren.find((c: SyntaxNode) => c.type === 'string_content'); if (stringContent) { return { moduleName: getNodeText(stringContent, source), signature: importText }; } } return null; }, }; /** * Detect tree-sitter's misparse of a macro-annotated class/struct, e.g. * `class MACRO Name { … }` or `class MACRO Name : public Base { … }` (#946). * Not knowing `MACRO` is a macro, tree-sitter reads `class MACRO` as an * *elaborated type specifier* (a bodyless `class_specifier`/`struct_specifier` * whose "type name" is the macro) and the rest as a function: `Name` becomes the * declarator and the `{ … }` a function body — so the whole declaration surfaces * as a `function_definition` named after the class, with a line range spanning * the entire class body. (A base clause, when present, additionally lands in an * `ERROR` node, but it isn't required — the leading macro alone triggers this.) * * Two structural signals pin it down with no risk to genuine code: * - the `type` field is a *bodyless* class/struct specifier — an elaborated * type, not a real inline-defined return type like * `struct P { int x; } makeP() { … }` (which carries a field list); and * - the declarator is not a `function_declarator` — a real function definition * always has one, which also leaves the legal-but-rare `class Foo f() { … }` * (an elaborated return type on a genuine function) alone. * * The class body is mangled by the same misparse and is unrecoverable, so — * matching how macro-prefixed C prototypes are handled — we drop the spurious * node rather than mint a misleading whole-body `function` that pollutes * callers/impact and skews kind statistics. */ function isMacroMisparsedTypeDecl(node: SyntaxNode): boolean { const typeNode = getChildByField(node, 'type'); if (!typeNode) return false; if (typeNode.type !== 'class_specifier' && typeNode.type !== 'struct_specifier') return false; if (typeNode.namedChildren.some((c: SyntaxNode) => c.type === 'field_declaration_list')) return false; const declarator = getChildByField(node, 'declarator'); if (declarator && declarator.type === 'function_declarator') return false; return true; } /** * Blank an export/visibility macro in a `class/struct EXPORT_MACRO Name …` * *definition* header before parsing. Not knowing the macro, tree-sitter reads * `class EXPORT_MACRO` as an elaborated type specifier and the rest as a * function, so the whole class — its name, base clause, and members — drops out * of the index (#946 catches the resulting phantom function but can't recover * the class), which silently breaks type-hierarchy / inheritance-impact queries * for effectively every Unreal-Engine (`*_API`), Qt/Boost (`*_EXPORT`), LLVM * (`*_ABI`), … class. Replacing the macro with equal-length spaces preserves * every byte offset (and thus line/column), so the declaration then parses as a * normal class_specifier and the existing extraction emits the node, members, * and `extends` edge. (#1061, follow-up to #946.) * * Matched tightly so it can't touch the same macro used as an ordinary value * elsewhere (`int x = SOME_API;`): the macro is the ALL-CAPS token sitting * *between* `class`/`struct` and the type name, and the trailing `[:{]` * definition-guard fires only when a base clause or body follows — the only * shape that misparses. That guard also leaves elaborated-type variable * declarations (`struct FOO var;`, `class FOO obj = …`) untouched, since those * end in `;` / `=` / `[`, never `:` / `{`. C++-only (wired into cppExtractor), * so C's heavier use of `struct TAG var;` never reaches it. */ export function blankCppExportMacros(source: string): string { if (source.indexOf('class') === -1 && source.indexOf('struct') === -1) return source; return source.replace( /\b(class|struct)(\s+)([A-Z][A-Z0-9_]+)(?=\s+[A-Za-z_]\w*(?:\s+final)?\s*[:{])/g, (_m, kw, ws, macro) => kw + ws + ' '.repeat(macro.length) ); } /** * Blank a known inline-specifier macro sitting in front of a function's return * type (`FORCEINLINE FString GetName(…)`), before parsing. Not knowing the * macro, tree-sitter can't reconcile `MACRO (` — an extra * type-like token before the name — and drops into error recovery: the macro * becomes the return type and, for a non-primitive return, the return type gets * glued onto the name (`GetName` → `"FString GetName"`), so the function can't * be found by name and its callers don't link. This is pervasive in Unreal * Engine (`FORCEINLINE (…)`) and in vendored third-party libraries * that define their own inline macro (pugixml's `PUGI__FN`, Godot's * `_FORCE_INLINE_`, Boost's `BOOST_FORCEINLINE`, …). Replacing the macro with * equal-length spaces preserves every byte offset (so line/column stay exact) * and the declaration then parses as an ordinary function — recovering the real * name AND the return type — mirroring how `blankCppExportMacros` recovers * macro-annotated classes (#946/#1061). * * Matched tightly so it can't touch an ordinary identifier: only the exact, * curated inline-specifier tokens below (never an arbitrary all-caps token, so a * real return type like `HRESULT DoIt()` is untouched), and only in specifier * position — immediately followed by whitespace and the identifier that starts * the return type or name. That lookahead leaves value/expression uses * (`x = FORCEINLINE ? …`), string literals, and longer words * (`FORCEINLINE_SOMETHINGELSE`, word-boundary) alone. To cover a new codebase's * inline macro, add its exact token to the list. */ const CPP_INLINE_MACROS = [ // Unreal Engine 'FORCEINLINE_DEBUGGABLE', 'FORCENOINLINE', 'FORCEINLINE', // pugixml (ubiquitous vendored XML parser): `#define PUGI__FN inline` before // the return type, plus `PUGIXML_FUNCTION` (linkage macro) between the return // type and the name — the blank mechanism handles both positions. 'PUGI__FN_NO_INLINE', 'PUGI__FN', 'PUGIXML_FUNCTION', // Godot '_ALWAYS_INLINE_', '_FORCE_INLINE_', // Boost 'BOOST_FORCEINLINE', 'BOOST_NOINLINE', // Qt (per-method markers + inline) 'Q_INVOKABLE', 'Q_SCRIPTABLE', 'Q_ALWAYS_INLINE', 'Q_SLOT', 'Q_SIGNAL', // Folly / Abseil / LLVM / V8 / Eigen / rapidjson 'FOLLY_ALWAYS_INLINE', 'FOLLY_NOINLINE', 'ABSL_ATTRIBUTE_ALWAYS_INLINE', 'ABSL_ATTRIBUTE_NOINLINE', 'LLVM_ATTRIBUTE_ALWAYS_INLINE', 'LLVM_ATTRIBUTE_NOINLINE', 'V8_INLINE', 'V8_NOINLINE', 'EIGEN_STRONG_INLINE', 'EIGEN_ALWAYS_INLINE', 'EIGEN_DEVICE_FUNC', 'RAPIDJSON_FORCEINLINE', // Mozilla / SpiderMonkey 'MOZ_ALWAYS_INLINE', 'MOZ_NEVER_INLINE', // Protocol Buffers 'PROTOBUF_ALWAYS_INLINE', 'PROTOBUF_NOINLINE', // {fmt} / spdlog 'FMT_CONSTEXPR20', 'FMT_CONSTEXPR', 'FMT_INLINE', // Hedley + nlohmann/json (bundles Hedley) 'JSON_HEDLEY_ALWAYS_INLINE', 'JSON_HEDLEY_NEVER_INLINE', 'HEDLEY_ALWAYS_INLINE', 'HEDLEY_NEVER_INLINE', // GLM (graphics math — pervasive in games/rendering) 'GLM_FUNC_QUALIFIER', 'GLM_FUNC_DECL', 'GLM_CONSTEXPR', 'GLM_INLINE', // Bullet Physics / Skia / OpenCV / EASTL / Cocos2d-x / Chromium-WebKit 'SIMD_FORCE_INLINE', 'SK_ALWAYS_INLINE', 'CV_ALWAYS_INLINE', 'CV_INLINE', 'EA_FORCE_INLINE', 'EA_NOINLINE', 'CC_INLINE', 'NEVER_INLINE', // C libraries: GLib, SQLite (internal linkage) 'G_INLINE_FUNC', 'SQLITE_PRIVATE', 'SQLITE_API', // Windows calling conventions (linkage position — recover the return type; the // name is salvaged regardless). Only the unambiguous, non-word-like ones. 'STDMETHODCALLTYPE', 'WINAPIV', 'WINAPI', 'APIENTRY', // Common cross-ecosystem inline/attribute hints 'ALWAYS_INLINE', 'FORCE_INLINE', 'NOINLINE', ] as const; // One alternation, longest token first so a longer macro wins over a prefix. const CPP_INLINE_MACRO_RE = new RegExp( `\\b(${[...CPP_INLINE_MACROS].sort((a, b) => b.length - a.length).join('|')})\\b(?=\\s+[A-Za-z_])`, 'g' ); export function blankCppInlineMacros(source: string): string { if (!CPP_INLINE_MACROS.some((m) => source.indexOf(m) !== -1)) return source; return source.replace(CPP_INLINE_MACRO_RE, (m) => ' '.repeat(m.length)); } // Bare C/C++ type/qualifier tokens that must never be taken as a recovered // function name (guards `recoverMangledCppName` against the `Ret (name)` idiom, // where the token before the params is the return type, not the name). const CPP_PRIMITIVE_NAMES = new Set([ 'bool', 'void', 'int', 'char', 'short', 'long', 'float', 'double', 'unsigned', 'signed', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', 'char_t', 'size_t', 'auto', 'const', 'struct', 'class', 'enum', 'union', 'typename', ]); /** * Universal fallback (any macro, no list) for a C/C++ function name still mangled * because a macro we don't blank sat in front of the return type: `MACRO Ret * name(…)` / `Ret MACRO name(…)` misparse so the return type is glued onto the * name ("Ret name", "char_t* to_str(double v)"). Recover the real identifier — * the token immediately before the parameter list (or the last token). This runs * AFTER the curated pre-parse blank, so it only ever sees the residual tail that * blanking didn't already fix cleanly (which also recovers the return type). * * Safe by construction: only touches an ALREADY-mangled name — one with an * internal space that isn't a legit `operator …`/destructor — so a well-formed * name is returned unchanged. Guarded against the two ways it could mis-pick: * the `Ret (name)` parenthesized-name idiom (left as-is, ambiguous), and a token * that is a bare primitive/keyword rather than a real identifier. */ export function recoverMangledCppName(name: string): string { if (!/\s/.test(name) || name.startsWith('operator') || name.startsWith('~')) return name; if (/^\S+\s+\([A-Za-z_]\w*\)/.test(name)) return name; // `Ret (name)` idiom — leave alone const beforeParams = name.includes('(') ? name.slice(0, name.indexOf('(')) : name; const tokens = beforeParams.trim().split(/\s+/); const candidate = tokens[tokens.length - 1]; if (!candidate || !/^[A-Za-z_]\w*$/.test(candidate) || CPP_PRIMITIVE_NAMES.has(candidate)) return name; return candidate; } /** * Blank Metal Shading Language `[[attribute]]` annotations before parsing. * MSL (≈ C++14) puts attributes AFTER the declarator — `float4 position * [[position]];`, `constant Uniforms &u [[buffer(0)]]` — a position * tree-sitter-cpp can't reconcile: a struct field with a trailing attribute * misparses into a shape that emits a spurious `extends` reference from the * struct to the field's *type* (`VertexIn extends float3`), which becomes a * wrong inheritance edge whenever the repo defines that type itself (simd * typedefs in a shared ShaderTypes.h are common). Replacing the attribute with * equal-length spaces preserves every byte offset and lets fields and * parameters parse as ordinary declarations, mirroring the macro blanks above. * * Matched tightly to the attribute shape — `[[ident]]`, `[[ident(args)]]`, and * comma-separated lists (`[[buffer(0), raster_order_group(0)]]`) — so a * subscripted lambda call (`arr[[]{ … }()]`, the only other way `[[` appears in * C++-family source) can never match: after `[[` a lambda continues with `]`, * never an identifier followed by `]]`. Applied ONLY to `.metal` files — in * regular C++ the pre-declarator attribute position (`[[nodiscard]] int f()`) * is legal syntax the grammar parses natively, and blanking it would be pure * blast radius. (#1121) */ const METAL_ATTRIBUTE_RE = /\[\[\s*[A-Za-z_]\w*(?:\s*\([^()\n]*\))?(?:\s*,\s*[A-Za-z_]\w*(?:\s*\([^()\n]*\))?)*\s*\]\]/g; export function blankMetalAttributes(source: string): string { if (source.indexOf('[[') === -1) return source; return source.replace(METAL_ATTRIBUTE_RE, (m) => ' '.repeat(m.length)); } /** * Blank annotation-style macro invocations that decorate a declaration but carry * NO terminating semicolon — the pervasive Unreal-Engine reflection markup * (`UPROPERTY(...)`, `UFUNCTION(...)`, `UCLASS(...)`, `GENERATED_BODY()`, * `UE_DEPRECATED_FORGAME(...)`, `DECLARE_DELEGATE_*(...)`, …) that sits on its * own line right before a member/type. tree-sitter's C++ grammar doesn't know * these are macros, so each one drops into error recovery; in a big reflected * class (`CharacterMovementComponent.h` has ~240 of them) the errors accumulate * until the enclosing `class_specifier` can't close and collapses into an ERROR * node — the whole class definition, its members, and its `extends` edges vanish * from the graph. Neither `blankCppExportMacros` (class-header export macros) nor * `blankCppInlineMacros` (return-type inline specifiers) touches these in-body * markup macros. Replacing each with equal-length spaces preserves every byte * offset (so line/column stay exact) and the class then parses normally. * * Deliberately name-list-FREE — UE alone has hundreds of such macros and projects * add their own — so it keys on structure, not a curated list, matched tightly to * avoid touching legitimate C++: * - the macro must be the FIRST non-whitespace token on its line (`^[ \t]*`), * which is where declaration markup lives — so a macro used inside an * expression or condition (`if (CHECK(x))`, `x = MACRO(a) + b`) is never * matched (it isn't line-leading); * - the name must be ALL-CAPS (`[A-Z][A-Z0-9_]{2,}`), since ordinary * function/type names called at line start are lower/mixed case; * - the char after the balanced `(...)` must START A DECLARATION — a letter, * `_`, `~` (destructor), or `#` (a following directive). Declaration markup is * always followed by the thing it decorates (`UPROPERTY(...)\n float X;`, * `UE_DEPRECATED(...) UPROPERTY(...)`), whereas a statement call is followed by * `;` (`FOO(x);`), an init-list item by `,`/`{`, and an expression fragment by * an operator (`MAKE(a) + 1`) — all rejected. String/char literals inside the * args are skipped so an embedded `)` can't mis-close the balance. * * C++-only (wired into cppExtractor). A blanked macro inside a block comment is * harmless (comments don't parse), and the rare line-leading no-semicolon * ALL-CAPS call that isn't markup only loses that one annotation, never a whole * class. */ export function blankCppAnnotationMacroCalls(source: string): string { if (!/^[ \t]*[A-Z][A-Z0-9_]{2,}\s*\(/m.test(source)) return source; const chars = source.split(''); const re = /^([ \t]*)([A-Z][A-Z0-9_]{2,})(\s*)\(/gm; let m: RegExpExecArray | null; while ((m = re.exec(source)) !== null) { const macroStart = m.index + (m[1] ?? '').length; // skip leading indent let i = m.index + m[0].length - 1; // index of the opening '(' let depth = 0; let end = -1; for (; i < source.length; i++) { const c = source[i]; if (c === '"' || c === "'") { const quote = c; i++; while (i < source.length && source[i] !== quote) { if (source[i] === '\\') i++; i++; } continue; } if (c === '(') depth++; else if (c === ')') { depth--; if (depth === 0) { end = i + 1; break; } } } if (end < 0) continue; let j = end; while (j < source.length && /\s/.test(source[j] as string)) j++; const after = source[j]; // Only markup is followed by the declaration it decorates; a statement call // (`;`), init-list item (`,`/`{`), or expression fragment (operator) is not. if (!after || !/[A-Za-z_~#]/.test(after)) continue; for (let k = macroStart; k < end; k++) { if (chars[k] !== '\n' && chars[k] !== '\r') chars[k] = ' '; } re.lastIndex = end; } return chars.join(''); } /** * Blank a macro that is the ONLY token on its line — no parens, no semicolon: * namespace-management macros (`FMT_BEGIN_NAMESPACE`, `FMT_END_EXPORT`, * `JEMALLOC_DIAGNOSTIC_DISABLE_SPURIOUS`), Qt's `Q_OBJECT`, and friends. A * bare identifier is not a statement or declaration in C or C++, so * tree-sitter drops into error recovery at every one — and since the kernel * path defers ANY erroring file to wasm, this single idiom deferred 13/73 fmt * files and a comparable share of jemalloc, forfeiting the native-parse win * on exactly the header-heavy trees it targets (the wasm path also mis-nests * scopes around them today). Replacing the token with equal-length spaces * preserves every byte offset and the surrounding declarations parse clean. * * Matched tightly so a real identifier can never be touched — ALL of: * - the line consists of ONE ALL-CAPS token (≥4 chars, with `_`), optionally * followed by a same-line comment — a lone lowercase identifier or any * second token disqualifies; * - the PREVIOUS non-blank line does not end in a continuation character * (`=`, an operator, `,`, `(`, `?`, `:`, or a `\` macro-definition * continuation) — so an ALL-CAPS operand split onto its own line inside a * multi-line expression (`int x =\n SOME_CONST\n | OTHER;`) is left * alone; and * - the NEXT non-blank line starts like a declaration/scope token * (letter, `_`, `#`, `{`, `}`, or `~`) or the file ends — an operator, * string literal, or `;` continuation rejects the match. * Shared by C and C++ (the idiom is identical in both). */ const LONE_MACRO_LINE_RE = /^[ \t]*([A-Z][A-Z0-9_]{3,})[ \t]*(?:\/\/[^\n\r]*|\/\*[^\n\r]*\*\/[ \t]*)?\r?$/; const LONE_MACRO_CONTINUATION_END_RE = /[=+\-*/%&|^<>?:,(\\]$/; export function blankLoneMacroLines(source: string): string { if (!/^[ \t]*[A-Z][A-Z0-9_]{3,}[ \t]*\r?$/m.test(source)) return source; const lines = source.split('\n'); const content = (l: string): string => l.replace(/\r$/, '').trim(); let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; const m = LONE_MACRO_LINE_RE.exec(line); if (!m) continue; // Underscore requirement rides the macro convention (FMT_BEGIN_NAMESPACE, // Q_OBJECT); a solid all-caps word (`NDEBUG`-style) alone is too risky. if (!(m[1] as string).includes('_')) continue; let prev = i - 1; while (prev >= 0 && content(lines[prev] as string) === '') prev--; if (prev >= 0 && LONE_MACRO_CONTINUATION_END_RE.test(content(lines[prev] as string))) continue; let next = i + 1; while (next < lines.length && content(lines[next] as string) === '') next++; if (next < lines.length) { const first = content(lines[next] as string)[0]; if (!first || !/[A-Za-z_#{}~]/.test(first)) continue; } const start = line.indexOf(m[1] as string); lines[i] = line.slice(0, start) + ' '.repeat((m[1] as string).length) + line.slice(start + (m[1] as string).length); changed = true; } return changed ? lines.join('\n') : source; } /** * Blank an export/visibility macro sitting in front of a *member* or *method* * declaration inside a class/namespace (`ENGINE_API virtual void Tick(…)`, * `static ENGINE_API void AddReferencedObjects(…)`, `UE_API FVector GetVel() * const`), before parsing. `blankCppExportMacros` only recovers the macro in a * `class MACRO Name` *header*; the very same macro also prefixes almost every * exported member of a big Unreal-Engine class, and tree-sitter — not knowing * it's a macro — reads `MACRO (` as an extra type token and * drops each such declaration into error recovery. In a heavily-exported header * (`Actor.h`, `World.h`, …) hundreds of these accumulate: the return types pile * up as orphan ERROR tokens and, combined with other markup, can still tip the * enclosing class into collapse. Replacing the macro with equal-length spaces * preserves every byte offset (line/column stay exact) and each member parses * as an ordinary declaration. * * Matched tightly so it can't touch the same token used as a value * (`int x = SOME_API;`, `if (mode == FOO_API)`): the token must be ALL-CAPS AND * end in the conventional visibility-macro suffix `_API` / `_EXPORT` / `_ABI` * (Unreal `*_API`, Qt/Boost `*_EXPORT`, LLVM `*_ABI`) — ordinary identifiers * effectively never carry these suffixes — and must be immediately followed by * whitespace then a declaration token (`\s+[A-Za-z_]`: a type, `virtual`, * `static`, or the name). A value use is instead followed by `;`, `)`, `,`, * `=`, `::`, or an operator, all of which fail the look-ahead. C++-only (wired * into cppExtractor). */ const CPP_API_PREFIX_RE = /\b[A-Z][A-Z0-9_]*(?:_API|_EXPORT|_ABI)\b(?=\s+[A-Za-z_])/g; export function blankCppApiPrefixMacros(source: string): string { if (!/_(?:API|EXPORT|ABI)\b/.test(source)) return source; return source.replace(CPP_API_PREFIX_RE, (m) => ' '.repeat(m.length)); } /** * Blank an Unreal-Engine annotation macro that appears MID-LINE (not * line-leading, so `blankCppAnnotationMacroCalls` never sees it) inside a * declaration: an enum value's `UMETA(DisplayName="…")`, a parameter's * `UPARAM(ref)`, or a deprecation tag wedged into a `using`/member declaration * (`using FOnNetTick UE_DEPRECATED(5.5, "…") = TMulticastDelegate;` * in `World.h`, which otherwise collapses `UWorld`). tree-sitter can't reconcile * these embedded macro calls and drops into error recovery, and a mid-line one * inside a big enum or a class-scope `using` can cascade into the whole enum / * class being lost. Replacing the entire `MACRO(...)` (balanced parens, string * literals skipped so an embedded `)` can't mis-close) with equal-length spaces * preserves every byte offset and the declaration parses normally. * * Keyed on an explicit UE-only name list (`UMETA`, `UPARAM`, and the * `UE_DEPRECATED*` family) — these identifiers are exclusive to Unreal's * reflection layer and appear in no standard-C++ or other-library code, so * blanking them is zero-risk to non-UE sources. (The line-LEADING forms of * `UE_DEPRECATED(...)` are already handled by `blankCppAnnotationMacroCalls`; * this covers the mid-line forms it structurally can't.) C++-only. */ const CPP_INLINE_ANNOTATION_RE = /\b(?:UMETA|UPARAM|UE_DEPRECATED\w*)\s*\(/g; export function blankCppInlineAnnotationMacros(source: string): string { if (!/\b(?:UMETA|UPARAM|UE_DEPRECATED)/.test(source)) return source; const chars = source.split(''); const re = new RegExp(CPP_INLINE_ANNOTATION_RE.source, 'g'); let m: RegExpExecArray | null; while ((m = re.exec(source)) !== null) { let i = m.index + m[0].length - 1; // index of the opening '(' let depth = 0; let end = -1; for (; i < source.length; i++) { const c = source[i]; if (c === '"' || c === "'") { const quote = c; i++; while (i < source.length && source[i] !== quote) { if (source[i] === '\\') i++; i++; } continue; } if (c === '(') depth++; else if (c === ')') { depth--; if (depth === 0) { end = i + 1; break; } } } if (end < 0) continue; for (let k = m.index; k < end; k++) { if (chars[k] !== '\n' && chars[k] !== '\r') chars[k] = ' '; } re.lastIndex = end; } return chars.join(''); } /** * Blank CUDA-specific constructs before parsing `.cu`/`.cuh` files (parsed with * the C++ grammar). Three shapes tree-sitter-cpp can't reconcile, each replaced * with equal-length whitespace so every byte offset survives (#387): * * 1. Execution-space / storage specifiers: in `__global__ void step(…)` or * `__shared__ float tile[256]` the specifier parses as the declaration's * TYPE and shunts the real return/value type into an ERROR node — mangling * signatures and, for `__shared__` arrays, the declared name itself. Blanked * unconditionally (no following-token lookahead) so extended lambdas * (`[=] __device__ (int i) { … }`) recover too. `__restrict__` is deliberately * absent: the grammar already parses it natively as a type_qualifier. * 2. `__launch_bounds__(…)` between specifier and declarator — same misparse. * The parenthesized form is blanked first; a bare leftover token is caught * by the specifier list. * 3. Kernel-launch configs `step<<>>(args)`: the * chevrons lex as shift operators around an empty-named template, so no * call_expression exists and the host→kernel call edge — the main reason to * index CUDA at all — is lost. Blanking the `<<<…>>>` span leaves * `step (args)`, a plain call the grammar * parses natively (templated launches `k<<<…>>>(…)` included). * * The launch-config match is deliberately bounded — statement/brace characters * excluded, span capped, newlines preserved by the replacer — so a stray `<<<` * (a committed merge-conflict marker, a string literal) can never blank a run * of real code: an unmatched launch degrades to the status quo for that call * site (no call edge), never to corruption. Applied to `.cu`/`.cuh` files and — * because much real CUDA lives in extension-less headers (cutlass launches the * majority of its kernels from `.h`; flash-attention's launch templates are * `.h`; llm.c keeps device helpers in C-detected `.h`) — to any C/C++-family * file whose CONTENT carries a strong CUDA marker (`looksLikeCudaSource`). * Unlike Metal's `[[attribute]]` (legal C++ syntax elsewhere, hence Metal's * strict extension gate), no CUDA marker is valid C++ anywhere: `<<<` isn't * legal syntax and the dunder specifiers are implementation-reserved names no * real codebase defines — so a content-triggered blank on a non-CUDA file can * only ever whitespace tokens inside comments or strings, which parse the same. */ const CUDA_LAUNCH_BOUNDS_RE = /\b__launch_bounds__\s*\([^()\n]*\)/g; const CUDA_SPECIFIER_RE = /\b__(?:global|device|host|constant|shared|managed|grid_constant|forceinline|noinline|launch_bounds)__\b/g; // `;` stays excluded (launch configs are expressions; a stray `<<<` spanning // real statements always crosses one) and the span is capped. Braces are // allowed through the regex — `k<<>>(…)` is a real // launch shape — but the replacer only blanks a BALANCED match: a merge // conflict's `<<<<<<< … >>>>>>>` region that dodges every `;` still opens // braces it never closes, so it fails the balance check and stays untouched. const CUDA_LAUNCH_CONFIG_RE = /<<<[^;]{0,400}?>>>/g; export function blankCudaConstructs(source: string): string { let out = source; if (out.indexOf('__') !== -1) { out = out .replace(CUDA_LAUNCH_BOUNDS_RE, (m) => ' '.repeat(m.length)) .replace(CUDA_SPECIFIER_RE, (m) => ' '.repeat(m.length)); } if (out.indexOf('<<<') !== -1) { out = out.replace(CUDA_LAUNCH_CONFIG_RE, (m) => { let depth = 0; for (let i = 0; i < m.length; i++) { const ch = m.charCodeAt(i); if (ch === 0x7b /* { */) depth++; else if (ch === 0x7d /* } */ && --depth < 0) return m; } return depth === 0 ? m.replace(/[^\n]/g, ' ') : m; }); } return out; } /** Strong content markers for CUDA source in files without a CUDA extension * (headers). The dunders are execution-space specifiers that only nvcc defines; * `cudaStream_t` is the runtime's stream handle, pervasive in launcher headers * that themselves declare no kernel. Deliberately excludes weak markers (`dim3`, * `<<<`) that could plausibly appear in non-CUDA text. */ function looksLikeCudaSource(source: string): boolean { return ( source.indexOf('__global__') !== -1 || source.indexOf('__device__') !== -1 || source.indexOf('__constant__') !== -1 || source.indexOf('cudaStream_t') !== -1 ); } /** * Restore preprocessor-directive lines to their original bytes after the * blanking passes ran. The token-level blanks match on shape, not context, so * a macro name that happens to sit inside a DIRECTIVE gets blanked too — and * blanking the name position of `#define FMT_API FMT_VISIBILITY("default")` * leaves a nameless `# define FMT_VISIBILITY(…)`, which is a parse * ERROR (fmt's base.h carries several). Inside a directive the blanks were * never useful anyway: tree-sitter stores `#define` bodies as raw * preproc_arg text it doesn't parse, so blanking there can only ever break * the directive itself. Copying the original directive lines back (including * `\`-continuation lines of multi-line defines) is offset-preserving by * construction and strictly reduces parse errors on both extraction arms. */ function restoreDirectiveLines(original: string, blanked: string): string { if (blanked === original || original.indexOf('#') === -1) return blanked; const o = original.split('\n'); const b = blanked.split('\n'); let changed = false; let continuation: boolean = false; for (let i = 0; i < o.length && i < b.length; i++) { const line = o[i] as string; const isDirective: boolean = continuation || /^[ \t]*#/.test(line); if (isDirective && b[i] !== line) { b[i] = line; changed = true; } continuation = isDirective && /\\\s*$/.test(line.replace(/\r$/, '')); } return changed ? b.join('\n') : blanked; } /** C/C++ source pre-processing before tree-sitter: recover macro-annotated class * definitions, macro-prefixed function definitions, macro-prefixed members, and * macro-decorated members (Unreal-Engine reflection markup) — plus the non-C++ * surface of the dialects parsed with the C++ grammar: `.metal` MSL attribute * annotations, and CUDA specifiers + launch syntax (by `.cu`/`.cuh` extension * or by content, for CUDA living in `.h`/`.hpp` headers). Offset-preserving; * directive lines are restored at the end (see restoreDirectiveLines). */ function preParseCppSource(source: string, filePath?: string): string { // blankCLeadingAttrMacros runs AFTER the api-prefix blank so a stacked // `FMT_NORETURN FMT_API void f(…)` reduces to the `MACRO Ret name(` shape // it matches (the _API token is already spaces by then). let blanked = blankLoneMacroLines( blankCLeadingAttrMacros( blankCppAnnotationMacroCalls( blankCppInlineAnnotationMacros( blankCppApiPrefixMacros(blankCppInlineMacros(blankCppExportMacros(source))) ) ) ) ); const lower = filePath ? filePath.toLowerCase() : ''; if (lower.endsWith('.metal')) { blanked = blankMetalAttributes(blanked); } else if (lower.endsWith('.cu') || lower.endsWith('.cuh') || looksLikeCudaSource(source)) { blanked = blankCudaConstructs(blanked); } return restoreDirectiveLines(source, blanked); } /** * Blank an unknown attribute macro sitting in front of a C function * definition's return type: `SEC_ATTR UINT32 LostName(VOID) { … }` (macro * wrapping `__attribute__((…))`, common in embedded/kernel C). tree-sitter's * C grammar reads the macro as the declaration's type, the real return type * as the declarator, and stores the PARAMETER LIST as the function name — * `LostName` indexes as `"(VOID)"` and is unfindable (#1211). The C++ grammar * recovers this shape differently (glued name, salvaged post-hoc by * `recoverMangledCppName`), but in C the real name never reaches the mangled * string, so only a pre-parse blank can recover it. * * Attribute macros are project-specific (`SEC_ATTR`, `INIT_TEXT`, …), so this * keys on structure, not a curated list, matched tightly: * - line-leading (`^[ \t]*`) — declaration position, never an expression use; * - ALL-CAPS token of ≥3 chars (`[A-Z][A-Z0-9_]{2,}`) — ordinary C types in * definitions are rarely spelled this way, and when they are (`UINT32 f()`) * they're followed by ONE identifier + `(`, which the lookahead rejects; * - followed by TWO identifier tokens (return type, then name — `*` allowed * for pointer returns) and then `(` — i.e. exactly the * `MACRO Ret name(` definition shape. `MACRO name(` calls, `#define` * lines (start with `#`), and multi-word builtin returns * (`MACRO unsigned int f(` — where the C grammar already keeps the name) * are all left untouched. * Equal-length spaces preserve every byte offset, like the C++ blanks above. */ const C_LEADING_ATTR_MACRO_RE = /^([ \t]*)([A-Z][A-Z0-9_]{2,})(?=\s+[A-Za-z_]\w*[\s*]+[A-Za-z_]\w*\s*\()/gm; export function blankCLeadingAttrMacros(source: string): string { return source.replace( C_LEADING_ATTR_MACRO_RE, (_m, ws: string, macro: string) => ws + ' '.repeat(macro.length) ); } /** * Blank the body of `#ifdef __cplusplus … #endif` guard regions in C sources. * The ubiquitous C-header compatibility idiom * * #ifdef __cplusplus * extern "C" { * #endif * * is NOT valid C — `extern "C" {` (and any other C++-only line under the * guard) drops tree-sitter-c into error recovery, so effectively every public * C header carries parse errors. The wasm path shrugs (recovery keeps the * rest); the kernel path defers EVERY erroring file to wasm by policy — so * this one idiom pushed C-header deferral to ~32% on redis (vs the <10% * gate) and forfeited the native-parse win exactly where C repos have the * most files. A C compiler never sees the guarded lines (`__cplusplus` is * only defined for C++), so blanking the region BODY mirrors the * preprocessor's own view of the file. * * Matched conservatively, line-based and offset-preserving: * - the opener must be `#ifdef __cplusplus` / `#if defined(__cplusplus)`; * - the body may contain NO other preprocessor directive (a nested `#if`, * `#else`, or `#define` bails the whole region — those need real * preprocessing, so the file keeps its current behavior); * - the region must close with `#endif` within a few lines (guards are * tiny; a giant region is something else). * The `#ifdef`/`#endif` directive lines themselves are kept — an empty * preproc_ifdef parses clean — and every blanked byte becomes a space with * `\r` preserved, so offsets, lines, and columns survive on CRLF checkouts. */ const C_CPLUSPLUS_GUARD_OPEN_RE = /^[ \t]*#[ \t]*(?:ifdef[ \t]+__cplusplus\b|if[ \t]+defined[ \t]*\(?[ \t]*__cplusplus[ \t]*\)?)/; const C_PREPROC_DIRECTIVE_RE = /^[ \t]*#/; const C_PREPROC_ENDIF_RE = /^[ \t]*#[ \t]*endif\b/; const C_CPLUSPLUS_GUARD_MAX_BODY_LINES = 40; export function blankCCplusplusGuardBodies(source: string): string { if (source.indexOf('__cplusplus') === -1) return source; const lines = source.split('\n'); const stripCr = (l: string): string => (l.endsWith('\r') ? l.slice(0, -1) : l); let changed = false; for (let i = 0; i < lines.length; i++) { if (!C_CPLUSPLUS_GUARD_OPEN_RE.test(stripCr(lines[i] as string))) continue; let end = -1; for (let j = i + 1; j < lines.length && j - i - 1 <= C_CPLUSPLUS_GUARD_MAX_BODY_LINES; j++) { const line = stripCr(lines[j] as string); if (C_PREPROC_ENDIF_RE.test(line)) { end = j; break; } if (C_PREPROC_DIRECTIVE_RE.test(line)) break; // nested directive — bail } if (end < 0) continue; for (let k = i + 1; k < end; k++) { lines[k] = (lines[k] as string).replace(/[^\r]/g, ' '); } changed = true; i = end; } return changed ? lines.join('\n') : source; } /** * Blank a C iterator-macro call in STATEMENT position — `ql_foreach(iter, * &arena->tcache_ql, link) { … }` (jemalloc), `for_each_string_list_item(item, * &list) { … }` (git), `list_for_each_entry(pos, head, member) { … }` (the * Linux kernel's core iteration idiom). A call followed by a brace block is * not a C statement, so tree-sitter-c drops into error recovery at every use — * these macros are the single largest source of parse errors in macro-heavy C * trees (git: ~39% of files error; the kernel path defers each one to wasm). * Blanking JUST the macro call leaves the brace block as a bare compound * statement — valid C — so the body's calls/locals extract normally on both * arms instead of riding error recovery. * * C-ONLY, and matched tightly: * - the call must be INDENTED (statement position; file-scope definitions * start at column 0, and an unbraced file-scope `name(args) { }` is a * valid implicit-int function definition that must not be touched); * - lowercase-led identifier (iterator macros are lowercase by convention; * this also excludes constructors if the file is really C++) that is not a * control keyword; * - the parens balance ON the line (string literals skipped), and after * them only `{` or end-of-line may follow — a `;` (a real call statement), * an operator, or any other token disqualifies; * - when the line ends at `)`, the NEXT non-blank line must begin with `{`. * C++ deliberately does NOT get this pass: an indented snake_case * constructor (`basic_string_view(const Char* s) : … {`) is exactly this * shape, and blanking it would corrupt every STL-style class. */ const C_STMT_MACRO_KEYWORDS = new Set([ 'if', 'while', 'for', 'switch', 'return', 'do', 'else', 'sizeof', ]); export function blankCStatementMacroCalls(source: string): string { const lines = source.split('\n'); let changed = false; const content = (l: string): string => l.replace(/\r$/, '').trim(); for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; const m = /^[ \t]+([a-z_][a-z0-9_]*)[ \t]*\(/.exec(line); if (!m || C_STMT_MACRO_KEYWORDS.has(m[1] as string)) continue; const open = line.indexOf('(', m[0].length - 1); let depth = 0; let close = -1; for (let k = open; k < line.length; k++) { const ch = line[k]; if (ch === '"' || ch === "'") { const quote = ch; k++; while (k < line.length && line[k] !== quote) { if (line[k] === '\\') k++; k++; } continue; } if (ch === '(') depth++; else if (ch === ')') { depth--; if (depth === 0) { close = k; break; } } } let endLine = i; if (close < 0) { // Parens don't balance on the head line — the kernel wraps iterator // macros (`hlist_for_each_entry_rcu(p, head, hlist,\n\t\t // lockdep_is_held(&kprobe_mutex)) {`). Continue the same // string-skipping paren scan over a few continuation lines. A `;` or // a brace anywhere in the span means a real statement or compound // literal — bail (missing a blank is safe; corrupting one is not). if (line.indexOf(';') !== -1) continue; for (let j = i + 1; j <= i + 5 && j < lines.length && close < 0; j++) { const cont = lines[j] as string; let bail = false; for (let k = 0; k < cont.length; k++) { const ch = cont[k]; if (ch === '"' || ch === "'") { const quote = ch; k++; while (k < cont.length && cont[k] !== quote) { if (cont[k] === '\\') k++; k++; } continue; } if (ch === ';' || ch === '{' || ch === '}') { bail = true; break; } if (ch === '(') depth++; else if (ch === ')') { depth--; if (depth === 0) { close = k; endLine = j; break; } } } if (bail) break; } if (close < 0) continue; } const endLineStr = lines[endLine] as string; const after = endLineStr.slice(close + 1).replace(/\r$/, '').trim(); if (after === '') { // Brace on the next line (`ql_foreach(…)\n{`) or a brace-less // single-statement body (`for_each_subsys(ss, i)\n\tstmt;` — blanking // leaves the bare statement, valid C). A next line starting with an // operator/string/`;` is an expression continuation — bail. let next = endLine + 1; while (next < lines.length && content(lines[next] as string) === '') next++; if (next >= lines.length) continue; const first = content(lines[next] as string)[0]; if (!first || !/[A-Za-z_{]/.test(first)) continue; } else if (after !== '{') { continue; } const identStart = line.indexOf(m[1] as string); if (endLine === i) { lines[i] = line.slice(0, identStart) + ' '.repeat(close + 1 - identStart) + line.slice(close + 1); } else { lines[i] = line.slice(0, identStart) + line.slice(identStart).replace(/[^\r]/g, ' '); for (let j = i + 1; j < endLine; j++) { lines[j] = (lines[j] as string).replace(/[^\r]/g, ' '); } lines[endLine] = endLineStr.slice(0, close + 1).replace(/[^\r]/g, ' ') + endLineStr.slice(close + 1); i = endLine; // the blanked span can't host another head } changed = true; } return changed ? lines.join('\n') : source; } /** * Blank a lowercase compiler-annotation word SANDWICHED between a storage * class and the rest of a declaration — `static notrace void tick(…)`, * `static nokprobe_inline void arm(…)` (kernel compiler.h markers). The * dunder word-list can't carry these bare-word forms: `notrace` is a * plausible identifier. The sandwich IS the guard — the token counts only * when directly preceded by `static`/`extern`/`inline` AND followed by * another word, a position where it cannot be a variable name (an archaic * implicit-int `static notrace = 1;` fails the following-word requirement). * C-only. */ const C_SANDWICHED_ANNOTATIONS = [ 'noinline_for_stack', 'nokprobe_inline', 'noinline', 'notrace', 'noinstr', ] as const; const C_SANDWICH_RE = new RegExp( `\\b(static|extern|inline)([ \\t]+)(${C_SANDWICHED_ANNOTATIONS.join('|')})\\b(?=[ \\t]+[A-Za-z_])`, 'g' ); export function blankCSandwichedAnnotations(source: string): string { C_SANDWICH_RE.lastIndex = 0; if (!C_SANDWICH_RE.test(source)) return source; C_SANDWICH_RE.lastIndex = 0; return source.replace( C_SANDWICH_RE, (_m, storage: string, ws: string, ann: string) => storage + ws + ' '.repeat(ann.length) ); } /** * Blank a C23 `auto` type-inference keyword — `auto hb = hbr.hb;` (the * futex code; tree-sitter-c predates C23 auto and errors the enclosing * function). The old storage-class reading (`auto int x = 1;`) has a TYPE * between `auto` and the `=` and is untouched — the match requires * `auto IDENT =` directly, which only the C23 form exhibits. Blanking * leaves a plain assignment statement. C-only. */ const C_AUTO_INFER_RE = /\bauto(?=[ \t]+[A-Za-z_]\w*[ \t]*=)/g; export function blankCAutoInference(source: string): string { C_AUTO_INFER_RE.lastIndex = 0; if (!C_AUTO_INFER_RE.test(source)) return source; C_AUTO_INFER_RE.lastIndex = 0; return source.replace(C_AUTO_INFER_RE, () => ' '); } /** * Blank a trailing parameter-attribute macro — `int argc UNUSED,` / * `struct repository *repo UNUSED)` — git's house style for * `__attribute__((unused))` on nearly every callback parameter (and the same * shape as `MAYBE_UNUSED`/`G_GNUC_UNUSED` elsewhere). tree-sitter-c can't * parse a second identifier after the parameter name, so every such * SIGNATURE drops into error recovery — the single largest deferral bucket * on git (~150 files). Blanking the macro leaves an ordinary parameter. * * Matched tightly: an identifier, whitespace, then an ALL-CAPS ≥3-char token * immediately before `,` or `)`. Two juxtaposed identifiers in that position * have no other valid-C reading — in a CALL the would-be macro is preceded * by `,`/`(`, an operator, or a literal, never by a bare identifier. C-only: * C++ grammars accept more juxtapositions (user-defined suffixes, macro'd * `final`/`override`), so cpp keeps its existing recovery there. */ const C_TRAILING_PARAM_ATTR_RE = /\b([A-Za-z_]\w*)([ \t]+)([A-Z][A-Z0-9_]{2,})(?=[ \t]*[,)])/g; export function blankCTrailingParamAttrMacros(source: string): string { if (!C_TRAILING_PARAM_ATTR_RE.test(source)) { C_TRAILING_PARAM_ATTR_RE.lastIndex = 0; return source; } C_TRAILING_PARAM_ATTR_RE.lastIndex = 0; return source.replace( C_TRAILING_PARAM_ATTR_RE, (_m, name: string, ws: string, macro: string) => name + ws + ' '.repeat(macro.length) ); } /** * Blank the Linux-kernel/sparse declaration-annotation macros — `static int * __init audit_init(void)`, `void __user *buf`, `__bpf_kfunc void f(…)`, * `int x __ro_after_init;`. These lowercase double-underscore annotations sit * between storage/type tokens and the declarator, a position tree-sitter-c * can't reconcile, and they blanket the Linux tree: measured on the kernel's * own `kernel/` + `mm/` subtrees, they are the largest single deferral cause * (the `__init` family alone heads ~37% of erroring files). * * A structural match is IMPOSSIBLE here: `__u32 count` (a real typedef) and * `__init foo` (an annotation) are byte-shape identical — so unlike the * shape-keyed blanks above, this is a CURATED list (the CPP_INLINE_MACROS * precedent) of well-known sparse/section/compiler annotations that are * reserved-namespace macros in every codebase that spells them. Whole-word, * equal-length spaces, C-only (the C++ grammar's kernel exposure is * negligible and cpp keeps its narrower blank set). */ const C_KERNEL_ANNOTATIONS = [ '__init', '__exit', '__initdata', '__initconst', '__exitdata', '__devinit', '__devexit', '__cpuinit', '__meminit', '__meminitdata', '__net_init', '__net_exit', '__net_initdata', '__init_or_module', '__user', '__kernel', '__iomem', '__percpu', '__rcu', '__force', '__nocast', '__must_check', '__maybe_unused', '__always_unused', '__used', '__cold', '__hot', '__weak', '__pure', '__sched', '__malloc', '__visible', '__deprecated', '__ro_after_init', '__read_mostly', '__refdata', '__latent_entropy', '__randomize_layout', '__no_randomize_layout', '__bpf_kfunc', '__function_aligned', '__always_inline', '__noreturn', // Round-2 additions, each measured heading first-error lines on the // v7.2-rc2 kernel/+mm/ census (2026-07-17): cacheline placement, sparse // lock/section markers, and bpf/typecheck annotations. Real dunder TYPES // (`__u32`, `__s64`) and operators (`__alignof__`) are deliberately absent. '__cacheline_aligned_in_smp', '__cacheline_aligned', '__cacheline_internodealigned_in_smp', '____cacheline_aligned_in_smp', '____cacheline_aligned', '____cacheline_internodealigned_in_smp', '__noclone', '__lockfunc', '__ref', '__private', '__bitwise', '__nosavedata', '__no_kcsan', '__cpuidle', '__ksym', '__initdata_memblock', '__initdata_or_meminfo', ] as const; // `(?!\s*\()` keeps the parameterized annotations (`__printf(1, 2)`, // `__aligned(8)`, `__section("x")`) intact — blanking just their name would // strand the argument list as a floating parenthesis and CREATE an error. const C_KERNEL_ANNOTATION_RE = new RegExp( `\\b(${[...C_KERNEL_ANNOTATIONS].sort((a, b) => b.length - a.length).join('|')})\\b(?!\\s*\\()`, 'g' ); export function blankCKernelAnnotations(source: string): string { if (source.indexOf('__') === -1) return source; C_KERNEL_ANNOTATION_RE.lastIndex = 0; if (!C_KERNEL_ANNOTATION_RE.test(source)) return source; let out = source.replace(C_KERNEL_ANNOTATION_RE, (m) => ' '.repeat(m.length)); // `container_of(ptr, struct T, member)` — the type-keyword argument is the // one call shape tree-sitter-c cannot read (a macro taking a TYPE), and it // is pervasive across the Linux tree. Blanking just the `struct`/`union` // keyword leaves `container_of(ptr, T, member)` — a plain // identifier argument the grammar parses natively. Keyed to the macro name // so no other `struct` keyword anywhere is ever touched. if (out.indexOf('container_of') !== -1) { out = out.replace( /(\bcontainer_of\s*\([^;()]*?,\s*)(struct|union)(\s+)/g, (_m, head: string, kw: string, ws: string) => head + ' '.repeat(kw.length) + ws ); } return out; } /** * Blank the PARAMETERIZED sparse/compiler annotations whole — name AND * argument list — `struct file *f __free(fput) = NULL;`, `static void * __printf(4, 0) log_it(…)`, `} owners[] __counted_by(count);`, * `__bpf_md_ptr(struct bpf_iter_meta *, meta);`. The word-list blank above * deliberately skips these via `(?!\s*\()` because blanking just the NAME * strands `(args)` as a floating parenthesis — so every such file kept * deferring (the v7.2-rc2 kernel/+mm/ census puts `__free`/`__printf`/ * `__counted_by`/`__aligned` at the head of 39 first-error lines). Blanking * the whole `__name(args)` span leaves an ordinary declaration. * * CURATED for the same reason as the word list: `__aligned(8)` (annotation) * and `__hash(key)` (a real static helper call) are byte-shape identical, so * only reserved-namespace names that are annotations in every codebase that * spells them are listed. One nesting level of parens is allowed * (`__aligned(sizeof(struct x))`); newlines inside the span survive so byte * offsets are preserved. C-only. */ const C_PARAMETERIZED_ANNOTATIONS = [ '__free', '__printf', '__scanf', '__counted_by', '__counted_by_le', '__counted_by_be', '__guarded_by', '__pt_guarded_by', '__acquires', '__releases', '__must_hold', '__cleanup', '__aligned', '__section', '__bpf_md_ptr', '__assume_aligned', ] as const; const C_PARAM_ANNOTATION_RE = new RegExp( `\\b(${[...C_PARAMETERIZED_ANNOTATIONS].sort((a, b) => b.length - a.length).join('|')})[ \\t]*\\((?:[^()]|\\([^()]*\\))*\\)`, 'g' ); export function blankCParameterizedAnnotationMacros(source: string): string { if (source.indexOf('__') === -1) return source; C_PARAM_ANNOTATION_RE.lastIndex = 0; if (!C_PARAM_ANNOTATION_RE.test(source)) return source; C_PARAM_ANNOTATION_RE.lastIndex = 0; let result = ''; let last = 0; let m: RegExpExecArray | null; while ((m = C_PARAM_ANNOTATION_RE.exec(source)) !== null) { const start = m.index; let end = start + (m[0] as string).length; // Field-position form — `__bpf_md_ptr(struct bpf_iter_meta *, meta);` as // the whole line: a lone `;` field is ITSELF a parse error (measured; // an empty struct body is fine), so the semicolon blanks with it. A // mid-line match (`… __free(fput) = NULL;`) keeps its statement tail. const lineStart = source.lastIndexOf('\n', start - 1) + 1; if (/^[ \t]*$/.test(source.slice(lineStart, start))) { const after = /^[ \t]*;/.exec(source.slice(end)); if (after) end += after[0].length; } result += source.slice(last, start) + source.slice(start, end).replace(/[^\n\r]/g, ' '); last = end; } return result + source.slice(last); } /** * Blank a bare `struct`/`union`/`enum` TYPE-keyword argument inside a macro * call — `kzalloc_obj(struct bpf_mount_opts)`, `alloc_percpu(struct irqstat)`, * `list_first_entry(&pending,\n\t\tstruct async_entry, domain_list)` — the * `container_of` disease generalized (that blank stays as the keyed * precedent). A bare type is never a valid C expression argument, so * tree-sitter drops into error recovery at every such call; removing just the * keyword leaves a plain identifier argument the grammar parses natively. * This single shape heads ~35 first-error lines on the kernel/+mm/ census * (kzalloc_obj/list_entry/alloc_percpu + multi-line continuations). * * Guarded three ways, because `f(struct T)` has VALID look-alikes: * - head exclusions: `sizeof(struct T)` / `offsetof(struct T, m)` / * `_Generic(x, struct T *: …)` all parse natively (probed) and must keep * their keyword; `va_arg` gets its own dedicated blank below. * - call-vs-declaration: in `int wf(struct a, struct b);` (a wrapped * prototype — valid C) the head is preceded by a TYPE, so any identifier * or `*` before the head rejects the match — except the word `return`, * which precedes real calls. Newline/`=`/`,`/`(`/`;`/`{`/`>` etc. accept. * - the keyword must OPEN a top-level argument and the argument must be * exactly `struct T` (optionally `struct T *`, whose stars blank too — * `DEFINE_PER_CPU(struct task_struct *, ksoftirqd)` leaves two plain * identifier arguments): a cast (`f((struct T *)p)`) opens a nested * group, and a declaration argument (`TP_PROTO(struct foo *bar)`) trails * an extra identifier — neither matches. * A hand-rolled bounded scanner rather than a nested-alternation regex: * preceding args may themselves contain calls * (`hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(&d->h)),\n * struct bpf_dtab_netdev, index_hlist)`), and lazy nested regex groups over * untrusted repo text invite catastrophic backtracking. C-only. */ const C_TYPE_ARG_HEAD_EXCLUSIONS = new Set([ 'sizeof', 'alignof', '_Alignof', 'typeof', '__typeof__', '__typeof', 'offsetof', '_Generic', 'va_arg', 'if', 'while', 'for', 'switch', 'case', ]); const C_TYPE_ARG_HEAD_RE = /\b([A-Za-z_]\w*)[ \t]*\(/g; const C_TYPE_ARG_OPENER_RE = /^(struct|union|enum)([ \t\r\n]+)([A-Za-z_]\w*)([ \t\r\n]*\*+)?(?=[ \t\r\n]*[,)])/; const C_TYPE_ARG_SCAN_CAP = 600; export function blankCTypeKeywordArgs(source: string): string { if (!/\b(?:struct|union|enum)[ \t\r\n]/.test(source)) return source; let chars: string[] | null = null; C_TYPE_ARG_HEAD_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = C_TYPE_ARG_HEAD_RE.exec(source)) !== null) { const head = m[1] as string; if (C_TYPE_ARG_HEAD_EXCLUSIONS.has(head)) continue; // Reject declaration shapes: an identifier or `*` (a return/field type) // immediately before the head — unless that word is `return`. let j = m.index - 1; while (j >= 0 && (source[j] === ' ' || source[j] === '\t')) j--; const prev = j >= 0 ? (source[j] as string) : ''; if (/[\w*]/.test(prev)) { let w = j; while (w >= 0 && /\w/.test(source[w] as string)) w--; if (source.slice(w + 1, j + 1) !== 'return') continue; } const open = m.index + (m[0] as string).length - 1; let depth = 1; let atArgStart = true; for (let k = open + 1; k < source.length && k - open <= C_TYPE_ARG_SCAN_CAP; k++) { const ch = source[k] as string; if (ch === '"' || ch === "'") { const quote = ch; k++; while (k < source.length && source[k] !== quote) { if (source[k] === '\\') k++; k++; } atArgStart = false; continue; } if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue; if (ch === '(') { depth++; atArgStart = false; continue; } if (ch === ')') { depth--; if (depth === 0) break; atArgStart = false; continue; } if (ch === ';' || ch === '{' || ch === '}') break; // not an argument list if (ch === ',') { if (depth === 1) atArgStart = true; continue; } if (depth === 1 && atArgStart) { const opener = C_TYPE_ARG_OPENER_RE.exec(source.slice(k, k + 160)); if (opener) { chars ??= [...source]; const kw = opener[1] as string; for (let b = k; b < k + kw.length; b++) chars[b] = ' '; if (opener[4]) { const tail = opener[4] as string; const stars = (/\*+$/.exec(tail) as RegExpExecArray)[0].length; const starsStart = k + kw.length + (opener[2] as string).length + (opener[3] as string).length + (tail.length - stars); for (let b = starsStart; b < starsStart + stars; b++) chars[b] = ' '; } k += (opener[0] as string).length - 1; } } atArgStart = false; } } return chars ? chars.join('') : source; } /** * Blank a file-scope `static`/`extern`-prefixed ALL-CAPS declaration macro — * `static DEFINE_PER_CPU(struct llist_head, rstat_backlog_list);`, * `static DECLARE_WORK(init_free_wq, do_free_init);`, * `static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);` — the single * largest deferral family on the kernel/+mm/ census (~45 first-error lines * across the DEFINE_/DECLARE_ variants). A storage-class specifier followed * by a call expression is never valid C, so every one errors; the whole line * blanks to spaces (the macro-declared variable was invisible to extraction * anyway, and the file's remaining symbols recover). * * The UNPREFIXED form (`EXPORT_SYMBOL(x);`, `DEFINE_MUTEX(lock);` at column * 0) parses natively as a K&R implicit-int declaration (probed) — extraction * ignores declarations, so those lines are left alone; the type-keyword-arg * blank above already recovers the `DEFINE_PER_CPU(struct T, x);` bare form. * Matched tightly: `static`/`extern` first on the line (indentation allowed — * `static DEFINE_RATELIMIT_STATE(…)` appears at BLOCK scope too, and the * storage-class-plus-call shape is invalid at every scope), CAPS macro name, * parens balancing ON the line (string literals skipped), then exactly `;` * to end of line. Initializer forms (`… ) = { …`) are deliberately not * matched — blanking the head would strand the brace block. C-only. */ const C_PREFIXED_DECL_MACRO_RE = /^[ \t]*(?:static|extern)[ \t]+[A-Z][A-Z0-9_]{2,}[ \t]*\(/; export function blankCFileScopePrefixedDeclMacros(source: string): string { if (!/^[ \t]*(?:static|extern)[ \t]+[A-Z]/m.test(source)) return source; const lines = source.split('\n'); let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; const m = C_PREFIXED_DECL_MACRO_RE.exec(line); if (!m) continue; const open = line.indexOf('(', m[0].length - 1); let depth = 0; let close = -1; for (let k = open; k < line.length; k++) { const ch = line[k]; if (ch === '"' || ch === "'") { const quote = ch; k++; while (k < line.length && line[k] !== quote) { if (line[k] === '\\') k++; k++; } continue; } if (ch === '(') depth++; else if (ch === ')') { depth--; if (depth === 0) { close = k; break; } } } if (close < 0) continue; // spans lines — leave for a future round if (line.slice(close + 1).replace(/\r$/, '').trim() !== ';') continue; lines[i] = line.replace(/[^\n\r]/g, ' '); changed = true; } return changed ? lines.join('\n') : source; } /** * REWRITE (the one non-blank pass in this family — it moves a token) a * 2-argument `static CAPS_MACRO(type, name) = {`-style initialized * declaration macro into the declaration it expands to — `static * DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {` becomes `static * struct cpuhp_cpu_state cpuhp_state) …` → `static struct * cpuhp_cpu_state` + padding + `cpuhp_state` + padding + `= {`. The blank * family can't help here: blanking the head strands the brace block, and * dropping the whole span would discard the initializer's function * references (`.startup.single = bringup_cpu` — real cFnPtr wiring). The * NAME keeps its exact original column and the `= {` tail keeps its exact * offsets (`d`-flag group indices); only the TYPE token sits left of where * the macro name was, and type tokens contribute strings, not positions. * Only the two-argument `(, )` form with an `=` tail * matches; three-argument macros and `;`-terminated forms (the blank above) * never do. C-only. */ const C_PREFIXED_DECL_MACRO_INIT_RE = /^([ \t]*)static[ \t]+[A-Z][A-Z0-9_]{2,}[ \t]*\(([^();'"]*?),[ \t]*([A-Za-z_]\w*)[ \t]*\)([ \t]*=[ \t]*\{?[ \t]*\r?)$/d; export function rewriteCPrefixedDeclMacroInitializers(source: string): string { if (!/^[ \t]*static[ \t]+[A-Z]/m.test(source)) return source; const lines = source.split('\n'); let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; const m = C_PREFIXED_DECL_MACRO_INIT_RE.exec(line); if (!m) continue; const indices = (m as RegExpExecArray & { indices: Array<[number, number]> }).indices; const arg1 = (m[2] as string).trim().replace(/[ \t]+/g, ' '); if (!/^[A-Za-z_][\w \t*]*$/.test(arg1)) continue; // a type-token run only const name = m[3] as string; const nameStart = (indices[3] as [number, number])[0]; const tailStart = (indices[4] as [number, number])[0]; const prefix = (m[1] as string) + 'static ' + arg1; if (prefix.length + 1 > nameStart) continue; // rewrite must fit left of the name lines[i] = prefix + ' '.repeat(nameStart - prefix.length) + name + ' '.repeat(tailStart - nameStart - name.length) + line.slice(tailStart); changed = true; } return changed ? lines.join('\n') : source; } /** * Blank a QUALIFIED/POINTER type argument of `va_arg` — `va_arg(ap, const * char *)`, `va_arg(args, unsigned long)`. tree-sitter-c parses the * single-token forms (`va_arg(ap, int)`, `va_arg(ap, foo_t)`) natively * (probed), but multi-token type descriptors error the whole enclosing * function. Blanking the comma and the entire second argument leaves * `va_arg(ap )` — a one-argument call the grammar accepts. * Function-pointer types (`va_arg(ap, void (*)(int))`) contain parens and * are deliberately unmatched. C-only. */ const C_VA_ARG_RE = /\bva_arg[ \t]*\(([^(),]+)(,[^()]*)\)/g; export function blankCVaArgQualifiedTypeArgs(source: string): string { if (source.indexOf('va_arg') === -1) return source; C_VA_ARG_RE.lastIndex = 0; return source.replace(C_VA_ARG_RE, (m, arg1: string, rest: string) => { if (/^,[ \t]*[A-Za-z_]\w*[ \t]*$/.test(rest)) return m; // single token — parses natively return `va_arg(${arg1}${rest.replace(/[^\n\r]/g, ' ')})`; }); } /** * Blank the DOTS of a GNU NAMED-variadic function-like `#define` parameter — * `#define verbose(env, fmt, args...) …` → `#define verbose(env, fmt, * args ) …`. The `ident...` parameter form (unlike standard `...`) errors * the DIRECTIVE itself (measured — and so does trailing whitespace after a * bare `#define NAME`, which is why the tail is NOT blanked wholesale); with * just the dots blanked the params parse as ordinary identifiers and the * body (`##args` included — the preproc body is opaque to the grammar) is * untouched. `restoreDirectiveLines` exists to keep directives out of the * other blanks' blast radius, so this pass runs AFTER the restore and edits * the directive deliberately. Multi-line bodies are fine — the params always * sit on the `#define` line. C-only (the census hits are the kernel's bpf * verifier headers). */ const C_NAMED_VARIADIC_DEFINE_RE = /^([ \t]*#[ \t]*define[ \t]+[A-Za-z_]\w*\([^()\n]*?\b\w+)\.\.\./; export function blankCNamedVariadicDefineDots(source: string): string { if (source.indexOf('...') === -1) return source; const lines = source.split('\n'); let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; const m = C_NAMED_VARIADIC_DEFINE_RE.exec(line); if (!m) continue; const keep = m[1] as string; lines[i] = keep + ' ' + line.slice(keep.length + 3); changed = true; } return changed ? lines.join('\n') : source; } /** C source pre-processing: neutralize `#ifdef __cplusplus` compat-guard * bodies (invisible to a C compiler; `extern "C" {` otherwise errors every * public header), blank declaration-markup macro calls and lone macro lines * (`REDIS_NO_SANITIZE("bounds")` before a definition, jemalloc's diagnostic * toggles — the same structural shapes the C++ side already blanks), recover * functions hidden behind a leading attribute macro (#1211), then — for * C-detected headers in CUDA projects (llm.c keeps `__device__` helpers and * kernel prototypes in plain `.h`) — the same content-gated CUDA blank as * C++. Offset-preserving. */ function preParseCSource(source: string): string { const inner = blankCKernelAnnotations(blankCCplusplusGuardBodies(source)); let blanked = blankCLeadingAttrMacros( blankLoneMacroLines( blankCStatementMacroCalls( blankCTrailingParamAttrMacros( blankCppAnnotationMacroCalls( rewriteCPrefixedDeclMacroInitializers( blankCFileScopePrefixedDeclMacros( blankCVaArgQualifiedTypeArgs( blankCTypeKeywordArgs( blankCParameterizedAnnotationMacros( blankCAutoInference(blankCSandwichedAnnotations(inner)) ) ) ) ) ) ) ) ) ) ); if (looksLikeCudaSource(blanked)) blanked = blankCudaConstructs(blanked); // The named-variadic `#define` pass runs AFTER the directive restore — it // deliberately edits directive lines (see its doc comment). return blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked)); } export const cppExtractor: LanguageExtractor = { // Recover macro-annotated class/struct definitions (`class MYMODULE_API Foo : Base`, // #1061/#946) and macro-prefixed functions (`FORCEINLINE FString Foo()`, #1093 // follow-up) that tree-sitter otherwise misparses. preParse: preParseCppSource, // Universal net for any macro the curated blank list misses. recoverMangledName: recoverMangledCppName, functionTypes: ['function_definition'], classTypes: ['class_specifier'], // A bodiless `class_specifier` is a forward declaration (`class Foo;`) or an // elaborated type reference, not a definition. Skip it so dozens of forward // decls across headers don't mint phantom `class` nodes that crowd out — and // get picked as the blast-radius representative over — the single real // definition, exactly as bodiless struct/enum specifiers are already skipped. (#1093) skipBodilessClass: true, methodTypes: ['function_definition'], interfaceTypes: [], structTypes: ['struct_specifier'], // C++ unions additionally carry member functions, which extract through the // same aggregate-body walk as structs while preserving their distinct kind. unionTypes: ['union_specifier'], enumTypes: ['enum_specifier'], enumMemberTypes: ['enumerator'], typeAliasTypes: ['type_definition', 'alias_declaration'], // typedef and using importTypes: ['preproc_include'], callTypes: ['call_expression'], variableTypes: ['declaration'], nameField: 'declarator', bodyField: 'body', paramsField: 'parameters', resolveName: extractCppQualifiedMethodName, getReceiverType: extractCppReceiverType, getReturnType: extractCppReturnType, getVisibility: (node) => { // Check for access specifier in parent const parent = node.parent; if (parent) { for (let i = 0; i < parent.childCount; i++) { const child = parent.child(i); if (child?.type === 'access_specifier') { const text = child.text; if (text.includes('public')) return 'public'; if (text.includes('private')) return 'private'; if (text.includes('protected')) return 'protected'; } } } return undefined; }, resolveTypeAliasKind: (node, _source) => { // C++ typedef: `typedef enum { ... } name;`, `typedef struct { ... } name;`, // or `typedef union { ... } name;` — see the C extractor. for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum'; if ( child.type === 'struct_specifier' && getChildByField(child, 'body') ) return 'struct'; if (child.type === 'union_specifier' && getChildByField(child, 'body')) return 'union'; } return undefined; }, isMisparsedFunction: (name, node) => { // C++ macros like NLOHMANN_JSON_NAMESPACE_BEGIN cause tree-sitter to misparse // namespace blocks as function_definitions (e.g. name = "namespace detail"). // Also filter C++ keywords that tree-sitter occasionally misinterprets as // function/method names (e.g. switch statements inside macro-confused scopes). if (name.startsWith('namespace')) return true; const cppKeywords = ['switch', 'if', 'for', 'while', 'do', 'case', 'return']; if (cppKeywords.includes(name)) return true; // `class MACRO Name : public Base { … }` misparses to a function_definition // named after the class. `blankCppExportMacros` (preParse) recovers the // common ALL-CAPS export-macro shape; this drop is the fallback for any // residual misparse it doesn't blank — still no phantom function (#1061/#946). return isMacroMisparsedTypeDecl(node); }, extractImport: (node, source) => { const importText = source.substring(node.startIndex, node.endIndex).trim(); // C++ includes: #include , #include "myheader.h" const systemLib = node.namedChildren.find((c: SyntaxNode) => c.type === 'system_lib_string'); if (systemLib) { return { moduleName: getNodeText(systemLib, source).replace(/^<|>$/g, ''), signature: importText }; } const stringLiteral = node.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal'); if (stringLiteral) { const stringContent = stringLiteral.namedChildren.find((c: SyntaxNode) => c.type === 'string_content'); if (stringContent) { return { moduleName: getNodeText(stringContent, source), signature: importText }; } } return null; }, };