mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 07:02:59 +08:00
1643 lines
79 KiB
TypeScript
1643 lines
79 KiB
TypeScript
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<typename T> T Box<T>::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>` → `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<typename T> class
|
|
* Base { … }` is indexed as a node named `Base`, but a derived class
|
|
* `class D : public Base<int>` records its base as the full `Base<int>` (and
|
|
* `class Q : public ns::Tpl<int>` as `ns::Tpl<int>`) — 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<int>` → `Base`, `ns::Tpl<Foo<int>>` → `ns::Tpl`, and the rare
|
|
* `Outer<int>::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, `<anonymous>` 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 <stdio.h>, #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 <return-type> <name>(` — 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 <ret> <name>(…)`) 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 <return-type> <name>(` 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<void(float)>;`
|
|
* 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<<<grid, block, smem, stream>>>(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<T, 256><<<…>>>(…)` 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<<<dim3{1,1,1}, dim3{256,1,1}>>>(…)` 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 `(<type-ish>, <ident>)` 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 <iostream>, #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;
|
|
},
|
|
};
|