|
|
@@ -146,6 +146,122 @@ export function matchByQualifiedName(
|
|
|
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
|
|
|
*/
|
|
|
@@ -164,6 +280,23 @@ export function matchMethodCall(
|
|
|
|
|
|
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)
|
|
|
const classCandidates = context.getNodesByName(objectOrClass!);
|
|
|
|