Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fe7542b
fix(vbnet): index names that begin with a keyword (SharedCache, Dimen…
colbymchenry Oct 6, 2026
5b73d0f
fix(js,ts): find a store exported on a later line when its name conta…
colbymchenry Oct 6, 2026
6421644
fix(dart): link calls in top-level and field initializers (#2367)
colbymchenry Oct 6, 2026
b7e05f3
fix(dart): link a chained call only through its receiver's declared t…
colbymchenry Oct 6, 2026
5a11cd7
fix(vbnet): link fields and properties read or written through a valu…
colbymchenry Oct 6, 2026
0571286
fix(js,ts): link calls through an import whose name contains $ (#2378)
colbymchenry Oct 6, 2026
4d6168a
fix(dart): a bare call to a parameter or local links nothing (#2379)
colbymchenry Oct 6, 2026
652c36b
fix(dart): index const constructors and redirecting factories; a call…
colbymchenry Oct 6, 2026
3f96f80
fix(dart): link generic calls the parser reads as two comparisons (#2…
colbymchenry Oct 6, 2026
1b752de
fix(dart): a member with no body keeps its dartdoc and annotations (#…
colbymchenry Oct 6, 2026
d500e58
fix(dart): link a call chained on a generic lookup like BlocProvider.…
colbymchenry Oct 6, 2026
5f5b8db
fix(js,ts): find a store exported by an export { } list when its name…
colbymchenry Oct 6, 2026
249d9a8
fix(dart): a comment inside a member chain no longer loses a call or …
colbymchenry Oct 6, 2026
0c860c0
fix(dart): a bare call or type name reaches only what its library can…
colbymchenry Oct 6, 2026
9924684
fix(dart): a member keeps the dartdoc written above its annotations (…
colbymchenry Oct 6, 2026
631df01
fix(dart): an import or export links the file its URI names, not a na…
colbymchenry Oct 6, 2026
d2fb323
fix(dart): an annotation links the constant or constructor it names, …
colbymchenry Oct 6, 2026
837a186
fix(dart): a call through an import prefix reaches only what that imp…
colbymchenry Oct 6, 2026
83f54a1
fix(dart): a library links the files its part directives name (#2391)
colbymchenry Oct 6, 2026
3f5a9e2
fix(sync): an import links the file it names when that file appears l…
colbymchenry Oct 6, 2026
256fa48
merge: reconcile upstream through 3f5a9e2e
bompus Oct 7, 2026
460f6b8
fix(dart): locate multiline receivers at their token position
bompus Oct 7, 2026
2a3b091
fix(dart): contain library directive reads within the project
bompus Oct 7, 2026
81229ef
fix(dart): bound local scopes across bracket strings
bompus Oct 7, 2026
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
48 changes: 47 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions 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 [`aeb8f955`](https://github.com/colbymchenry/codegraph/commit/aeb8f95581f23946d04b5ae9b3dbda918f5a7700) (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 [`3f5a9e2e`](https://github.com/colbymchenry/codegraph/commit/3f5a9e2e1d6b2feb51cbc23cc2a757487cbbf47a) (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 All @@ -76,7 +76,7 @@ Then run `codegraph init` in each project, as in [Get Started](#get-started). In

### What the fork adds

Compared with upstream `main` at `aeb8f955` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.
Compared with upstream `main` at `3f5a9e2e` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.

| Feature | Upstream | Fork | What it does |
|---|:-:|:-:|---|
Expand Down Expand Up @@ -111,15 +111,15 @@ The other languages are the same in both, listed under [Supported Languages](#su

C# property accessors and expression-bodied properties contribute calls and references owned by the property. VB.NET member bodies and field initializers also contribute their calls and references.

C# field and property initializers retain their calls and references under the member that owns them. Target-typed `new()` resolves relative declared types through enclosing namespaces and honors `global::` qualification without requiring a redundant `using`. VB.NET resolves typed receivers, enclosing and inherited members, and Shared member reads without choosing unrelated project declarations.
C# field and property initializers retain their calls and references under the member that owns them. Target-typed `new()` resolves relative declared types through enclosing namespaces and honors `global::` qualification without requiring a redundant `using`. VB.NET resolves typed receivers, enclosing and inherited members, and field or property reads through values and Shared types without choosing unrelated project declarations. Names beginning with keywords, such as `SharedCache`, `Dimension` and `NewItem`, retain their declarations.

Go imports follow the nearest indexed module and the longest matching module path, including module changes during incremental sync. Unexported receivers and embedded methods stay in their declaring package. Dart getter reads become calls only when the receiver type reaches that getter and no nearer field in the visible class hierarchy overrides it; enum extensions and type-position references participate in resolution. Rust enum-variant values retain their enum references.
Go imports follow the nearest indexed module and the longest matching module path, including module changes during incremental sync. Unexported receivers and embedded methods stay in their declaring package. Dart imports, exports and part directives follow their library URIs and visibility rules. Library directive reads stay inside the indexed project, including resolved symlink targets. Calls through import prefixes, annotations and member chains follow the visible declaration and written receiver types, including explicit generic lookup types. Parameters and locals shadow bare calls. Top-level and field initializers contribute calls; const constructors, redirecting factories and annotated members retain their declarations and dartdoc. Getter reads become calls only when the receiver type reaches that getter and no nearer field in the visible class hierarchy overrides it; enum extensions and type-position references participate in resolution. Rust enum-variant values retain their enum references.

Named JavaScript and TypeScript object literals own their function members, including local objects and classic-script global assignments. Member calls follow the visible object; loop-local objects stay within their scope. Destructured member calls resolve across lines while preserving the source binding at the destructure declaration; unrelated bare names stay unresolved. Vue template expressions contribute calls to script bindings while preserving component ownership. Encoded attribute expressions retain their original source positions, and template-local bindings stay within their scope.

A COBOL copybook named in an explore query prioritizes its indexed source and lists its COPY and EXEC SQL INCLUDE sites. Missing indexed source is reported explicitly.

`codegraph status` reports files that need re-indexing and files with recorded parse errors. `status --json` includes `index.filesNeedingReindex` and `index.filesWithParseErrors`; `files --json` includes each file's extraction errors. A transient parser failure preserves the previous graph and retries on the next sync.
`codegraph status` reports files that need re-indexing and files with recorded parse errors. `status --json` includes `index.filesNeedingReindex` and `index.filesWithParseErrors`; `files --json` includes each file's extraction errors. A transient parser failure preserves the previous graph and retries on the next sync. When a file named by an unresolved import appears later, incremental sync retries that import without selecting unrelated namesakes.

The MCP launcher can replace a daemon from an older release when its hello confirms coordinated writer handover. `serve --mcp --path <root> --preserve-existing` opts out of replacement on initial connection and reconnect, requires an index at that exact root, and keeps fallback reads without a watcher. Adding `--initialize-index` lets the elected daemon create a missing exact-root index after acquiring writer ownership. Legacy daemons stay running while new sessions serve reads without auto-sync; stop the old MCP sessions and daemon, then reconnect with the current install. A daemon exits when its installation is deleted or its package version changes. Different managed builds of the same release retain the fork's version-identity checks.

Expand Down
247 changes: 247 additions & 0 deletions __tests__/dart-annotated-member-docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/**
* A Dart member's annotations sit between it and the dartdoc written above
* them:
*
* /// Builds the widget.
* @override
* Widget build(BuildContext context) { … }
*
* Both extractors read a docstring from the comments directly before the
* declaration and stopped at the first node that was not a comment, so the
* annotation ended the run before the `///` was reached and the member lost
* its documentation. `@override`, `@protected`, `@mustCallSuper`, `@pragma`,
* `@internal` and `@visibleForTesting` are everywhere in Flutter code.
*
* The walk now steps over annotations, stacked and multi-line ones included,
* and a comment between an annotation and the member joins the dartdoc above
* the way adjacent comments already did. Whatever else comes first (a field,
* the previous member's body, a top-level variable, an import) still ends it.
* A class, mixin, extension, enum or typedef kept its dartdoc already: its
* annotations open its own node, so the dartdoc above them is that node's
* previous sibling.
*
* Runs against the native kernel (when built) and the generic walker over native parse trees, which
* must agree.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult } from '../src/types';

const KERNEL_PATH = path.join(
__dirname,
'..',
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
);
const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';

const SOURCE = `// Copyright 2026 the authors.
import 'package:meta/meta.dart';

@visibleForTesting
void afterImport() {}

/// Runs at startup.
@pragma('vm:entry-point')
void main() {}

/// Calls into the platform.
@pragma('vm:external-name', 'version')
external int platformVersion();

/// Watches the counter.
@riverpod
@Deprecated('Use the generated provider')
int counter(Ref ref) => 0;

/// The answer.
final answer = 42;

@visibleForTesting
void afterVariable() {}

abstract class Repository {
/// Loads the user with [id].
@protected
Future<User> load(String id);
}

/// A counter.
@immutable
class Counter {
/// Starts at zero.
@literal
const Counter.zero() : value = 0;

/// Reads a counter from JSON.
@visibleForTesting
factory Counter.fromJson(Map<String, Object?> json) => const Counter.zero();

/// Builds the widget.
@override
Widget build(BuildContext context) => const Text('0');

/// Releases resources.
///
/// Call it once.
@protected
@mustCallSuper
void dispose() {}

/// The current count.
@override
int get count => value;

/// Use [build] instead.
@Deprecated(
'Use build. '
'Removed in 2.0.',
)

Widget render() => const Text('');

/// Joined with the comment below the annotation.
@protected
// ignore: must_call_super
void joined() {}

/// The cached value.
@JsonKey(name: 'value')
final int value;

@override
void afterField() {}

void before() {}
@override
void afterBody() {}
}

/// A mixin.
@internal
mixin Logging on Counter {}

/// An extension.
@internal
extension CounterX on Counter {}

/// An enum.
@JsonEnum()
enum Mode { light, dark }

/// A typedef.
@internal
typedef Callback = void Function();
@visibleForTesting
void afterTypedef() {}
`;

const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;

describe('Dart members keep the dartdoc written above their annotations', () => {
let savedEnv: Record<string, string | undefined> = {};

beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['dart']);
});

beforeEach(() => {
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
resetKernelForTests();
});

afterEach(() => {
for (const k of ENV_KEYS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
resetKernelForTests();
});

function extract(backend: 'kernel' | 'generic', source: string): ExtractionResult {
if (backend === 'generic') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource('lib/counter.dart', source, 'dart');
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract('lib/counter.dart', source, 'dart');
expect(result, 'kernel extraction').not.toBeNull();
return result!;
}

const backends = kernelAvailable ? (['kernel', 'generic'] as const) : (['generic'] as const);

for (const crlf of [false, true]) {
it.each(backends)(`%s${crlf ? ' (CRLF)' : ''}`, (backend) => {
const result = extract(backend, crlf ? SOURCE.replace(/\n/g, '\r\n') : SOURCE);
const node = (qualifiedName: string) => {
const found = result.nodes.find((n) => n.qualifiedName === qualifiedName);
expect(found, qualifiedName).toBeDefined();
return found!;
};
const doc = (qualifiedName: string) => node(qualifiedName).docstring;
const decorators = (qualifiedName: string): string[] => {
const id = node(qualifiedName).id;
return result.unresolvedReferences
.filter((r) => r.referenceKind === 'decorates' && r.fromNodeId === id)
.map((r) => r.referenceName)
.sort();
};

// Top-level functions, an external one and stacked annotations.
expect(doc('main')).toBe('Runs at startup.');
expect(doc('platformVersion')).toBe('Calls into the platform.');
expect(doc('counter')).toBe('Watches the counter.');

// Members: methods, a getter, a factory, a member with no body and a
// `const` constructor (both read from before their `declaration`).
expect(doc('Counter::build')).toBe('Builds the widget.');
expect(doc('Counter::count')).toBe('The current count.');
expect(doc('Counter::fromJson')).toBe('Reads a counter from JSON.');
expect(doc('Repository::load')).toBe('Loads the user with [id].');
expect(doc('Counter::zero')).toBe('Starts at zero.');
expect(doc('Counter::dispose')).toBe('Releases resources.\n\nCall it once.');

// A multi-line annotation, with a blank line before the member.
expect(doc('Counter::render')).toBe('Use [build] instead.');

// A comment between the annotation and the member joins the dartdoc,
// as adjacent comments do.
expect(doc('Counter::joined')).toBe(
'Joined with the comment below the annotation.\nignore: must_call_super'
);

// What comes before the annotations still ends the walk: an import, a
// top-level variable, a field (whose dartdoc and annotation are its
// own), the previous member's body and another declaration.
expect(doc('afterImport')).toBeUndefined();
expect(doc('afterVariable')).toBeUndefined();
expect(doc('Counter::afterField')).toBeUndefined();
expect(doc('Counter::afterBody')).toBeUndefined();
expect(doc('afterTypedef')).toBeUndefined();

// Class-like declarations kept theirs already: the annotations open
// their own node.
expect(doc('Counter')).toBe('A counter.');
expect(doc('Logging')).toBe('A mixin.');
expect(doc('CounterX')).toBe('An extension.');
expect(doc('Mode')).toBe('An enum.');
expect(doc('Callback')).toBe('A typedef.');

// The annotations are still recorded where they were.
expect(decorators('counter')).toEqual(['Deprecated', 'riverpod']);
expect(decorators('Counter::build')).toEqual(['override']);
expect(decorators('Counter::dispose')).toEqual(['mustCallSuper', 'protected']);
expect(decorators('Counter::zero')).toEqual(['literal']);
expect(decorators('Counter::afterField')).toEqual(['override']);
});
}
});
Loading