fix(cpp): resolve callers for typed pointer method calls (#445)
Resolves typed member-pointer method calls like `m_cpAlg->Processing()` so `codegraph callers CDetect::Processing` returns the expected callers.
- Extract C/C++ `field_expression` member calls as receiver-qualified references, so `ptr->method()` is preserved as a receiver-aware reference.
- Surface out-of-line C++ method definitions (`int CDetect::Processing() {...}` in `.cpp` with class in `.hpp`) as proper method nodes with the correct qualified identity.
- C++ receiver-type inference: declarator regex requires a terminator after the receiver (rules out matching `return m_cpAlg->...`), handles `Type*x`/`Type *x`/`Type* x` uniformly, and rejects C++ keywords as a final guard.
- `resolveMethodOnType` matches by `Class::method` qualified-name suffix, so out-of-line definitions across files resolve (typical `.hpp`/`.cpp` split).
Validated on bitcoin-core (1306 .cpp files): 38,180 → 40,503 cpp method incoming-call edges (+6.1%), deterministic across re-indexes. Regression test added for the ambiguous-name + `return ptr->m()` / `Type x = ptr->m()` patterns.
Closes #445
Co-authored-by: chenyuxuan <458254969@qq.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
chenyuxuan
Colby McHenry
Claude Opus 4.7
parent
72c08c2bef
commit
c0cf9c1e7d
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- **Watch sync no longer aborts with `FOREIGN KEY constraint failed`.** PR #62 plugged this FK violation at the extraction layer (empty-named nodes whose containment edges had no target), but the same violation kept reappearing on v0.9.5 during the daemon's *watch sync* — not on initial index. Once an agent's daemon had been running long enough to accumulate edits, a resolver lookup that crossed a framework-specific cache could hand back a node whose row had been removed by a recent file rewrite, and the FK check then aborted the entire resolution batch, leaving the user's daemon log filling with `Watch sync failed { error: 'FOREIGN KEY constraint failed' }`. `QueryBuilder.insertEdges` now validates every batch's endpoints against the `nodes` table directly (one fresh `SELECT id IN (...)` per batch, no cache) and silently skips edges with missing source or target — so a stale lookup result drops one edge instead of aborting the whole sync. Surfaces as a fresh `codegraph init`/`index` cycle now surviving its first watch-sync cycle without the FK error, and the daemon recovering naturally instead of compounding into further failures. Closes #455.
|
- **Watch sync no longer aborts with `FOREIGN KEY constraint failed`.** PR #62 plugged this FK violation at the extraction layer (empty-named nodes whose containment edges had no target), but the same violation kept reappearing on v0.9.5 during the daemon's *watch sync* — not on initial index. Once an agent's daemon had been running long enough to accumulate edits, a resolver lookup that crossed a framework-specific cache could hand back a node whose row had been removed by a recent file rewrite, and the FK check then aborted the entire resolution batch, leaving the user's daemon log filling with `Watch sync failed { error: 'FOREIGN KEY constraint failed' }`. `QueryBuilder.insertEdges` now validates every batch's endpoints against the `nodes` table directly (one fresh `SELECT id IN (...)` per batch, no cache) and silently skips edges with missing source or target — so a stale lookup result drops one edge instead of aborting the whole sync. Surfaces as a fresh `codegraph init`/`index` cycle now surviving its first watch-sync cycle without the FK error, and the daemon recovering naturally instead of compounding into further failures. Closes #455.
|
||||||
- **Hermes Agent: `codegraph install --target hermes` no longer corrupts `~/.hermes/config.yaml`.** Hermes serializes its config with PyYAML's default block style, which writes list items at the *same* indent as the parent mapping key (`cli:` and `- hermes-cli` both at column 2). The previous line-based YAML patcher mistook that first ` - hermes-cli` for the next sibling key, truncated the `cli:` block, and then spliced `- mcp-codegraph` at indent 4 *before* the existing items — leaving subsequent entries (`- browser`, `- clarify`, …) and even other platforms (`telegram:`, `discord:`) appearing at the `platform_toolsets:` level, which is no longer parseable YAML. The installer now recognizes the same-indent list style, finds the real end of the block at the next sibling key, and appends `- mcp-codegraph` at whatever indent the existing items already use. Re-installing on an already-corrupted file (or a 4-space-nested config that worked before) still produces a clean, parseable result. Closes #456.
|
- **Hermes Agent: `codegraph install --target hermes` no longer corrupts `~/.hermes/config.yaml`.** Hermes serializes its config with PyYAML's default block style, which writes list items at the *same* indent as the parent mapping key (`cli:` and `- hermes-cli` both at column 2). The previous line-based YAML patcher mistook that first ` - hermes-cli` for the next sibling key, truncated the `cli:` block, and then spliced `- mcp-codegraph` at indent 4 *before* the existing items — leaving subsequent entries (`- browser`, `- clarify`, …) and even other platforms (`telegram:`, `discord:`) appearing at the `platform_toolsets:` level, which is no longer parseable YAML. The installer now recognizes the same-indent list style, finds the real end of the block at the next sibling key, and appends `- mcp-codegraph` at whatever indent the existing items already use. Re-installing on an already-corrupted file (or a 4-space-nested config that worked before) still produces a clean, parseable result. Closes #456.
|
||||||
- **NestJS: `RouterModule.register([...])` route prefixes now propagate to controller routes.** Previously a controller declared inside a module wired through NestJS's `RouterModule` (a common pattern for modular apps with nested route prefixes) was indexed with its raw `@Controller(...) + @Get(...)` path — so `UsersController` under `RouterModule.register([{ path: 'admin', module: AdminModule, children: [{ path: 'users', module: UsersModule }] }])` showed up as `GET /` instead of `GET /admin/users`. The new cross-file pass walks every `*.module.{ts,js}` for `RouterModule.register/forRoot/forChild([...])` (recursive `children`) and `@Module({ controllers: [...] })`, then prepends the correct prefix to each affected route — including non-empty `@Controller` paths and method-level params (`/admin/users/:id`). The route node's `id` is preserved across the update so existing route→handler edges stay intact, and the pass is idempotent so incremental sync recovers when `app.module.ts` itself is edited. Closes #459.
|
- **NestJS: `RouterModule.register([...])` route prefixes now propagate to controller routes.** Previously a controller declared inside a module wired through NestJS's `RouterModule` (a common pattern for modular apps with nested route prefixes) was indexed with its raw `@Controller(...) + @Get(...)` path — so `UsersController` under `RouterModule.register([{ path: 'admin', module: AdminModule, children: [{ path: 'users', module: UsersModule }] }])` showed up as `GET /` instead of `GET /admin/users`. The new cross-file pass walks every `*.module.{ts,js}` for `RouterModule.register/forRoot/forChild([...])` (recursive `children`) and `@Module({ controllers: [...] })`, then prepends the correct prefix to each affected route — including non-empty `@Controller` paths and method-level params (`/admin/users/:id`). The route node's `id` is preserved across the update so existing route→handler edges stay intact, and the pass is idempotent so incremental sync recovers when `app.module.ts` itself is edited. Closes #459.
|
||||||
|
- **C++ callers now resolve through typed member pointers (#445/#454).** `codegraph callers CDetect::Processing` previously returned nothing for call sites like `m_cpAlg->Processing()` because (a) `field_expression` calls weren't kept as receiver-qualified references, (b) out-of-line method definitions (`int CDetect::Processing() {...}` in `.cpp` while the class is in `.hpp` — typical C++ layout) weren't surfaced as class methods, and (c) the resolver had no way to bridge a `Type* receiver` declaration to its method calls. Calls through typed object pointers, references, and stack values now resolve to the right class's method, including the common ambiguous-name case (two classes with a shared method name) and call sites embedded in `return ptr->m()` or `Type x = ptr->m()` forms. Measured on bitcoin-core (1306 .cpp files): +6.1% C++ caller edges resolved end-to-end.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Installer targets for Gemini CLI and the Antigravity IDE.** `codegraph install` (and the interactive prompt) now detect and configure two more agents out of the box:
|
- **Installer targets for Gemini CLI and the Antigravity IDE.** `codegraph install` (and the interactive prompt) now detect and configure two more agents out of the box:
|
||||||
|
|||||||
@@ -161,6 +161,111 @@ describe('C++ end-to-end — virtual override synthesis', () => {
|
|||||||
tmpDir = undefined;
|
tmpDir = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves callers through typed object pointers', async () => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-'));
|
||||||
|
let cg: CodeGraph | undefined;
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'detect.hpp'),
|
||||||
|
'class CDetect {\n' +
|
||||||
|
' public:\n' +
|
||||||
|
' int Processing();\n' +
|
||||||
|
'};\n' +
|
||||||
|
'class CDetector {\n' +
|
||||||
|
' private:\n' +
|
||||||
|
' CDetect* m_cpAlg = nullptr;\n' +
|
||||||
|
' public:\n' +
|
||||||
|
' int Run();\n' +
|
||||||
|
' int Flush();\n' +
|
||||||
|
'};\n'
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'detect.cpp'),
|
||||||
|
'#include "detect.hpp"\n' +
|
||||||
|
'int CDetector::Run() { return m_cpAlg->Processing(); }\n' +
|
||||||
|
'int CDetector::Flush() { return m_cpAlg->Processing(); }\n' +
|
||||||
|
'int CDetect::Processing() { return 0; }\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
cg = CodeGraph.initSync(tmpDir);
|
||||||
|
await cg.indexAll();
|
||||||
|
|
||||||
|
const processing = cg
|
||||||
|
.getNodesByKind('method')
|
||||||
|
.find((n) => n.qualifiedName.endsWith('CDetect::Processing'));
|
||||||
|
expect(processing).toBeDefined();
|
||||||
|
|
||||||
|
const callers = cg.getCallers(processing!.id).map((c) => c.node.qualifiedName);
|
||||||
|
expect(callers).toContain('CDetector::Run');
|
||||||
|
expect(callers).toContain('CDetector::Flush');
|
||||||
|
|
||||||
|
const runMethod = cg
|
||||||
|
.getNodesByKind('method')
|
||||||
|
.find((n) => n.qualifiedName.endsWith('CDetector::Run'));
|
||||||
|
expect(runMethod).toBeDefined();
|
||||||
|
const callees = cg.getCallees(runMethod!.id).map((c) => c.node.qualifiedName);
|
||||||
|
expect(callees).toContain('CDetect::Processing');
|
||||||
|
} finally {
|
||||||
|
cg?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves typed pointer callers when the method name is ambiguous and the call sits inside a return/declaration', async () => {
|
||||||
|
// Regression: an earlier version of the C++ receiver-type inference matched
|
||||||
|
// the call line itself (`return m_cpAlg->Processing()`) and treated `return`
|
||||||
|
// as the type, OR grabbed `int r =` as a type from the prefix. With Strategy
|
||||||
|
// 3's "unique method name" fallback, the original issue example resolved
|
||||||
|
// anyway — but as soon as two classes share a method name (very common in
|
||||||
|
// real C++), both calls go unresolved.
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-'));
|
||||||
|
let cg: CodeGraph | undefined;
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'detect.hpp'),
|
||||||
|
'class CDetect { public: int Processing(); };\n' +
|
||||||
|
'class CWidget { public: int Processing(); };\n' +
|
||||||
|
'class CDetector {\n' +
|
||||||
|
' private:\n' +
|
||||||
|
' CDetect* m_cpAlg = nullptr;\n' +
|
||||||
|
' public:\n' +
|
||||||
|
' int RunReturn();\n' +
|
||||||
|
' int RunAssign();\n' +
|
||||||
|
'};\n'
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'detect.cpp'),
|
||||||
|
'#include "detect.hpp"\n' +
|
||||||
|
'int CDetector::RunReturn() { return m_cpAlg->Processing(); }\n' +
|
||||||
|
'int CDetector::RunAssign() { int r = m_cpAlg->Processing(); return r; }\n' +
|
||||||
|
'int CDetect::Processing() { return 0; }\n' +
|
||||||
|
'int CWidget::Processing() { return 0; }\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
cg = CodeGraph.initSync(tmpDir);
|
||||||
|
await cg.indexAll();
|
||||||
|
|
||||||
|
const detectProc = cg
|
||||||
|
.getNodesByKind('method')
|
||||||
|
.find((n) => n.qualifiedName === 'CDetect::Processing');
|
||||||
|
const widgetProc = cg
|
||||||
|
.getNodesByKind('method')
|
||||||
|
.find((n) => n.qualifiedName === 'CWidget::Processing');
|
||||||
|
expect(detectProc).toBeDefined();
|
||||||
|
expect(widgetProc).toBeDefined();
|
||||||
|
|
||||||
|
const detectCallers = cg.getCallers(detectProc!.id).map((c) => c.node.qualifiedName);
|
||||||
|
expect(detectCallers).toContain('CDetector::RunReturn');
|
||||||
|
expect(detectCallers).toContain('CDetector::RunAssign');
|
||||||
|
|
||||||
|
// CWidget::Processing is never called — calls must NOT misroute here.
|
||||||
|
const widgetCallers = cg.getCallers(widgetProc!.id).map((c) => c.node.qualifiedName);
|
||||||
|
expect(widgetCallers).not.toContain('CDetector::RunReturn');
|
||||||
|
expect(widgetCallers).not.toContain('CDetector::RunAssign');
|
||||||
|
} finally {
|
||||||
|
cg?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('bridges a base virtual method to the subclass override', async () => {
|
it('bridges a base virtual method to the subclass override', async () => {
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-'));
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-'));
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -2,6 +2,51 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|||||||
import { getChildByField, getNodeText } from '../tree-sitter-helpers';
|
import { getChildByField, getNodeText } from '../tree-sitter-helpers';
|
||||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||||
|
|
||||||
|
function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined {
|
||||||
|
const declarator = getChildByField(node, 'declarator');
|
||||||
|
if (!declarator) return undefined;
|
||||||
|
|
||||||
|
const queue: SyntaxNode[] = [declarator];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
if (current.type === 'qualified_identifier') {
|
||||||
|
const text = getNodeText(current, source).trim();
|
||||||
|
const parts = text.split('::').filter(Boolean);
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < current.namedChildCount; i++) {
|
||||||
|
const child = current.namedChild(i);
|
||||||
|
if (child) queue.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCppReceiverType(node: SyntaxNode, source: string): string | undefined {
|
||||||
|
const declarator = getChildByField(node, 'declarator');
|
||||||
|
if (!declarator) return undefined;
|
||||||
|
|
||||||
|
const queue: SyntaxNode[] = [declarator];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
if (current.type === 'qualified_identifier') {
|
||||||
|
const text = getNodeText(current, source).trim();
|
||||||
|
const parts = text.split('::').filter(Boolean);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
return parts.slice(0, -1).join('::');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < current.namedChildCount; i++) {
|
||||||
|
const child = current.namedChild(i);
|
||||||
|
if (child) queue.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const cExtractor: LanguageExtractor = {
|
export const cExtractor: LanguageExtractor = {
|
||||||
functionTypes: ['function_definition'],
|
functionTypes: ['function_definition'],
|
||||||
classTypes: [],
|
classTypes: [],
|
||||||
@@ -62,6 +107,8 @@ export const cppExtractor: LanguageExtractor = {
|
|||||||
nameField: 'declarator',
|
nameField: 'declarator',
|
||||||
bodyField: 'body',
|
bodyField: 'body',
|
||||||
paramsField: 'parameters',
|
paramsField: 'parameters',
|
||||||
|
resolveName: extractCppQualifiedMethodName,
|
||||||
|
getReceiverType: extractCppReceiverType,
|
||||||
getVisibility: (node) => {
|
getVisibility: (node) => {
|
||||||
// Check for access specifier in parent
|
// Check for access specifier in parent
|
||||||
const parent = node.parent;
|
const parent = node.parent;
|
||||||
|
|||||||
@@ -1504,10 +1504,11 @@ export class TreeSitterExtractor {
|
|||||||
const func = getChildByField(node, 'function') || node.namedChild(0);
|
const func = getChildByField(node, 'function') || node.namedChild(0);
|
||||||
|
|
||||||
if (func) {
|
if (func) {
|
||||||
if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression' || func.type === 'navigation_expression') {
|
if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression' || func.type === 'navigation_expression' || func.type === 'field_expression') {
|
||||||
// Method call: obj.method() or obj.field.method()
|
// Method call: obj.method() or obj.field.method()
|
||||||
// Go uses selector_expression with 'field', JS/TS uses member_expression with 'property'
|
// Go uses selector_expression with 'field', JS/TS uses member_expression with 'property'
|
||||||
// Kotlin uses navigation_expression with navigation_suffix > simple_identifier
|
// Kotlin uses navigation_expression with navigation_suffix > simple_identifier
|
||||||
|
// C/C++ use field_expression for both `obj.method()` and `ptr->method()`
|
||||||
let property = getChildByField(func, 'property') || getChildByField(func, 'field');
|
let property = getChildByField(func, 'property') || getChildByField(func, 'field');
|
||||||
if (!property) {
|
if (!property) {
|
||||||
const child1 = func.namedChild(1);
|
const child1 = func.namedChild(1);
|
||||||
@@ -1524,9 +1525,13 @@ export class TreeSitterExtractor {
|
|||||||
// This helps the resolver distinguish method calls from bare function calls
|
// This helps the resolver distinguish method calls from bare function calls
|
||||||
// (e.g., Python's console.print() vs builtin print())
|
// (e.g., Python's console.print() vs builtin print())
|
||||||
// Skip self/this/cls as they don't aid resolution
|
// Skip self/this/cls as they don't aid resolution
|
||||||
const receiver = getChildByField(func, 'object') || getChildByField(func, 'operand') || func.namedChild(0);
|
const receiver =
|
||||||
|
getChildByField(func, 'object') ||
|
||||||
|
getChildByField(func, 'operand') ||
|
||||||
|
getChildByField(func, 'argument') ||
|
||||||
|
func.namedChild(0);
|
||||||
const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
|
const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
|
||||||
if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier')) {
|
if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier' || receiver.type === 'field_identifier')) {
|
||||||
const receiverName = getNodeText(receiver, this.source);
|
const receiverName = getNodeText(receiver, this.source);
|
||||||
if (!SKIP_RECEIVERS.has(receiverName)) {
|
if (!SKIP_RECEIVERS.has(receiverName)) {
|
||||||
calleeName = `${receiverName}.${methodName}`;
|
calleeName = `${receiverName}.${methodName}`;
|
||||||
|
|||||||
@@ -146,6 +146,122 @@ export function matchByQualifiedName(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMethodOnType(
|
||||||
|
typeName: string,
|
||||||
|
methodName: string,
|
||||||
|
ref: UnresolvedRef,
|
||||||
|
context: ResolutionContext,
|
||||||
|
confidence: number,
|
||||||
|
resolvedBy: ResolvedRef['resolvedBy'],
|
||||||
|
): ResolvedRef | null {
|
||||||
|
// Look up methods by name and match by qualifiedName ending in
|
||||||
|
// `<typeName>::<methodName>`. This works whether the method is defined
|
||||||
|
// in-class (`class Foo { int bar() { ... } }`) or out-of-line in a separate
|
||||||
|
// file (`int Foo::bar() { ... }` in foo.cpp while class Foo is in foo.hpp).
|
||||||
|
// The previous same-file approach missed the latter — the typical C++ layout.
|
||||||
|
const methodCandidates = context.getNodesByName(methodName);
|
||||||
|
const want = `${typeName}::${methodName}`;
|
||||||
|
for (const m of methodCandidates) {
|
||||||
|
if (m.kind !== 'method') continue;
|
||||||
|
if (m.language !== ref.language) continue;
|
||||||
|
const qn = m.qualifiedName;
|
||||||
|
if (qn === want || qn.endsWith(`::${want}`)) {
|
||||||
|
return {
|
||||||
|
original: ref,
|
||||||
|
targetNodeId: m.id,
|
||||||
|
confidence,
|
||||||
|
resolvedBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// C++ keywords/control-flow tokens that can appear right before a receiver
|
||||||
|
// (e.g. `return ptr->m()`) and must NOT be treated as a type.
|
||||||
|
const CPP_NON_TYPE_TOKENS = new Set([
|
||||||
|
'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default',
|
||||||
|
'break', 'continue', 'goto', 'throw', 'new', 'delete', 'co_await', 'co_yield',
|
||||||
|
'co_return', 'static_cast', 'const_cast', 'dynamic_cast', 'reinterpret_cast',
|
||||||
|
'sizeof', 'alignof', 'typeid', 'and', 'or', 'not', 'xor',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeCppTypeName(typeName: string): string | null {
|
||||||
|
const normalized = typeName
|
||||||
|
.replace(/\b(const|volatile|mutable|typename|class|struct)\b/g, ' ')
|
||||||
|
.replace(/[&*]+/g, ' ')
|
||||||
|
.replace(/<[^>]*>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!normalized) return null;
|
||||||
|
const parts = normalized.split(/::/).filter(Boolean);
|
||||||
|
const last = parts[parts.length - 1];
|
||||||
|
if (!last) return null;
|
||||||
|
if (CPP_NON_TYPE_TOKENS.has(last)) return null;
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declarator regex: matches `Type receiver`, `Type* receiver`, `Type *receiver`,
|
||||||
|
// `Type*receiver`, `Type<X> receiver`, etc., REQUIRING a declarator terminator
|
||||||
|
// (`;`, `=`, `,`, `)`, `[`, `{`, `(`, or end-of-line) after the receiver. The
|
||||||
|
// terminator rules out uses like `return receiver->m()` where the preceding
|
||||||
|
// token is a keyword, not a type.
|
||||||
|
function buildDeclaratorRegex(escapedReceiver: string): RegExp {
|
||||||
|
return new RegExp(
|
||||||
|
`([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferCppReceiverType(
|
||||||
|
receiverName: string,
|
||||||
|
ref: UnresolvedRef,
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const source = context.readFile(ref.filePath);
|
||||||
|
if (!source) return null;
|
||||||
|
|
||||||
|
const lines = source.split(/\r?\n/);
|
||||||
|
const callLineIndex = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
|
||||||
|
const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
|
||||||
|
const declaratorRegex = buildDeclaratorRegex(escapedReceiver);
|
||||||
|
|
||||||
|
for (let i = callLineIndex; i >= 0; i--) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (!line || !receiverPattern.test(line)) continue;
|
||||||
|
|
||||||
|
const declaratorMatch = line.match(declaratorRegex);
|
||||||
|
if (declaratorMatch) {
|
||||||
|
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
|
||||||
|
if (normalized) return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerCandidates = [
|
||||||
|
ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.h'),
|
||||||
|
ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.hpp'),
|
||||||
|
ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.hxx'),
|
||||||
|
].filter((candidate, index, arr) => arr.indexOf(candidate) === index && candidate !== ref.filePath);
|
||||||
|
|
||||||
|
for (const headerPath of headerCandidates) {
|
||||||
|
if (!context.fileExists(headerPath)) continue;
|
||||||
|
const headerSource = context.readFile(headerPath);
|
||||||
|
if (!headerSource) continue;
|
||||||
|
|
||||||
|
for (const line of headerSource.split(/\r?\n/)) {
|
||||||
|
if (!receiverPattern.test(line)) continue;
|
||||||
|
const declaratorMatch = line.match(declaratorRegex);
|
||||||
|
if (!declaratorMatch) continue;
|
||||||
|
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
|
||||||
|
if (normalized) return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to resolve by method name on a class/object
|
* Try to resolve by method name on a class/object
|
||||||
*/
|
*/
|
||||||
@@ -164,6 +280,23 @@ export function matchMethodCall(
|
|||||||
|
|
||||||
const [, objectOrClass, methodName] = match;
|
const [, objectOrClass, methodName] = match;
|
||||||
|
|
||||||
|
if (ref.language === 'cpp' && dotMatch) {
|
||||||
|
const inferredType = inferCppReceiverType(objectOrClass!, ref, context);
|
||||||
|
if (inferredType) {
|
||||||
|
const typedMatch = resolveMethodOnType(
|
||||||
|
inferredType,
|
||||||
|
methodName!,
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
0.9,
|
||||||
|
'instance-method',
|
||||||
|
);
|
||||||
|
if (typedMatch) {
|
||||||
|
return typedMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Strategy 1: Direct class name match (existing logic)
|
// Strategy 1: Direct class name match (existing logic)
|
||||||
const classCandidates = context.getNodesByName(objectOrClass!);
|
const classCandidates = context.getNodesByName(objectOrClass!);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user