diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe35074f..2065e530e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Indexing a mid-size project is faster.** Below 150,000 pending references the resolver used to stay on one thread for the whole run. It now boots its worker pool part-way through once at least 20,000 references are still unresolved and they would take the main thread at least 0.4 s (an explicit `CODEGRAPH_PARALLEL_RESOLVE_MIN` turns this off). Full index of JetBrains Exposed (110,854 references) 18.5 s to 9.6 s, koel 4.9 s to 4.1 s, Alamofire 1.65 s to 1.41 s (median of 3 interleaved runs each, identical graph), at 56–132% more peak memory; a 13,900-reference project never starts the pool and is unchanged. - **More C++ links land on the right target.** `Ptr p(0);` for `using Ptr = Widget*;` declares a pointer, so it no longer counts as constructing a `Widget`. A class nested in a function-local class is as local to that function as the class around it. A class an included `.cc` file declares is seen by the file that includes it, and of two headers declaring one class an object creation takes the one it includes. A `using Base::foo;` in a nested class no longer counts as the outer class's. `NULL` is an integer zero, not a `nullptr`, when an overload is chosen. A header's unnamed namespace belongs to the files that include the header, not to every file. Two annotation-macro cases read as extraction reads them: `const int FOO_BAR = 1;` keeps its name, and `n * FOO_BAR::VALUE` stays a product. +- **Fewer wrong C++ links from overloads, aliases and specializations.** `Foo x; Take(x);` links `Take(const Foo&)`, not `Take(Foo&&)`; an `int` converted to the parameter's class still takes the `&&` one. A `constexpr Widget* p` points at a non-const `Widget`, and an element read through a const container's iterator is const, so each picks the matching `const` or non-const overload. `f(a)` on an `int a[3][4]` takes `f(int (*)[4])`, not `f(int*)`. A qualified call into a namespace a macro opens links none of the overloads that fit its arguments best when several tie. A namespace alias applies from its own line on and only inside the block that declares it, and `namespace a = ::lib;` names the global `lib`. `ROCKSDB_NAMESPACE::DB` written before `#define ROCKSDB_NAMESPACE rocksdb`, or after its `#undef`, no longer resolves through the macro; an `#undef` in an `#if 0` branch changes nothing, and an `#include` after an `#undef` that defines the macro defines it again. A specialization of `a::f` no longer removes `b::f` from a call's choices, `foo.foo()` reads the template arguments after the method name, and `Put()` in a class that only specializes `Put` links nothing instead of a base class's `Put`. Re-index C++ projects after upgrading. +- **Two C cases no longer link a call to the wrong definition.** A `static` function in another source file is reached only through an `#include` written before the call and not inside `#if 0`. A flag counts as set for a header's flag-only definition only by a real `#define` in the call's own component: not one inside a comment or raw string, not one in a nested component, and an empty build file still marks a component boundary. Re-index C projects after upgrading. - **Syncing a changed file in a large Python project is faster.** When a Python class has no method of the name being linked (a test class that passes `Handler.__init__` as a value, say), the resolver checks every class with that method for one that derives from it. For each class whose base is a package import (`import unittest`, then `unittest.TestCase`) it parsed the whole file again, and its cache holds only a few files, so two such references took about a second each. A plain one-line `import a.b` is now read from its line; other forms still use the parse. In CPython a one-file sync takes a median 5.5 s instead of 7.7 s on Node and 5.2 s instead of 8.1 s on Bun (4 alternating runs per build, ranges not overlapping; the phase that retries unresolved references drops from 4.6 s to 2.3 s). A full index is within the run-to-run variation, and the graph is the same on CPython and Django (no edge differs). - **The same sync resolves each Python class's bases once.** The scan above resolved the base classes of every candidate again for each reference that needed it. The bases of a class depend only on its own header and file, so they are now kept for the rest of the run. In CPython the phase that retries unresolved references drops from a median 2.29 s to 2.08 s on Node (-9%, 4 alternating runs per build, ranges not overlapping) and a one-file sync by about 3% (5.33 s to 5.15 s on the second sync); on Bun the sync time shows no clear change. A full index is within the run-to-run variation, and no edge, node or unresolved reference differs on CPython or Django. - **Questions to the MCP server are faster in a project with no `codegraph.json`.** While ranking results, CodeGraph checked for the project's `codegraph.json` once per result path, and with no such file each check raised and caught an error. It now asks the file system without raising, so `codegraph_explore` and the other query tools spend less CPU on most projects. Editing, adding or deleting `codegraph.json` is still picked up without a restart (#491). diff --git a/__tests__/c-feature-macro-variants.test.ts b/__tests__/c-feature-macro-variants.test.ts index 4e8bf90d1..15fb73f8c 100644 --- a/__tests__/c-feature-macro-variants.test.ts +++ b/__tests__/c-feature-macro-variants.test.ts @@ -184,3 +184,129 @@ describe('a C call past a definition only a flag nothing sets compiles', () => { expect(calls('client_version')).toEqual(['include/api.h:4']); }); }); + +/** + * Where a flag counts as set: an empty build file still bounds a component, a + * `#define` in a comment or a raw string sets nothing, and a nested component's + * `#define` does not set the flag for the component around it. + */ +describe('a C flag set only in a comment, a nested component or beyond an empty build file', () => { + const FILES: Record = { + 'lib/CMakeLists.txt': lines( + 'add_library(lib other.c)', + ), + 'lib/other.c': lines( + '#define USE_FAST 1', + 'int other(void) { return 0; }', + ), + 'lib/sub/CMakeLists.txt': '', + 'lib/sub/f.h': lines( + '#ifdef USE_FAST', + 'static inline int f(void) { return 1; }', + '#else', + 'int f(void);', + '#endif', + ), + 'lib/sub/f.c': lines( + 'int f(void) { return 0; }', + ), + 'lib/sub/use.c': lines( + '#include "f.h"', + 'int g(void) { return f(); }', + ), + 'doc/CMakeLists.txt': lines( + 'add_library(doc f.c)', + ), + 'doc/notes.c': lines( + '/*', + '#define USE_NOTE 1', + '*/', + 'int notes(void) { return 0; }', + ), + 'doc/raw.cc': lines( + 'const char* text = R"(', + '#define USE_RAW 1', + ')";', + ), + 'doc/f.h': lines( + '#ifdef USE_NOTE', + 'static inline int nt(void) { return 1; }', + '#else', + 'int nt(void);', + '#endif', + '#ifdef USE_RAW', + 'static inline int rw(void) { return 1; }', + '#else', + 'int rw(void);', + '#endif', + ), + 'doc/f.c': lines( + 'int nt(void) { return 0; }', + 'int rw(void) { return 0; }', + ), + 'doc/use.c': lines( + '#include "f.h"', + 'int note_use(void) { return nt(); }', + 'int raw_use(void) { return rw(); }', + ), + 'outer/CMakeLists.txt': lines( + 'add_library(outer f.c)', + ), + 'outer/plugin/CMakeLists.txt': lines( + 'add_library(plugin p.c)', + ), + 'outer/plugin/p.c': lines( + '#define USE_PLUGIN 1', + 'int p(void) { return 0; }', + ), + 'outer/f.h': lines( + '#ifdef USE_PLUGIN', + 'static inline int nf(void) { return 1; }', + '#else', + 'int nf(void);', + '#endif', + ), + 'outer/f.c': lines( + 'int nf(void) { return 0; }', + ), + 'outer/use.c': lines( + '#include "f.h"', + 'int ng(void) { return nf(); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-feature-components-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('an empty build file bounds a component', () => { + expect(callees('g')).toEqual(['f@lib/sub/f.c:1']); + }); + it('a `#define` in a comment or a raw string defines nothing', () => { + expect(callees('note_use')).toEqual(['nt@doc/f.c:1']); + expect(callees('raw_use')).toEqual(['rw@doc/f.c:2']); + }); + it("a nested component's `#define` is not the outer component's", () => { + expect(callees('ng')).toEqual(['nf@outer/f.c:1']); + }); +}); diff --git a/__tests__/c-static-call-rescue.test.ts b/__tests__/c-static-call-rescue.test.ts index a175a3f33..f037c8501 100644 --- a/__tests__/c-static-call-rescue.test.ts +++ b/__tests__/c-static-call-rescue.test.ts @@ -127,3 +127,62 @@ describe("a C call reaches the function its file sees, not another file's static expect(cg.getFileDependencies('src/sort.c')).not.toContain('deps/jemalloc/test/include/test/math.h'); }); }); + +/** + * A `static` function another source file defines is visible through an + * `#include` of that file only where the include is compiled and written before + * the call: not under `#if 0`, and not after the call. + */ +describe("a C call sees another source file's static only through an include before it", () => { + const FILES: Record = { + 'deps/dict.c': lines( + 'static unsigned dictHash(const char* k) { return 0; }', + ), + 'inactive.c': lines( + '#if 0', + '#include "deps/dict.c"', + '#endif', + 'unsigned h1(void) { return dictHash("a"); }', + ), + 'later.c': lines( + 'unsigned h2(void) { return dictHash("a"); }', + '#include "deps/dict.c"', + ), + 'active.c': lines( + '#include "deps/dict.c"', + 'unsigned h3(void) { return dictHash("a"); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-static-include-order-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('not through an include under `#if 0` or after the call', () => { + expect(callees('h1')).toEqual([]); + expect(callees('h2')).toEqual([]); + }); + it('through an include before the call', () => { + expect(callees('h3')).toEqual(['dictHash@deps/dict.c:1']); + }); +}); diff --git a/__tests__/cpp-free-function-overloads.test.ts b/__tests__/cpp-free-function-overloads.test.ts index c48af450b..ecf2509a1 100644 --- a/__tests__/cpp-free-function-overloads.test.ts +++ b/__tests__/cpp-free-function-overloads.test.ts @@ -255,3 +255,60 @@ describe('a C++ call picks the free-function overload its arguments fit', () => expect(calls('WarnCode')).toEqual(['lg::Warn@log.cc:4']); }); }); + +/** + * Overloads the argument's type fits equally well are a tie even when another + * overload fits it worse: `f(short)` beside `f(long)`, `f(int)` and `f(char)` + * is not `f(long)` because it is declared first. The overloads sit in a + * namespace a macro opens, where only the qualified-name step can place the + * call; in a namespace written out, the name step picks among overloads the + * arguments fit alike by where they are, as above. + */ +describe('a C++ call whose best-fitting overloads tie', () => { + const FILES: Record = { + 'real.h': lines( + '#define REAL_BEGIN namespace real {', + '#define REAL_END }', + 'REAL_BEGIN', + 'void f(long v) {}', + 'void f(int v) {}', + 'void f(char v) {}', + 'REAL_END', + ), + 'use.cc': lines( + '#include "real.h"', + 'namespace r = real;', + 'void Direct() { short s = 1; real::f(s); }', + 'void ThroughAlias() { short s = 1; r::f(s); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-overload-top-tie-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('links none of the tied overloads', () => { + expect(callees('Direct')).toEqual([]); + expect(callees('ThroughAlias')).toEqual([]); + }); +}); diff --git a/__tests__/cpp-global-alias-target.test.ts b/__tests__/cpp-global-alias-target.test.ts index 5c6754c98..fb8bf7689 100644 --- a/__tests__/cpp-global-alias-target.test.ts +++ b/__tests__/cpp-global-alias-target.test.ts @@ -11,7 +11,7 @@ * `arena_.Fuse(other.arena_)` fell back to the type's last name, landing on * the calling method `hpb::Arena::Fuse`. */ -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -214,3 +214,68 @@ describe('other C++ names written from the global scope', () => { } }); }); + +/** + * A namespace alias applies where C++ declares it: from its declaration on, and + * only inside the function that declares it. An alias of `::lib` names the global + * `lib`, not a `lib` nested in the namespace around the code. + */ +describe('a C++ namespace alias applies where it is declared', () => { + const FILES: Record = { + 'lib.h': lines( + 'namespace lib { struct Type { void Run() {} }; }', + ), + 'a.cc': lines( + '#include "lib.h"', + 'void Setup() {', + ' namespace x = lib;', + '}', + 'void UseLocal(x::Type t) { t.Run(); }', + 'void UseBefore(y::Type t) { t.Run(); }', + 'namespace y = lib;', + 'namespace z = lib;', + 'void UseAfter(z::Type t) { t.Run(); }', + ), + 'scope.h': lines( + 'namespace glob { struct Type { void Run() {} }; }', + 'namespace scope {', + 'namespace glob { struct Type { void Run() {} }; }', + 'namespace a = ::glob;', + 'void Use(a::Type t) { t.Run(); }', + '}', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-alias-scope-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('not outside the function that declares it, nor before its declaration', () => { + expect(callees('UseLocal')).toEqual([]); + expect(callees('UseBefore')).toEqual([]); + expect(callees('UseAfter')).toEqual(['lib::Type::Run@lib.h:1']); + }); + it('an alias of `::glob` names the global namespace', () => { + expect(callees('scope::Use')).toEqual(['glob::Type::Run@scope.h:1']); + }); +}); diff --git a/__tests__/cpp-implicit-this-calls.test.ts b/__tests__/cpp-implicit-this-calls.test.ts index 7d7edc923..35935f52e 100644 --- a/__tests__/cpp-implicit-this-calls.test.ts +++ b/__tests__/cpp-implicit-this-calls.test.ts @@ -20,6 +20,8 @@ import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; +const lines = (...l: string[]): string => [...l, ''].join('\n'); + function writeProject(prefix: string, files: Record): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); for (const [rel, content] of Object.entries(files)) { @@ -522,3 +524,57 @@ class IntList : public ListBase { expect(callsFrom(cg, 'src/util/list.h', 'util::IntList::Push')).toEqual(['Grow -> util::ListBase::Grow']); }); }); + +/** + * A bare or `this->` call to a member template whose class defines only explicit + * specializations of it: `Put()` in a class that specializes only `Put` + * calls the class's own template, which has no body here. Before, lookup went on + * past the class to the base class's `Put`. + */ +describe('a C++ call to a member template its class only specializes for other arguments', () => { + const FILES: Record = { + 'a.h': lines( + 'struct Base { template void Put() {} };', + 'struct Codec { void Put() {} };', + 'struct Writer : Base {', + ' template void Put();', + ' void Run() { Put(); }', + ' void RunThis() { this->Put(); }', + ' void RunInt() { Put(); }', + '};', + 'template <> void Writer::Put() {}', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-implicit-specialization-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it("links nothing: neither the base class's template nor a namesake", () => { + expect(callees('Writer::Run')).toEqual([]); + expect(callees('Writer::RunThis')).toEqual([]); + }); + it('still reaches the specialization the call names', () => { + expect(callees('Writer::RunInt')).toEqual(['Writer::Put@a.h:9']); + }); +}); diff --git a/__tests__/cpp-macro-namespace.test.ts b/__tests__/cpp-macro-namespace.test.ts index ca53c9fa9..73e0615d8 100644 --- a/__tests__/cpp-macro-namespace.test.ts +++ b/__tests__/cpp-macro-namespace.test.ts @@ -13,6 +13,8 @@ import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; +const lines = (...l: string[]): string => [...l, ''].join('\n'); + let root = ''; let cg: CodeGraph; @@ -137,3 +139,115 @@ describe('C++ namespaces opened by a macro', () => { ]); }); }); + +/** + * A name macro spells a namespace only from its `#define` on: a reference written + * before `#define ROCKSDB_NAMESPACE rocksdb` is not `rocksdb::DB`. + */ +describe('a C++ name macro applies from its definition on', () => { + const FILES: Record = { + 'db.h': lines( + 'namespace rocksdb { struct DB { void Get() {} }; }', + ), + 'use.cc': lines( + '#include "db.h"', + 'void Before(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + '#define ROCKSDB_NAMESPACE rocksdb', + 'void After(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + '#undef ROCKSDB_NAMESPACE', + 'void Undone(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-macro-order-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('not before its `#define`, nor after its `#undef`', () => { + expect(callees('Before')).toEqual([]); + expect(callees('Undone')).toEqual([]); + expect(callees('After')).toEqual(['rocksdb::DB::Get@db.h:1']); + }); +}); + +/** + * Which `#define` or `#undef` of a name macro is the last one in effect: one in a branch no + * build compiles (`#if 0`) is not, and an `#undef` does not hide a definition an `#include` + * after it brings in. + */ +describe('a C++ name macro follows the directives that are in effect', () => { + const FILES: Record = { + 'db.h': lines('namespace rocksdb { struct DB { void Get() {} }; }'), + 'config.h': lines('#define ROCKSDB_NAMESPACE rocksdb'), + 'inactive.cc': lines( + '#include "db.h"', + '#define ROCKSDB_NAMESPACE rocksdb', + '#if 0', + '#undef ROCKSDB_NAMESPACE', + '#endif', + 'void Active(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + ), + 'redefined.cc': lines( + '#include "db.h"', + '#undef ROCKSDB_NAMESPACE', + '#include "config.h"', + 'void Redefined(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + ), + 'hidden.cc': lines( + '#include "db.h"', + '#include "config.h"', + '#undef ROCKSDB_NAMESPACE', + 'void Hidden(ROCKSDB_NAMESPACE::DB* db) { db->Get(); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-macro-effect-')); + for (const [file, text] of Object.entries(FILES)) { + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + const callees = (caller: string): string[] => { + const fn = cg.getNodesByKind('function').find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('ignores an `#undef` in a branch no build compiles', () => { + expect(callees('Active')).toEqual(['rocksdb::DB::Get@db.h:1']); + }); + it('lets an include after an `#undef` define the macro again, and an `#undef` after it hide it', () => { + expect(callees('Redefined')).toEqual(['rocksdb::DB::Get@db.h:1']); + expect(callees('Hidden')).toEqual([]); + }); +}); diff --git a/__tests__/cpp-overload-argument-types.test.ts b/__tests__/cpp-overload-argument-types.test.ts index 52ad3b7a1..0487e89f1 100644 --- a/__tests__/cpp-overload-argument-types.test.ts +++ b/__tests__/cpp-overload-argument-types.test.ts @@ -10,7 +10,7 @@ * or `nullptr` literal, a name the calling function declares (or its * address), or an expression whose type is known. */ -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -513,3 +513,47 @@ describe('C++ overloads chosen by argument types', () => { } }); }); + +/** + * A multidimensional array decays to a pointer to its first row, not to its + * element: `int a[3][4]` is an `int (*)[4]`, not an `int*`. + */ +describe('a C++ multidimensional array argument', () => { + const FILES: Record = { + 'a.cc': lines( + 'void f(int* p) {}', + 'void f(int (*p)[4]) {}', + 'void TwoDim() { int a[3][4]; f(a); }', + 'void OneDim() { int b[3]; f(b); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-multidim-array-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('a two-dimensional array takes the pointer to its row', () => { + expect(callees('TwoDim')).toEqual(['f@a.cc:2']); + expect(callees('OneDim')).toEqual(['f@a.cc:1']); + }); +}); diff --git a/__tests__/cpp-overload-constness.test.ts b/__tests__/cpp-overload-constness.test.ts index d8d2b11c6..eec2c915c 100644 --- a/__tests__/cpp-overload-constness.test.ts +++ b/__tests__/cpp-overload-constness.test.ts @@ -193,3 +193,81 @@ describe("a C++ call picks between const and non-const overloads by its argument expect(calls('Look', 'peek')).toEqual(['peek:64']); }); }); + +/** + * More of what an argument's constness and value category decide: an lvalue never + * binds `Foo&&`; a `constexpr` pointer is itself const, not what it points at; and + * an element read through a const container's iterator is const. + */ +describe('a C++ argument binds a reference by its value category and constness', () => { + const FILES: Record = { + 'a.cc': lines( + '#include ', + 'struct Foo {};', + 'void Take(const Foo& f) {}', + 'void Take(Foo&& f) {}', + 'Foo Make() { return Foo(); }', + 'void Lvalue() { Foo x; Take(x); }', + 'void Temporary() { Take(Make()); }', + 'struct Widget {};', + 'void Point(Widget* w) {}', + 'void Point(const Widget* w) {}', + 'Widget global;', + 'void ConstexprPointer() { constexpr Widget* p = &global; Point(p); }', + 'void ConstPointer() { const Widget* p = &global; Point(p); }', + 'void ConstexprObject() { constexpr Widget w{}; Point(&w); }', + 'void Bind(Foo& f) {}', + 'void Bind(const Foo& f) {}', + 'void ConstContainer(const std::vector& v) { Bind(*v.begin()); }', + 'void MutableContainer(std::vector& v) { Bind(*v.begin()); }', + ), + 'b.cc': lines( + 'struct Value { Value(int v) {} };', + 'struct Box {', + ' void Push(Value&& v) {}', + ' void Push(const Value& v) {}', + '};', + 'void Convert(Box& box) { int i = 1; box.Push(i); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-overload-binding-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('an lvalue takes `const Foo&`, a temporary `Foo&&`', () => { + expect(callees('Lvalue')).toEqual(['Take@a.cc:3']); + expect(callees('Temporary')).toEqual(['Make@a.cc:5', 'Take@a.cc:4']); + // An `int` converted to `Value` is a temporary, so it may bind `Value&&`. + expect(callees('Convert')).toEqual(['Box::Push@b.cc:3']); + }); + it('a `constexpr` pointer points at a non-const object', () => { + expect(callees('ConstexprPointer')).toEqual(['Point@a.cc:9']); + expect(callees('ConstPointer')).toEqual(['Point@a.cc:10']); + expect(callees('ConstexprObject')).toEqual(['Point@a.cc:10']); + }); + it("a const container's element is const", () => { + expect(callees('ConstContainer')).toEqual(['Bind@a.cc:16']); + expect(callees('MutableContainer')).toEqual(['Bind@a.cc:15']); + }); +}); diff --git a/__tests__/cpp-specialization-names.test.ts b/__tests__/cpp-specialization-names.test.ts index c51642fd1..88de82530 100644 --- a/__tests__/cpp-specialization-names.test.ts +++ b/__tests__/cpp-specialization-names.test.ts @@ -463,3 +463,66 @@ describe('a C++ call reaches the specialization its template arguments name', () expect(overriders('pb::Field::Visit', 13)).toEqual([]); }); }); + +/** + * Explicit specializations choose among one template's candidates only: a + * specialization of `a::f` does not crowd out `b::f` for a call inside namespace + * `b`. And the template arguments a call writes are read after the name it calls, + * not after a receiver of the same name (`foo.foo()`). + */ +describe('a C++ specialization chooses only among its own template and the name the call writes', () => { + const FILES: Record = { + 'a.h': lines( + 'namespace a {', + 'template void f(T v) {}', + 'template <> void f(int v) {}', + '}', + 'namespace b {', + 'template void f(T v) {}', + 'void g() { f(1); }', + '}', + 'struct Foo {', + ' template void foo() {}', + '};', + 'template <> void Foo::foo() {}', + 'template <> void Foo::foo() {}', + ), + 'a.cc': lines( + '#include "a.h"', + 'void sameName(Foo& foo) { foo.foo(); }', + 'void otherName(Foo& x) { x.foo(); }', + ), + }; + + let root: string; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-specialization-family-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + }, 120_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** What a function or method calls, as `qualifiedName@file:line`. */ + const callees = (caller: string): string[] => { + const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller); + expect(fn, caller).toBeDefined(); + return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort(); + }; + + it('a call in namespace `b` reaches `b::f`, not a specialization of `a::f`', () => { + expect(callees('b::g')).toEqual(['b::f@a.h:6']); + }); + it('`foo.foo()` reads the arguments after the method, not the receiver', () => { + expect(callees('sameName')).toEqual(['Foo::foo@a.h:13']); + expect(callees('otherName')).toEqual(['Foo::foo@a.h:13']); + }); +}); diff --git a/codegraph-kernel/src/resolve/c_feature_variants.rs b/codegraph-kernel/src/resolve/c_feature_variants.rs index f104bd278..59680902f 100644 --- a/codegraph-kernel/src/resolve/c_feature_variants.rs +++ b/codegraph-kernel/src/resolve/c_feature_variants.rs @@ -160,15 +160,18 @@ impl KernelResolver { text } - /// The nearest directory at or above `dir` with a build file, else the project root. + /// The nearest directory at or above `dir` with a build file, else the project root. An empty + /// build file bounds a component as well. fn variant_component(&mut self, dir: &str) -> String { if let Some(hit) = self.c_variants.components.get(dir) { return hit.clone(); } - let component = if dir.is_empty() || !self.variant_build_text(dir).trim().is_empty() { dir.to_string() } else { self.variant_component(dir_of(dir)) }; + let component = if dir.is_empty() || BUILD_FILES.iter().any(|name| self.file_exists(&format!("{dir}/{name}"))) { dir.to_string() } else { self.variant_component(dir_of(dir)) }; self.c_variants.components.insert(dir.to_string(), component.clone()); component } - /// Does a C or C++ file under `component` `#define` `name`? Read only from files whose bytes hold the name. + /// Does a C or C++ file of `component` `#define` `name`? Read only from files whose bytes hold + /// the name; a `#define` in a comment or raw string is none, and a file of a component nested + /// inside it (under a directory with a build file of its own) is that component's. fn variant_defined_in(&mut self, name: &str, component: &str) -> bool { let key = (name.to_string(), component.to_string()); if let Some(&hit) = self.c_variants.defines.get(&key) { return hit; } @@ -187,7 +190,9 @@ impl KernelResolver { let prefix = if component.is_empty() { String::new() } else { format!("{component}/") }; let defined = shared_regex(&format!(r"(?m)^[ \t]*#[ \t]*define[ \t]+{}\b", regex::escape(name))).is_ok_and(|define| { files.iter().any(|file| file.starts_with(&prefix) - && self.read_file_uncached(file).is_some_and(|s| s.text().contains(name) && define.is_match(s.text()))) + && self.read_file_uncached(file).is_some_and(|s| s.text().contains(name) && define.is_match(s.text()) + && directive_lines(s.text()).iter().any(|line| define.is_match(line))) + && self.variant_component(dir_of(file)) == component) }); self.c_variants.defines.insert(key, defined); defined diff --git a/codegraph-kernel/src/resolve/cpp_implicit_this.rs b/codegraph-kernel/src/resolve/cpp_implicit_this.rs index d14dda5ba..ae38ec187 100644 --- a/codegraph-kernel/src/resolve/cpp_implicit_this.rs +++ b/codegraph-kernel/src/resolve/cpp_implicit_this.rs @@ -29,6 +29,10 @@ enum Lookup { Missing, /// The class may declare the name through a macro: lookup stops. Stop, + /// The class declares the name as a member template whose explicit specializations, none of + /// them the call's, are all the index has of it: the call reaches the template, which has no + /// body here, so no other method or function of that name is called. + Refused, } /// One class C++ lookup passes through, with the declarations that are the class there. @@ -112,16 +116,19 @@ pub(super) fn contains_word(text: &str, word: &str) -> bool { } impl KernelResolver { - /// The method a receiver-less (or `this`) C++ call reaches from the member function it is in. - pub(super) fn cpp_implicit_this(&mut self, r: &ResolveRefIn) -> Res> { + /// The method a receiver-less (or `this`) C++ call reaches from the member function it is in: + /// Some(None) when C++ lookup reaches a member template of the class whose specializations are + /// none the call's (Lookup::Refused), so nothing else of that name may be linked. + pub(super) fn cpp_implicit_this(&mut self, r: &ResolveRefIn) -> Res>> { let name = r.reference_name.as_str(); if r.language != "cpp" || r.reference_kind != "calls" || !re!(r"^[A-Za-z_][A-Za-z0-9_]*$").is_match(name) { return Ok(None); } let Some(receiver) = self.cpp_implicit_receiver(r) else { return Ok(None) }; let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(None) }; - let Some(node) = self.cpp_implicit_this_lookup(name, receiver, &caller, r)? else { return Ok(None) }; + let found = self.cpp_implicit_this_lookup(name, receiver, &caller, r)?; + if !matches!(found, Lookup::Found(_) | Lookup::Refused) { return Ok(None); } // (A recursive call names the function itself, which no local shadows.) if receiver == Receiver::Bare && name != caller.name && self.cpp_local_name(name, &caller, r, false)? { return Ok(None); } - Ok(Some(KCand { node, confidence: 0.9, resolved_by: "instance-method" })) + Ok(Some(match found { Lookup::Found(node) => Some(KCand { node, confidence: 0.9, resolved_by: "instance-method" }), _ => None })) } /// The method a C++ call of `name` written on `this` reaches from `caller` by C++ lookup, as @@ -130,26 +137,26 @@ impl KernelResolver { pub(super) fn cpp_implicit_this_method(&mut self, name: &str, caller: &Arc, r: &ResolveRefIn) -> Res>> { // A local of that name (a lambda, a callable) is what a bare call calls. if name != caller.name && self.cpp_local_name(name, caller, r, true)? { return Ok(None); } - self.cpp_implicit_this_lookup(name, Receiver::This, caller, r) + Ok(match self.cpp_implicit_this_lookup(name, Receiver::This, caller, r)? { Lookup::Found(node) => Some(node), _ => None }) } /// The method a C++ call of `name` with no receiver, or on `this`, reaches from `caller`: of the /// caller's class, a class it derives from, or one it is nested in. A gtest `TEST_F(Fixture, …)` /// or `TEST_P` body is a method of a class gtest derives from `Fixture`, so the fixture's methods - /// are its own: rocksdb's tests call `DBTestBase`'s `Put(…)` and `NewIterator(…)` bare. None when - /// the caller is no member function, or no class on the way has a method of that name. (Whether + /// are its own: rocksdb's tests call `DBTestBase`'s `Put(…)` and `NewIterator(…)` bare. Missing + /// when the caller is no member function, or no class on the way has a method of that name. (Whether /// a local of the caller's shadows it is the caller's to ask.) - fn cpp_implicit_this_lookup(&mut self, name: &str, receiver: Receiver, caller: &Arc, r: &ResolveRefIn) -> Res>> { + fn cpp_implicit_this_lookup(&mut self, name: &str, receiver: Receiver, caller: &Arc, r: &ResolveRefIn) -> Res { let owner = match self.cpp_member_owner(caller)? { Some(owner) => owner, - None => match self.cpp_gtest_fixture(caller, r)? { Some(fixture) => fixture, None => return Ok(None) }, + None => match self.cpp_gtest_fixture(caller, r)? { Some(fixture) => fixture, None => return Ok(Lookup::Missing) }, }; let mut found = Lookup::Missing; for scope in self.cpp_enclosing_classes(&owner, caller)? { found = self.cpp_scope_method(&scope, name, receiver, r)?; if !matches!(found, Lookup::Missing) || receiver == Receiver::This { break; } } - Ok(match found { Lookup::Found(node) => Some(node), _ => None }) + Ok(found) } /// How a C++ call the extractor recorded by its bare name is written, read at its column: @@ -283,7 +290,9 @@ impl KernelResolver { } if belongs { own.push(n); } } + let had = !own.is_empty(); let own = self.cpp_specialization_pool(own, r); + if own.is_empty() && had { return Ok(Lookup::Refused); } let holder = if scope.holds { scope.decls.first() } else { None }; let (inline, rest): (Vec<_>, Vec<_>) = match holder { Some(h) => own.into_iter().partition(|n| n.file_path == h.file_path && range_within(n, h)), diff --git a/codegraph-kernel/src/resolve/cpp_namespace.rs b/codegraph-kernel/src/resolve/cpp_namespace.rs index ebe65d06c..cd14e7b8b 100644 --- a/codegraph-kernel/src/resolve/cpp_namespace.rs +++ b/codegraph-kernel/src/resolve/cpp_namespace.rs @@ -1,16 +1,24 @@ //! Namespace ownership supplied by a project's namespace-opening macros. use super::*; +/// A file's `#define`s and `#undef`s of name macros: (line, name, whether it defines). +type Directives = Rc>; + #[derive(Default)] pub(super) struct NamespaceCache { ready: bool, openers: HashMap<(String,String),Vec>, functions: HashSet<(String,String)>, closers: HashSet<(String,String)>, - aliases: HashMap>, + /// File → the namespace aliases it declares at namespace scope: name → (target, line). + aliases: HashMap>, + /// File → the aliases a function or other block declares: (name, target, first line, last line). + local_aliases: HashMap>, includes: HashMap>, replacements: HashMap<(String,String),String>, frames: HashMap)>>, + /// File → its `#define`s and `#undef`s of name macros. + directives: HashMap, } impl KernelResolver { fn prepare_namespace_macros(&mut self)->Res<()> { @@ -24,7 +32,14 @@ impl KernelResolver { let code=super::awaited::strip_ts_comments(&source.text().replace("\\\r\n"," ").replace("\\\n"," ")); // (Of a file's alternatives for one build or another, `#if … namespace backend = …upb; // #elif … namespace backend = …cpp;`, the first.) - for m in re!(r"(?m)^\s*namespace\s+(\w+)\s*=\s*(?:::)?([\w:]+)\s*;").captures_iter(&code) {cache.aliases.entry(file.clone()).or_default().entry(m[1].to_string()).or_insert_with(|| m[2].to_string());} + if namespace_alias_pattern().is_match(&code) { + for (name,target,line,end) in cpp_namespace_alias_declarations(&super::awaited::strip_ts_comments(source.text())) { + match end { + Some(end)=>cache.local_aliases.entry(file.clone()).or_default().push((name,target,line,end)), + None=>{cache.aliases.entry(file.clone()).or_default().entry(name).or_insert((target,line));} + } + } + } for m in re!(r"(?m)^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]+([^\n]*)$").captures_iter(&code) { let body=m[4].trim(); if re!(r"^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$").is_match(body) {cache.closers.insert((file.clone(),m[1].to_string()));} @@ -62,6 +77,7 @@ impl KernelResolver { self.prepare_namespace_macros()?; let mut names: HashSet = self.cpp_namespaces.openers.values().filter_map(|path| path.first().cloned()).collect(); names.extend(self.cpp_namespaces.aliases.values().flat_map(|aliases| aliases.keys().cloned())); + names.extend(self.cpp_namespaces.local_aliases.values().flat_map(|aliases| aliases.iter().map(|(name,..)|name.clone()))); for node in self.table()?.nodes.iter() { if matches!(node.language.as_str(), "c" | "cpp") && matches!(node.kind.as_str(), "class" | "struct" | "union" | "enum" | "type_alias") { names.extend(node.qualified_name.split("::").filter(|s| !s.is_empty()).map(str::to_string)); @@ -108,39 +124,102 @@ impl KernelResolver { } self.cpp_namespaces.includes.insert(file.to_string(),seen.clone());Ok(seen) } - /// The namespace `name` is an alias of where `file` writes it: `pybind11` for `py` under - /// `namespace py = pybind11;`, declared in the file itself, else alike in the files it includes. - pub(super) fn cpp_namespace_alias(&mut self,file:&str,name:&str)->Res> { + /// The namespace `name` is an alias of where line `line` of `file` writes it: `pybind11` for + /// `py` under `namespace py = pybind11;`, declared before it in the block around it or in the + /// file itself, else alike in the files it includes before it. (`::lib` for an alias of the + /// global `lib`.) + pub(super) fn cpp_namespace_alias(&mut self,file:&str,name:&str,line:i64)->Res> { self.prepare_namespace_macros()?; - if let Some(local)=self.cpp_namespaces.aliases.get(file).and_then(|a|a.get(name)) {return Ok(Some(local.clone()));} + if let Some((_,target,..))=self.cpp_namespaces.local_aliases.get(file).and_then(|a|a.iter().rfind(|(n,_,from,to)|n==name && *from<=line && line<=*to)) {return Ok(Some(target.clone()));} + if let Some((target,_))=self.cpp_namespaces.aliases.get(file).and_then(|a|a.get(name)).filter(|(_,at)|*at<=line) {return Ok(Some(target.clone()));} let visible=self.namespace_visible_files(file,"cpp")?; - let aliases:HashSet<_>=visible.iter().filter_map(|file|self.cpp_namespaces.aliases.get(file).and_then(|a|a.get(name))).cloned().collect(); + let declaring:Vec<(String,String)>=visible.iter().filter(|f|f.as_str()!=file).filter_map(|f|self.cpp_namespaces.aliases.get(f).and_then(|a|a.get(name)).map(|(target,_)|(f.clone(),target.clone()))).collect(); + let mut aliases=HashSet::new(); + for (declarer,target) in declaring { + if self.cpp_included_before(file,line,&declarer,"cpp")? {aliases.insert(target);} + } Ok((aliases.len()==1).then(||aliases.into_iter().next().unwrap())) } - /// `qualified` spelled through the name macros `file` sees, when that differs: rocksdb's types - /// are `ROCKSDB_NAMESPACE::DB` in the index and `rocksdb::DB` to code that names them, under - /// `#define ROCKSDB_NAMESPACE rocksdb`; a tree declaring `namespace rocksdb` itself is named - /// `ROCKSDB_NAMESPACE::Cache` the other way round. - pub(super) fn cpp_name_macro_spelling(&mut self,file:&str,qualified:&str)->Res> { + /// Whether the `#include`s `file` writes before line `line` bring in `target`, directly or + /// through what those files include. An include in a branch only a build with a flag nothing + /// sets compiles (`#if 0`) counts only for a line in such a branch too. + pub(super) fn cpp_included_before(&mut self,file:&str,line:i64,target:&str,language:&str)->Res { + self.cpp_included_between(file,0,line,target,language) + } + /// `cpp_included_before`, counting only the includes written after line `from`. + fn cpp_included_between(&mut self,file:&str,from:i64,line:i64,target:&str,language:&str)->Res { + let imports:Vec<(i64,char,String)>=self.nodes_in_file(file)?.iter() + .filter(|n|n.kind=="import" && matches!(n.language.as_str(),"c"|"cpp") && n.start_line>from && n.start_lineRes> { self.prepare_namespace_macros()?; if self.cpp_namespaces.replacements.is_empty() {return Ok(None);} + let file=r.file_path.as_str(); let visible=self.namespace_visible_files(file,"cpp")?; + let mut written=Vec::new(); let spelled:Vec=qualified.split("::").map(|segment| { let forward:HashSet<_>=self.cpp_namespaces.replacements.iter().filter(|((f,name),_)|visible.contains(f) && name==segment).map(|(_,body)|body.clone()).collect(); - if forward.len()==1 {return forward.into_iter().next().unwrap();} + if forward.len()==1 {written.push(segment.to_string());return forward.into_iter().next().unwrap();} let reverse:HashSet<_>=self.cpp_namespaces.replacements.iter().filter(|((f,_),body)|visible.contains(f) && *body==segment).map(|((_,name),_)|name.clone()).collect(); if reverse.len()==1 {reverse.into_iter().next().unwrap()} else {segment.to_string()} }).collect(); + for name in written { + if !self.cpp_name_macro_defined_at(r,&name)? {return Ok(None);} + } let spelled=spelled.join("::"); Ok((spelled!=qualified).then_some(spelled)) } + /// Whether the name macro `name` is defined where `r` writes it: by the last `#define` or + /// `#undef` of it before the reference in its own file, else by an include written after + /// that directive (or anywhere before the reference, with no directive) of a file that + /// defines it. A directive in a branch only an unset flag compiles counts only for a + /// reference in such a branch too. + fn cpp_name_macro_defined_at(&mut self,r:&ResolveRefIn,name:&str)->Res { + let file=r.file_path.as_str(); + let directives=match self.cpp_namespaces.directives.get(file) { + Some(hit)=>hit.clone(), + None=>{ + let text=self.read_file(file).map(|s|s.text().to_string()).unwrap_or_default(); + let found:Vec<(i64,String,bool)>=super::cpp::directive_lines(&text).iter().enumerate() + .filter_map(|(i,line)|re!(r"^\s*#\s*(define|undef)\s+([A-Za-z_]\w*)").captures(line).map(|m|(i as i64+1,m[2].to_string(),&m[1]=="define"))).collect(); + let found=Rc::new(found); + self.cpp_namespaces.directives.insert(file.to_string(),found.clone());found + } + }; + let off_here=self.is_off_by_default(file,r.line); + let mut last=None; + for (line,n,define) in directives.iter().rev() { + if *line=self.cpp_namespaces.replacements.keys().filter(|(f,n)|n==name && f!=file).map(|(f,_)|f.clone()).collect(); + for definer in definers { + if self.cpp_included_between(file,from,r.line,&definer,&r.language)? {return Ok(true);} + } + Ok(false) + } pub(super) fn match_cpp_macro_namespaced(&mut self,r:&ResolveRefIn)->Res> { if !matches!(r.language.as_str(),"c"|"cpp") || !r.reference_name.trim_start_matches("::").contains("::") {return Ok(None);} self.prepare_namespace_macros()?; let visible=self.namespace_visible_files(&r.file_path,&r.language)?; let mut parts:Vec=r.reference_name.split("::").filter(|p|!p.is_empty()).map(str::to_string).collect(); if let Some(first)=parts.first().cloned() { - if let Some(alias)=self.cpp_namespace_alias(&r.file_path,&first)? {parts.splice(0..1,alias.split("::").map(str::to_string));} + if let Some(alias)=self.cpp_namespace_alias(&r.file_path,&first,r.line)? {parts.splice(0..1,alias.split("::").filter(|p|!p.is_empty()).map(str::to_string));} } let Some(name)=parts.last() else {return Ok(None)}; let candidates=self.nodes_by_name(name)?; @@ -173,3 +252,53 @@ impl KernelResolver { Ok(best.map(|node|KCand{node,confidence:0.8,resolved_by:"qualified-name"})) } } + +/// `namespace name = target;` at the start of a line (`target` keeps a leading `::`). +fn namespace_alias_pattern() -> Rc { + re!(r"(?m)^\s*namespace\s+(\w+)\s*=\s*((?:::)?[\w:]+)\s*;") +} + +/// The namespace aliases `text` (comments blanked) declares, in order: name, target, line, and +/// for one a function or other block declares, the last line of that block, where it stops +/// applying. One a namespace's body or `extern "C"` declares is the file's. +fn cpp_namespace_alias_declarations(text: &str) -> Vec<(String, String, i64, Option)> { + let found: Vec<(usize, String, String)> = namespace_alias_pattern().captures_iter(text).filter_map(|m| Some((m.get(1)?.start(), m[1].to_string(), m[2].to_string()))).collect(); + let bytes = text.as_bytes(); + let mut out: Vec<(String, String, i64, Option)> = Vec::new(); + // Each open block: whether it is a namespace's (or `extern "C"`'s). + let mut blocks: Vec = Vec::new(); + // Aliases a block declares, with how many blocks were open around them. + let mut pending: Vec<(usize, usize)> = Vec::new(); + let (mut next, mut line, mut statement, mut i) = (0usize, 1i64, 0usize, 0usize); + while i < bytes.len() { + while next < found.len() && found[next].0 <= i { + let (_, name, target) = &found[next]; + out.push((name.clone(), target.clone(), line, None)); + if blocks.iter().any(|namespace| !namespace) { pending.push((out.len() - 1, blocks.len())); } + next += 1; + } + match bytes[i] { + b'\n' => line += 1, + quote @ (b'"' | b'\'') => { + let mut j = i + 1; + while j < bytes.len() && bytes[j] != quote && bytes[j] != b'\n' { j += if bytes[j] == b'\\' { 2 } else { 1 }; } + i = j.min(bytes.len() - 1); + if bytes[i] == b'\n' { continue; } + } + b'{' => { + blocks.push(re!(r#"(?:\bnamespace\b[^;{}]*|\bextern\s*"[^"]*"\s*)$"#).is_match(&text[statement..i])); + statement = i + 1; + } + b'}' => { + blocks.pop(); + pending.retain(|&(at, depth)| if depth > blocks.len() { out[at].3 = Some(line); false } else { true }); + statement = i + 1; + } + b';' => statement = i + 1, + _ => {} + } + i += 1; + } + for (at, _) in pending { out[at].3 = Some(line); } + out +} diff --git a/codegraph-kernel/src/resolve/cpp_overloads.rs b/codegraph-kernel/src/resolve/cpp_overloads.rs index b242d75c9..b4fe68417 100644 --- a/codegraph-kernel/src/resolve/cpp_overloads.rs +++ b/codegraph-kernel/src/resolve/cpp_overloads.rs @@ -66,15 +66,22 @@ pub(super) fn cpp_type_category(ty: &str) -> CppTypeCategory { if STRING_TYPES.contains(&last.as_str()) { String } else { Class } } -/// A C++ parameter declaration's type: without its default argument and its name. -/// An array type (`Char[]`) as the pointer to its element an argument of it decays to (`Char*`). +/// An array type (`Char[]`) as the pointer to its element an argument of it decays to (`Char*`); +/// a multidimensional one (`int[][]`) as the pointer to its first row (`int[]*`). fn cpp_array_as_pointer(text: String) -> String { match super::cpp_range_for::cpp_array_split(&text) { - Some((base, dimensions)) if dimensions > 0 => format!("{base}*"), + Some((base, dimensions)) if dimensions > 0 => format!("{base}{}*", "[]".repeat(dimensions - 1)), _ => text, } } +/// Whether the C++ type `ty` is a pointer to an array: an argument's `int[]*`, a parameter's +/// `int (*rows)[4]` or `int rows[][4]`. +fn cpp_pointer_to_array(ty: &str) -> bool { + re!(r"\[\s*\]\s*\*|\(\s*\*[^()]*\)\s*\[|\]\s*\[").is_match(ty) +} + +/// A C++ parameter declaration's type: without its default argument and its name. pub(super) fn cpp_parameter_type(param: &str) -> String { let mut text = param; let mut depth = 0i32; @@ -140,13 +147,21 @@ impl KernelResolver { let tied: Vec<_> = overloads.iter().zip(&fits).filter(|(_, f)| **f == top).map(|(n, _)| n.clone()).collect(); if tied.len() == 1 || args.is_empty() { return Ok(tied[0].clone()); } let first = tied[0].clone(); - Ok(self.cpp_argument_type_choice(tied, name, r)?.unwrap_or(first)) + Ok(self.cpp_argument_type_ranking(tied, name, r)?.map_or(first, |(choice, _)| choice)) } /// Of overloads a call's arguments fit alike by count (in preference order), the one the types - /// the arguments have where the call reads them fit best; the first of those that fit best - /// alike. None when no argument's type can be read, or every overload fits the types alike. + /// the arguments have where the call reads them fit best. None when no argument's type can be + /// read, or several fit the types best alike (however many fit them worse) and no const twin + /// tells them apart: `f(short)` beside `f(long)`, `f(int)` and `f(char)` is none of them. pub(super) fn cpp_argument_type_choice(&mut self, tied: Vec>, name: &str, r: &ResolveRefIn) -> Res>> { + Ok(self.cpp_argument_type_ranking(tied, name, r)?.and_then(|(choice, decided)| decided.then_some(choice))) + } + + /// `cpp_argument_type_choice`'s ranking: the first of the overloads the argument types fit + /// best, or its const twin, and whether it alone fits them best. None when no argument's type + /// can be read. + fn cpp_argument_type_ranking(&mut self, tied: Vec>, name: &str, r: &ResolveRefIn) -> Res, bool)>> { let Some(types) = self.cpp_call_argument_types(name, r)? else { return Ok(None) }; if types.iter().all(Option::is_none) { return Ok(None); } let mut fits = Vec::with_capacity(tied.len()); @@ -155,10 +170,11 @@ impl KernelResolver { let at = fits.iter().position(|f| *f == top).unwrap_or(0); let best = tied[at].clone(); let others: Vec<_> = tied.iter().zip(&fits).enumerate().filter(|(i, (_, f))| **f == top && *i != at).map(|(_, (n, _))| n.clone()).collect(); - // Fit alike by type: decided only when a const twin is. - let alike = others.len() + 1 == tied.len(); + // Several fit the types best: decided only when a const twin is. + let alone = others.is_empty(); let choice = self.cpp_const_twin_choice(best.clone(), others, &types, r); - Ok((!alike || choice.id != best.id || at != 0).then_some(choice)) + let decided = alone || choice.id != best.id; + Ok(Some((choice, decided))) } /// The C++ function a call of `chosen`'s name reaches among `candidates` (cppFunctionOverload, @@ -297,10 +313,13 @@ impl KernelResolver { /// `DynamicCastMessage(*msg)` takes a reference, and rocksdb's `db_->Put(opts, key, ts, /// value)` on a `char key[]` no `ColumnFamilyHandle*`. A pointer or reference to a non-const /// type never takes a const object, nor a reference a value a call returns (`lvalue` false). - /// None of that holds for an argument of a template parameter's type (`dependent`), and the - /// pointer and `const` rules not for a parameter whose type the function's own template - /// parameter is (`deduced`). (Of two overloads alike but for a `const`, which one a non-const - /// object takes is `cpp_const_twin_choice`'s.) + /// An object of the parameter's own type that a name or dereference reads (`lvalue`) never + /// binds an rvalue reference: `Foo x; Take(x)` is `Take(const Foo&)`, not `Take(Foo&&)`; an + /// `int` converted to a `Value` is a temporary, which does. None of that holds for an argument of a + /// template parameter's type (`dependent`), and the pointer, `const` and rvalue rules not for a + /// parameter whose type the function's own template parameter is (`deduced`). (Of two + /// overloads alike but for a `const`, which one a non-const object takes is + /// `cpp_const_twin_choice`'s.) fn cpp_written_type_fit(&mut self, argument: &str, param: &str, category: CppTypeCategory, lvalue: Option, dependent: bool, deduced: bool) -> Res { use CppTypeCategory::*; let fit = self.cpp_unqualified_type_fit(argument, param, category)?; @@ -309,6 +328,10 @@ impl KernelResolver { if category == Pointer && argument_category == Class { return Ok(-3); } if !deduced && category == Pointer && argument_category == CString && !re!(r"\bvoid\b").is_match(param) { return Ok(-3); } if category == CString && argument_category == Pointer { return Ok(-3); } + // A pointer to an array (`int a[3][4]` decayed) is no pointer to its element, nor the other + // way round; `void*` takes either. + if !deduced && matches!(category, Pointer | CString) && cpp_pointer_to_array(argument) != cpp_pointer_to_array(param) && !re!(r"\bvoid\b").is_match(param) { return Ok(-3); } + if !deduced && lvalue == Some(true) && fit == 3 && re!(r"&&\s*$").is_match(&strip_template_arguments(param)) { return Ok(-3); } if cpp_refers_to_const(param) != Some(false) { return Ok(fit); } if !deduced && cpp_argument_const(argument, param) == Some(true) { return Ok(-3); } Ok(if lvalue == Some(false) && !strip_template_arguments(param).contains('*') { -3 } else { fit }) diff --git a/codegraph-kernel/src/resolve/cpp_range_for.rs b/codegraph-kernel/src/resolve/cpp_range_for.rs index e371f3e20..9bd197737 100644 --- a/codegraph-kernel/src/resolve/cpp_range_for.rs +++ b/codegraph-kernel/src/resolve/cpp_range_for.rs @@ -618,8 +618,13 @@ impl KernelResolver { "field_expression" => { let Some((object, method, template_arguments)) = self.cpp_member_object_of(function, source, r, depth)? else { return Ok(None) }; if template_arguments.is_none() { - // `logs_.begin()`, `map.find(key)`: the iterator a library container hands out. - if let Some(iterator) = cpp_library_iterator_of(&object, &method) { return Ok(Some(iterator)); } + // `logs_.begin()`, `map.find(key)`: the iterator a library container hands + // out. (A container a name declares `const` keeps it: its declared type + // leaves the `const` out.) + let constant = function.child_by_field_name("argument").filter(|a| a.kind() == "identifier") + .is_some_and(|name| super::cpp_receivers::cpp_declared_const(name, source)); + let container = if constant { CppWritten { text: format!("const {}", object.text), site: object.site.clone() } } else { object.clone() }; + if let Some(iterator) = cpp_library_iterator_of(&container, &method) { return Ok(Some(iterator)); } // `ws.front()`, `m.at(key)`, `p.get()`, `s.value()`: what it holds. if let Some(element) = self.cpp_library_element_of(&object, &method, r)? { return Ok(Some(element)); } } @@ -725,11 +730,16 @@ impl KernelResolver { /// `std::deque::const_iterator`, protobuf's `SourceCodeInfo_Location` for /// `RepeatedPtrField::iterator`. None for any other type, and for an /// element a class template's own `begin()` or `operator*` writes with its template parameter - /// (googletest's `ParamGenerator::iterator`), which the specialization decides. + /// (googletest's `ParamGenerator::iterator`), which the specialization decides. A + /// `const_iterator`'s element is const (`const Slice`); a pointer element stays as it is. pub(super) fn cpp_iterator_target(&mut self, iterator: CppWritten, r: &ResolveRefIn) -> Res> { let Some(container) = iterator_container(&iterator.text) else { return Ok(None) }; let Some(element) = self.cpp_range_element_of(CppWritten { text: container, site: iterator.site.clone() }, r, 1)? else { return Ok(None) }; if element.site.from_node_id != iterator.site.from_node_id && self.cpp_written_template_parameter(&element)? { return Ok(None); } + let constant = re!(r"::\s*const_(?:reverse_)?iterator\s*$").is_match(&without_type_keywords(&iterator.text)); + if constant && !top_level_pointer(&element.text) && !re!(r"^\s*const\b").is_match(&element.text) { + return Ok(Some(CppWritten { text: format!("const {}", element.text), site: element.site })); + } Ok(Some(element)) } @@ -1008,7 +1018,7 @@ impl KernelResolver { /// The iterator the member `member` of a standard library or abseil container of the type `object` /// returns, where the container's type is written: `std::deque::iterator` for /// rocksdb's `logs_.begin()`, whose `->` reaches the container's element (`cpp_iterator_target`). -/// None for any other member or type. +/// A const container's is its `const_iterator`. None for any other member or type. fn cpp_library_iterator_of(object: &CppWritten, member: &str) -> Option { let kind = ITERATOR_MEMBERS.iter().find(|(m, _)| *m == member)?.1; if top_level_pointer(&object.text) { return None; } @@ -1016,6 +1026,8 @@ fn cpp_library_iterator_of(object: &CppWritten, member: &str) -> Option bool { let name = &source.text()[node.byte_range()]; - let leading = |declaration: tree_sitter::Node| declaration.child_by_field_name("type") - .is_some_and(|ty| re!(r"\b(?:const|constexpr)\b").is_match(&source.text()[declaration.start_byte()..ty.start_byte()])); + let leading = |declaration: tree_sitter::Node| declaration.child_by_field_name("type").is_some_and(|ty| { + let head = &source.text()[declaration.start_byte()..ty.start_byte()]; + re!(r"\bconst\b").is_match(head) || (re!(r"\bconstexpr\b").is_match(head) && !cpp_binds_pointer(declaration, name, source)) + }); let mut at = node; while let Some(parent) = at.parent() { let block = matches!(parent.kind(), "compound_statement" | "declaration_list" | "translation_unit"); @@ -671,6 +674,21 @@ pub(super) fn cpp_declared_const(node: tree_sitter::Node, source: &SourceFile) - false } +/// Whether the declarator of `declaration` that binds `name` declares a pointer: `p` in +/// `constexpr Widget* p = &w;`. +fn cpp_binds_pointer(declaration: tree_sitter::Node, name: &str, source: &SourceFile) -> bool { + let mut cursor = declaration.walk(); + let found = declaration.children(&mut cursor).enumerate() + .find(|(i, child)| declaration.field_name_for_child(*i as u32) == Some("declarator") && cpp_declarator_binds(*child, name, source)) + .map(|(_, child)| child); + let Some(mut declarator) = found else { return false }; + if declarator.kind() == "init_declarator" { + let Some(inner) = declarator.child_by_field_name("declarator") else { return false }; + declarator = inner; + } + declarator.kind() == "pointer_declarator" +} + pub(super) fn cpp_ast_declared_type(ty: tree_sitter::Node, mut declarator: tree_sitter::Node, source: &SourceFile) -> String { let type_node = if matches!(ty.kind(), "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier") { ty.child_by_field_name("name").unwrap_or_else(|| { diff --git a/codegraph-kernel/src/resolve/cpp_specializations.rs b/codegraph-kernel/src/resolve/cpp_specializations.rs index e079ed72e..d100b3e2f 100644 --- a/codegraph-kernel/src/resolve/cpp_specializations.rs +++ b/codegraph-kernel/src/resolve/cpp_specializations.rs @@ -17,17 +17,29 @@ impl KernelResolver { /// - a call that writes none, or another reference, reaches a specialization only when there /// is nothing else. /// - /// The candidates as they are when none is a specialization. + /// The candidates as they are when none is a specialization. Each template's specializations + /// choose among the candidates of its own qualified name only: one of `a::f` leaves a call's + /// `b::f` in place, for the call's scope to choose between them. pub(super) fn cpp_specialization_pool(&mut self, candidates: Vec>, r: &ResolveRefIn) -> Vec> { if r.language != "cpp" { return candidates; } let written: Vec>>> = candidates.iter().map(|n| self.cpp_specialization_arguments(n)).collect(); if written.iter().all(Option::is_none) { return candidates; } - let others: Vec> = candidates.iter().zip(&written).filter(|(_, w)| w.is_none()).map(|(n, _)| n.clone()).collect(); - let Some(args) = self.cpp_call_template_arguments(r) else { return if others.is_empty() { candidates } else { others } }; - let named: Vec> = candidates.iter().zip(&written) - .filter(|(_, w)| w.as_ref().is_some_and(|w| cpp_template_arguments_match(w, &args))) - .map(|(n, _)| n.clone()).collect(); - if named.is_empty() { others } else { named } + let args = self.cpp_call_template_arguments(r); + let mut families: Vec<&str> = Vec::new(); + for n in &candidates { if !families.contains(&n.qualified_name.as_str()) { families.push(&n.qualified_name); } } + let mut kept: HashSet = HashSet::new(); + for family in families { + let members: Vec = (0..candidates.len()).filter(|&i| candidates[i].qualified_name == family).collect(); + let others: Vec = members.iter().copied().filter(|&i| written[i].is_none()).collect(); + if others.len() == members.len() { kept.extend(members); continue; } + let Some(args) = &args else { + kept.extend(if others.is_empty() { members } else { others }); + continue; + }; + let named: Vec = members.iter().copied().filter(|&i| written[i].as_ref().is_some_and(|w| cpp_template_arguments_match(w, args))).collect(); + kept.extend(if named.is_empty() { others } else { named }); + } + candidates.into_iter().enumerate().filter(|(i, _)| kept.contains(i)).map(|(_, n)| n).collect() } /// The template arguments the C++ function or method `n` is an explicit specialization for, @@ -72,7 +84,8 @@ impl KernelResolver { /// The template arguments `text` writes right after `name`, the first time from `from` on it is /// written as a word of its own, as they compare (cpp_template_argument_key). None when it writes -/// none there, or `<>`. +/// none there, or `<>`. A receiver or qualifier of the same name is passed over: in +/// `foo.foo()` the arguments follow the second `foo`. fn cpp_template_arguments_after(text: &str, name: &str, from: usize) -> Option> { if name.is_empty() { return None; } let bytes = text.as_bytes(); @@ -80,7 +93,8 @@ fn cpp_template_arguments_after(text: &str, name: &str, from: usize) -> Option|::)").is_match(text.get(open..).unwrap_or("")); + if !word(start.checked_sub(1).and_then(|i| bytes.get(i))) && !word(bytes.get(open)) && !receiver { let end = if bytes.get(open) == Some(&b'<') { cpp_template_arguments_end(bytes, open) } else { None }; let args: Vec = end.map(|end| split_cpp_top_level(&text[open + 1..end]).iter().map(|a| cpp_template_argument_key(a)).collect()).unwrap_or_default(); return (!args.is_empty()).then_some(args); diff --git a/codegraph-kernel/src/resolve/cpp_types.rs b/codegraph-kernel/src/resolve/cpp_types.rs index 0f5f9be8e..58075f39e 100644 --- a/codegraph-kernel/src/resolve/cpp_types.rs +++ b/codegraph-kernel/src/resolve/cpp_types.rs @@ -136,7 +136,7 @@ impl KernelResolver { } // A name written through a macro that stands for a namespace, or the other way round. if ty.contains("::") { - if let Some(spelled) = self.cpp_name_macro_spelling(&r.file_path, ty)? { + if let Some(spelled) = self.cpp_name_macro_spelling(r, ty)? { let raw = if raw.trim_start().starts_with("::") { format!("::{spelled}") } else { spelled.clone() }; if let [owner] = self.cpp_scoped_types(&raw, &spelled, r, included)?.as_slice() { return self.cpp_expand_owner(owner, r, depth, constructor, included); } } @@ -144,7 +144,7 @@ impl KernelResolver { // A name written through a namespace alias another file declares: protobuf hpb's // `backend::Arena` under `namespace backend = hpb::internal::backend::upb;`. if let Some((head, rest)) = ty.split_once("::").filter(|_| !raw.trim_start().starts_with("::")) { - if let Some(target) = self.cpp_namespace_alias(&r.file_path, head)? { + if let Some(target) = self.cpp_namespace_alias(&r.file_path, head, r.line)? { if let Some(found) = self.cpp_type_owner_visible(&format!("{target}::{rest}"), r, depth + 1, constructor, included)? { return Ok(Some(found)); } } } diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 897423217..cfdd60e3c 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -321,11 +321,11 @@ impl KernelResolver { || !c_source_ext_re().is_match(&candidate.file_path) || !self.is_static_c_function(candidate)? || (matches!(r.language.as_str(), "c" | "cpp") - && (self.c_includes(&r.file_path, &candidate.file_path)? - || self.c_sole_includer(candidate, &r.file_path)? - // Or by any path its include writes: hiredis's async.c includes - // `deps/hiredis/dict.c`. - || self.namespace_visible_files(&r.file_path, &r.language)?.contains(&candidate.file_path))); + && (self.c_sole_includer(candidate, &r.file_path)? + // Or through an include written before the call, by any path its + // include writes and through the headers it includes: hiredis's + // async.c includes `deps/hiredis/dict.c`. Not one under `#if 0`. + || self.cpp_included_before(&r.file_path, r.line, &candidate.file_path, &r.language)?)); // So is what a source file's unnamed namespace declares, and a type only source files // declare, with its members (cpp_translation_unit). return Ok(static_seen && !self.cpp_translation_unit_local(candidate, r)?); diff --git a/codegraph-kernel/src/resolve/pipeline.rs b/codegraph-kernel/src/resolve/pipeline.rs index 3c958eeb2..4991c99d8 100644 --- a/codegraph-kernel/src/resolve/pipeline.rs +++ b/codegraph-kernel/src/resolve/pipeline.rs @@ -782,7 +782,9 @@ if super::lang_scope::is_dart_member_read(r) { if let Some(result) = self.cpp_plain_call(r)? { return match result { Some(candidate) => self.finish_pre_framework(r, candidate), None => Ok(self.refused()) }; } - if let Some(candidate) = self.cpp_implicit_this(r)? { return self.finish_pre_framework(r, candidate); } + if let Some(result) = self.cpp_implicit_this(r)? { + return match result { Some(candidate) => self.finish_pre_framework(r, candidate), None => Ok(self.refused()) }; + } if r.language == "cpp" && r.reference_kind == "calls" { if let McShape::Parsed { receiver, .. } = self.method_call_shape(r)? { if self.cpp_external_receiver(&receiver, r)? { return Ok(ResolveOutcome::unresolved()); }