fix(extraction): recover C++ function names prefixed by an inline-specifier macro (#1100)
* fix(extraction): recover C++ function names prefixed by an inline-specifier macro An unknown inline-specifier macro before a function's return type (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery: the macro was read as the return type and — for a non-primitive return — the return type was glued onto the name, so the function was indexed as `"FString GetName"` instead of `GetName`, unfindable by name and with no caller links. This is pervasive in Unreal Engine, where inline helpers are written `FORCEINLINE <ret> <name>(…)` (e.g. ALS's `FORCEINLINE FString GetEnumerationToString`). Add `blankCppInlineMacros`, a preParse that blanks the known UE inline macros (`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`) with equal-length spaces so byte offsets stay exact and the declaration parses as an ordinary function — recovering both the real name AND the return type. This is the same recover-don't-drop approach as blankCppExportMacros (#946/#1061), and the two are composed into the cppExtractor preParse. Matched tightly (exact known tokens, only in specifier position — followed by the identifier that starts the return type/name), so ordinary identifiers, real all-caps return types (`HRESULT DoIt()`), string literals, expression uses, and longer words (`FORCEINLINE_COUNT`) are untouched — verified by controls. C++-only; Kotlin/Scala re-index byte-for-byte identical. Five regression tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note C++ inline-specifier-macro function name fix (#1100) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
712a406726
commit
9b2ce1c8f6
@@ -11,7 +11,7 @@ import * as os from 'os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction';
|
||||
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
|
||||
import { stripCppTemplateArgs, blankCppExportMacros } from '../src/extraction/languages/c-cpp';
|
||||
import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros } from '../src/extraction/languages/c-cpp';
|
||||
import { normalizePath } from '../src/utils';
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -2928,6 +2928,60 @@ class APXCharacter { // the one real definition
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ macro-prefixed function names (#1093 follow-up)', () => {
|
||||
// An unknown inline-specifier macro before the return type
|
||||
// (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery:
|
||||
// the macro became the return type and — for a non-primitive return — the
|
||||
// return type was glued onto the name (`"FString GetName"`), so the function
|
||||
// was unfindable by name and its callers didn't link. `blankCppInlineMacros`
|
||||
// blanks the known UE inline macros before parsing (offset-preserving), the
|
||||
// same recover-don't-drop approach as the macro-annotated-class fix. Pervasive
|
||||
// in Unreal Engine (`FORCEINLINE`).
|
||||
const infoOf = (code: string) =>
|
||||
extractFromSource('m.cpp', code).nodes
|
||||
.filter((n) => n.kind === 'method' || n.kind === 'function')
|
||||
.map((n) => ({ name: n.name, ret: n.returnType }));
|
||||
|
||||
it('recovers the real name AND return type of a FORCEINLINE function', () => {
|
||||
expect(infoOf('static FORCEINLINE FString GetName(int V) { return H(V); }')).toEqual([
|
||||
{ name: 'GetName', ret: 'FString' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles the templated UE helper shape (GetEnumerationToString)', () => {
|
||||
const names = infoOf(
|
||||
'template <typename E> static FORCEINLINE FString GetEnumerationToString(const E V) { return H(V); }'
|
||||
).map((x) => x.name);
|
||||
expect(names).toContain('GetEnumerationToString');
|
||||
});
|
||||
|
||||
it('handles FORCENOINLINE / FORCEINLINE_DEBUGGABLE, methods, void, and reference returns', () => {
|
||||
expect(infoOf('FORCENOINLINE FString A(int V){return H(V);}').map((x) => x.name)).toContain('A');
|
||||
expect(infoOf('FORCEINLINE_DEBUGGABLE FString B(int V){return H(V);}').map((x) => x.name)).toContain('B');
|
||||
expect(infoOf('struct S { FORCEINLINE FString GetName(int V) { return H(V); } };').map((x) => x.name)).toContain('GetName');
|
||||
expect(infoOf('static FORCEINLINE void DoThing(int V) { H(V); }').map((x) => x.name)).toContain('DoThing');
|
||||
expect(infoOf('static FORCEINLINE const FString& GetRef(int V) { return H(V); }').map((x) => x.name)).toContain('GetRef');
|
||||
});
|
||||
|
||||
it('leaves ordinary functions and real all-caps return types untouched (controls)', () => {
|
||||
expect(infoOf('FString GetName(int V) { return H(V); }')).toEqual([{ name: 'GetName', ret: 'FString' }]);
|
||||
// A real all-caps type that is NOT a listed inline macro stays the return type.
|
||||
expect(infoOf('HRESULT DoIt(int V) { return H(V); }')).toEqual([{ name: 'DoIt', ret: 'HRESULT' }]);
|
||||
});
|
||||
|
||||
it('blankCppInlineMacros preserves offsets and only touches specifier-position macros', () => {
|
||||
// Blanked with equal-length spaces (byte offsets preserved).
|
||||
expect(blankCppInlineMacros('FORCEINLINE FString F()')).toBe(' FString F()');
|
||||
expect(blankCppInlineMacros('FORCEINLINE FString F()')).toHaveLength('FORCEINLINE FString F()'.length);
|
||||
// Not in specifier position → untouched: string literals, expressions,
|
||||
// longer word (`FORCEINLINE_COUNT`), and the fast path.
|
||||
expect(blankCppInlineMacros('const char* s = "FORCEINLINE";')).toBe('const char* s = "FORCEINLINE";');
|
||||
expect(blankCppInlineMacros('x = FORCEINLINE + 1;')).toBe('x = FORCEINLINE + 1;');
|
||||
expect(blankCppInlineMacros('int FORCEINLINE_COUNT = 3;')).toBe('int FORCEINLINE_COUNT = 3;');
|
||||
expect(blankCppInlineMacros('no macros here')).toBe('no macros here');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ templated base-class inheritance (#1043)', () => {
|
||||
// Inheriting from a template (`class D : public Base<int>`) recorded the base
|
||||
// ref as the full instantiation `Base<int>`, which never name-matched the
|
||||
|
||||
Reference in New Issue
Block a user