Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading.

- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr<Iterator> iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr<FileSystem>` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr<std::deque<…>>`, the call no longer links to a project method. Re-index C++ projects after upgrading.
- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr<Iterator> iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr<FileSystem>` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr<std::deque<…>>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is left unlinked. Re-index C++ projects after upgrading.

- `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `<Route path="/docs" element={<Docs />} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind.

Expand All @@ -575,6 +575,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph sync` and the file watcher now update the files that import through a module that re-exports what it imports, like a package's `__init__.py` or an `index.ts` barrel, when that module is deleted or added, and in Python also the files that reach it through another package re-exporting it. Before, deleting `shop/__init__.py` left every `from shop import Order` linked to the class it re-exported, and deleting `components/index.ts` left every `import { Button } from './components'` linked, until each importing file changed or the project was re-indexed. When such a module came back, the imports stayed linked the way they were while it was missing, and in a large Python project an import of a module whose name is as common as `models` or `test`, like `from django.test import TestCase`, stayed unlinked. Python, JavaScript, TypeScript, Vue, Svelte and Astro.
- In JavaScript and TypeScript, a file that re-exports a module, like an `index.ts` with `export * from './button'` or `export { Button } from './button'`, now counts as importing that module, the way an `import` statement does. `codegraph affected` and a module's dependents now include the barrels that re-export it; before, a barrel made only of `export *` lines showed no dependency at all. Re-index JavaScript and TypeScript projects after upgrading.
- `codegraph sync` and the file watcher now also update the files that import through an `index.ts` re-exporting another barrel, like `export * from './dropdown-menu'`, when that inner barrel is deleted or added. Before, their imports stayed linked through the deleted barrel, or stayed linked the way they were while it was missing, until each importing file changed or the project was re-indexed.
- An import in a Vue, Svelte or Astro component now links the component to the file it imports, as an import in a script does, and so does an import of a scoped package that a `tsconfig` path or a workspace package maps to a project file, like `import { debounce } from '@excalidraw/common'`. Before, only the names such an import binds were linked, so a barrel or a module imported for its side effects showed no dependency from those files in `codegraph affected`, and `codegraph sync` could not update them when a barrel they import through was deleted or added. `codegraph sync` and the file watcher also link such an import again, and one written as `.` or `..`, when the file it names comes back. Re-index JavaScript, TypeScript, Vue, Svelte and Astro projects after upgrading.
- `codegraph sync` and the file watcher now move an import written without an extension, like `import { sizeOf } from './button'`, to a file added beside the one it found before when the new file comes first, such as `button.ts` next to `button.vue`. Before, the import and the names it binds stayed linked to `button.vue` until the importing file changed or the project was re-indexed. JavaScript, TypeScript, Vue, Svelte and Astro.

- In Kotlin, a call on a typed receiver now links to an extension function declared in a package, like `value.shout()` or the infix `a join b` with `value: Foo` and `fun Foo.shout()` under `package demo`, including one declared in another file of the package. Before, it linked only when the file had no `package` line, so the usual Kotlin project got no edge. On JetBrains Exposed, 1,020 such calls now link (`Table.selectAll()` to `FieldSet.selectAll`, `transaction.throwUnsupportedException(…)`), and none that linked before is lost. A member of the receiver's class still wins over an extension of the same name.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

## About this fork

This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`041db101`](https://github.com/colbymchenry/codegraph/commit/041db101) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.
This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`c19f0284`](https://github.com/colbymchenry/codegraph/commit/c19f0284) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.

The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below).

Expand Down
41 changes: 38 additions & 3 deletions __tests__/cpp-smart-pointer-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
* `Iterator` the call means inherits `Valid` from `IteratorBase`. A template
* parameter, a library type and an alias of a type outside the project hold
* no project class, so those calls get no edge; neither does a call the held
* class has no method for (unless a class template's specialization may
* declare it). A held type the project doesn't declare — a generated message
* — leaves the call as it was.
* class has no method for (unless a class template's specialization, or a
* base the index doesn't link, may declare it). A held type the project
* doesn't declare — a generated message — leaves the call as it was.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
Expand Down Expand Up @@ -454,4 +454,39 @@ describe('what keeps the call as it was', () => {
cg.close();
}
});

it('a held class with a base the index does not link, which may declare the method', async () => {
// protobuf's FlatAllocator derives from a `decltype(…)` that names FlatAllocatorImpl.
const cg = await indexed({
'src/google/protobuf/descriptor.cc': lines(
'#include <memory>',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <typename... T>',
'class FlatAllocatorImpl {',
' public:',
' void PlanArray(int array_size) {}',
'};',
'template <typename... T>',
'FlatAllocatorImpl<T...> ApplyTypeList(int);',
'class FlatAllocator : public decltype(ApplyTypeList<char, int>(0)) {};',
'} // namespace internal',
'void PlanAllocationSize(std::unique_ptr<internal::FlatAllocator>& alloc) {',
' alloc->PlanArray(1);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
// The held-type lookup leaves this call alone (the base is unlinked), and the
// fork's fallback guesses no method by its name for a receiver whose class it
// knows but which lacks the method, so the call stays unresolved (as for a
// reference in cpp-declared-type-receiver.test.ts).
expect(calls(cg, 'google::protobuf::PlanAllocationSize')).toEqual([]);
} finally {
cg.close();
}
});
});
182 changes: 182 additions & 0 deletions __tests__/cpp-string-literal-declarations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* A declaration written inside a C++ string literal is text, not code, so
* receiver inference doesn't take it for the receiver's declaration.
*
* protocolbuffers/protobuf's upb generator emits C code from raw string
* templates: `GenerateRepeatedSetters(upb::FieldDefPtr field, …)` writes
* `upb_MiniTableField field = $field_init$;` inside an `R"cc( … )cc"`, then
* calls `field.ctype()` on its own parameter. Reading back from the call
* found the template's line first, took `field` for a `upb_MiniTableField` —
* a C struct with no methods — and the call got no edge (before #2493, a
* guess by the method's name).
*
* A raw string can span many lines and hold quotes and comment markers, so
* its lines are found by reading the file once, as the compiler does. An
* ordinary string's contents are blanked on the line being read.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-strdecl-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}

function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}

/** `calls` callees of a function or method, as `qualifiedName`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.qualifiedName)
.sort();
}

const lines = (...l: string[]): string => [...l, ''].join('\n');

/** upb's field wrapper, and the C struct a generated template declares. */
const UPB = {
'upb/reflection/def.hpp': lines(
'namespace upb {',
'class FieldDefPtr {',
' public:',
' int ctype() const { return 0; }',
' const char* name() const { return ""; }',
'};',
'} // namespace upb',
),
'upb/mini_table/field.h': lines(
'typedef struct upb_MiniTableField {',
' int number;',
'} upb_MiniTableField;',
),
};

describe('a declaration inside a string literal is not the receiver\'s', () => {
it('reproduction: upb\'s generator declares `field` in the C code a raw string emits', async () => {
const cg = await indexed({
...UPB,
'upb_generator/c/generator.cc': lines(
'#include "upb/reflection/def.hpp"',
'#include "upb/mini_table/field.h"',
'namespace upb {',
'namespace generator {',
'void Emit(const char* text) {}',
'void GenerateRepeatedSetters(upb::FieldDefPtr field) {',
' Emit(R"cc(',
' UPB_INLINE void $0_resize(void* msg, size_t size) {',
' upb_MiniTableField field = $1;',
' // "a quote" and ) in the template',
' upb_Message_Resize(msg, &field, size);',
' }',
' )cc");',
' if (field.ctype() == 1) Emit(field.name());',
'}',
'} // namespace generator',
'} // namespace upb',
),
});
try {
expect([...new Set(calls(cg, 'upb::generator::GenerateRepeatedSetters'))]).toEqual([
'upb::FieldDefPtr::ctype',
'upb::FieldDefPtr::name',
'upb::generator::Emit',
]);
} finally {
cg.close();
}
});

it('an ordinary string that reads like a declaration', async () => {
const cg = await indexed({
'src/runner.h': lines(
'namespace demo {',
'class Runner {',
' public:',
' void Run() {}',
'};',
'class Stub {',
' public:',
' void Run() {}',
'};',
'class Printer {',
' public:',
' void Print(const char* text) {}',
'};',
'} // namespace demo',
),
'src/generator.cc': lines(
'#include "src/runner.h"',
'namespace demo {',
'void Generate(Printer* printer, Runner* runner) {',
' printer->Print("Stub* runner = nullptr;");',
' runner->Run();',
'}',
'} // namespace demo',
),
});
try {
expect(calls(cg, 'demo::Generate')).toEqual(['demo::Printer::Print', 'demo::Runner::Run']);
} finally {
cg.close();
}
});
});

describe('what a literal on the line does not hide', () => {
it('a declaration after a character literal holding a quote, or after a comment holding `R"(`', async () => {
const cg = await indexed({
'src/runner.h': lines(
'namespace demo {',
'class Runner {',
' public:',
' void Run() {}',
'};',
'class Stub {',
' public:',
' void Run() {}',
'};',
'inline Runner* MakeRunner() { return nullptr; }',
'} // namespace demo',
),
'src/quotes.cc': lines(
'#include "src/runner.h"',
'namespace demo {',
'void Quote(Stub* runner) {',
' char q = \'"\'; int n = 1\'000; Runner* runner = MakeRunner();',
Comment thread
bompus marked this conversation as resolved.
' runner->Run();',
'}',
'void Comment(Stub* runner) {',
' /* R"( not a raw string */ Runner* runner = MakeRunner();',
' runner->Run();',
'}',
'} // namespace demo',
),
});
try {
expect(calls(cg, 'demo::Quote')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']);
expect(calls(cg, 'demo::Comment')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']);
} finally {
cg.close();
}
});
});
Loading
Loading