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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `<script setup>` or of a TypeScript or JavaScript file (`const { t } = useI18n()`) now link to the function they call, so `callers` and impact for a composable or helper used this way are no longer mostly empty. A `v-for` item or slot prop is never taken for a project function, a handler like `@click="open = false"` no longer links to an unrelated function of the same name, and a component's own function named like a Nuxt helper, such as `clearError`, is now the one its calls reach. Thanks @deniskern for the report, and @drakeo338 and @L0garithmic. (#2340)
- In COBOL, asking `codegraph_explore` (or `codegraph explore` and `codegraph context`) about a copybook by name, such as `CVACT01Y` or a member brought in with `EXEC SQL INCLUDE`, now answers with the copybook's own source when it is indexed and every `COPY` or `EXEC SQL INCLUDE` statement that includes it, and says so when the copybook's source isn't part of the project. Before, it answered "No relevant code found", or showed an unrelated program whose name was a letter or two away. Thanks @popolusiak for the report. (#2342)
- In Dart, reading a getter such as `box.area` or `status.label` now counts as calling it, so `codegraph callers` and impact list the code that reads it — whether the getter is declared on the class, inherited, or added by an extension — and a method an extension adds to an enum, like `shape.shout()`, now links as well. A plain field, a getter of a type from outside your project, or a value whose type isn't written down still links nothing. Re-index Dart projects after upgrading. Thanks @brookly255-lv for the report. (#2338)
- In Dart, a type is now linked from more of the places it is written, not only from parameters and return types: the type an `extension … on` targets, a field's type, a top-level variable's type, generic arguments such as `Future<Report?>.value(null)` or Riverpod's `final reportProvider = Family<Report?, String>()`, and a local variable's type, a cast or a type check inside a function. So `callers` and impact for a model class now reach the extensions, models and providers that use it. Thanks @mg-mg-mg for the report. (#2327)

## [1.6.2] - 2026-10-03

Expand Down
165 changes: 165 additions & 0 deletions __tests__/dart-getter-reads.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Dart getter reads and extension members on enums (#2338).
*
* Reading a getter runs it: `x.area` calls `Box`'s `int get area`. A read
* linked to nothing, so `callers area` was empty while `x.grow()` worked. And
* a member an `extension … on` an ENUM adds (`s.shout()`) resolved nowhere,
* while the same extension on a class did.
*
* A read links only a getter the receiver's type reaches — never a field (they
* mint no nodes), a getter of a type outside the project, or a same-named
* getter guessed by name.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-getters-'));
const files: Record<string, string> = {
'pubspec.yaml': 'name: repro\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
// The issue's two files, verbatim.
'lib/shape.dart': `enum Shape { circle, square }
extension ShapeInfo on Shape {
String get label => name.toUpperCase();
String shout() => name.toUpperCase();
}
class Box {
const Box(this.size);
final int size;
int get area => size * size;
int grow() => size + 1;
}
extension BoxActions on Box {
int twice() => size * 2;
}
`,
'lib/use.dart': `import 'shape.dart';
String a(Shape s) => s.label; // getter, extension on enum
String b(Shape s) => s.shout(); // method, extension on enum
int c(Box x) => x.area; // getter, plain class
int d(Box x) => x.grow(); // method, plain class
int e(Box x) => x.twice(); // method, extension on class
`,
// Block bodies and class methods, through a parameter, a local and a field.
'lib/render.dart': `import 'shape.dart';
class Square extends Box {
const Square(int size) : super(size);
static Square get unit => const Square(1);
}
class Renderer {
final Box box;
final Shape shape;
Renderer(this.box, this.shape);
int paint(Box other) {
final local = Box(2);
return other.area + local.area;
}
int viaField() {
return box.area;
}
String tag() {
final text = shape.label;
return text + shape.shout();
}
}
int inherited(Square s) {
return s.area;
}
Square staticGetter() => Square.unit;
`,
// Reads that must link nothing.
'lib/other.dart': `class Panel {
int get size => 0;
int get length => 0;
}
extension on Box {
int get hidden => 0;
}
`,
'lib/negatives.dart': `import 'shape.dart';
int fieldRead(Box x) => x.size; // a field, not Panel's getter
int outside(List<int> xs) => xs.length; // a getter of a type outside the project
int untyped(dynamic d) => d.area; // nothing says what d is
int unnamed(Box x) => x.hidden; // an unnamed extension in another library
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `kind name` of each caller of the node with qualified name `qn`, sorted. */
function callersOf(qn: string): string[] {
const [target] = cg.getNodesByQualifiedName(qn);
expect(target, `no node ${qn}`).toBeDefined();
return cg.getCallers(target!.id).map(({ node, edge }) => `${edge.kind} ${node.qualifiedName}`).sort();
}

describe('Dart getter reads (#2338)', () => {
it('link a getter read on a plain class, as a call', () => {
expect(callersOf('Box::area')).toContain('calls c');
});

it('link a getter an extension on an enum adds', () => {
expect(callersOf('ShapeInfo::label')).toEqual(['calls Renderer::tag', 'calls a']);
});

it('link reads in block bodies and class methods, through parameters, locals and fields', () => {
expect(callersOf('Box::area')).toEqual([
'calls Renderer::paint',
'calls Renderer::viaField',
'calls c',
'calls inherited',
]);
});

it('link a static getter read through its class', () => {
expect(callersOf('Square::unit')).toEqual(['calls staticGetter']);
});

it('link nothing for a field, an outside type, an untyped receiver or an out-of-library unnamed extension', () => {
const ids = cg.getNodesInFile('lib/negatives.dart').map((n) => n.id);
const targets = cg.getOutgoingEdgesFrom(ids, ['calls'])
.map((edge) => cg.getNode(edge.target)!.qualifiedName);
expect(targets).toEqual([]);
expect(callersOf('Panel::size')).toEqual([]);
expect(callersOf('Panel::length')).toEqual([]);
});
});

describe('Dart extension members on an enum (#2338)', () => {
it('resolve a method an extension on an enum adds', () => {
expect(callersOf('ShapeInfo::shout')).toEqual(['calls Renderer::tag', 'calls b']);
});

it('still resolve methods on a class and its extensions', () => {
expect(callersOf('Box::grow')).toEqual(['calls d']);
expect(callersOf('BoxActions::twice')).toEqual(['calls e']);
});
});
126 changes: 126 additions & 0 deletions __tests__/dart-type-positions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Dart type positions that recorded no reference (#2327).
*
* `callers` / impact for a Dart type only saw parameter and return types: the
* type an extension is `on`, a field's type, and a type named in a body or an
* initializer (`Future<Report?>.value(null)`, riverpod's `final p =
* Family<Report?, String>()`) linked nothing, so model-to-UI impact missed
* most uses of shared API types.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-types-'));
const files: Record<string, string> = {
'pubspec.yaml': 'name: repro\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
// The issue's six files, verbatim.
'lib/report.dart': 'class Report {\n final int score;\n const Report(this.score);\n}\n',
'lib/a_parameter.dart': "import 'report.dart';\n\nint score(Report r) => r.score;\n",
'lib/b_return_generic.dart': "import 'report.dart';\n\nFuture<Report?> load() async => null;\n",
'lib/c_extension_on.dart': "import 'report.dart';\n\nextension ReportX on Report {\n bool get high => score > 5;\n}\n",
'lib/d_field_type.dart': "import 'report.dart';\n\nclass Holder {\n final Report report;\n const Holder(this.report);\n}\n",
'lib/e_toplevel_initializer_generic.dart': "import 'report.dart';\n\nclass Family<T, A> {\n const Family();\n}\n\nfinal reportProvider = Family<Report?, String>();\n",
'lib/f_expression_generic.dart': "import 'report.dart';\n\nvoid run() {\n Future<Report?>.value(null);\n}\n",
// The same gap's other shapes.
'lib/model.dart': `class Score {
const Score(this.value);
final int value;
}

class ScoreError implements Exception {}
`,
'lib/more.dart': `import 'model.dart' as m;
import 'model.dart';

extension ScoresX on List<Score> {
int get total => length;
}

enum Grade {
a(null);
const Grade(this.best);
final Score? best;
}

class Board {
Score? top;
final List<Score> all = [];
static const Score zero = Score(0);
var cache = <String, Score>{};
late final m.Score prefixed;
}

Score? current;
final scores = <Score>[];

int body(Object o) {
final Score s = o as Score;
if (o is Score) {}
final made = Score(1);
final built = const Score(2);
try {} on ScoreError catch (_) {}
return s.value + made.value + built.value;
}
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `kind qualifiedName` of each node with an incoming `kind` edge to `qn`, sorted and de-duplicated. */
function referrers(qn: string, kind: 'references' | 'instantiates' = 'references'): string[] {
const [target] = cg.getNodesByQualifiedName(qn);
expect(target, `no node ${qn}`).toBeDefined();
const edges = cg.getIncomingEdgesTo([target!.id], [kind]);
return [...new Set(edges.map((e) => cg.getNode(e.source)!).map((n) => `${n.kind} ${n.qualifiedName}`))].sort();
}

describe('Dart type positions (#2327)', () => {
it('reference the type from every position in the issue', () => {
expect(referrers('Report')).toEqual([
'class Holder', // field type
'class ReportX', // extension target
'constant reportProvider', // generic argument in a top-level initializer
'function load', // return type's generic argument (already linked)
'function run', // generic argument in an expression
'function score', // parameter type (already linked)
]);
});

it('reference the type from fields, enum fields, initializers, locals, casts, type tests and the file', () => {
expect(referrers('Score')).toEqual([
'class Board', // field types, `<String, Score>{}`, the prefixed `m.Score`
'class ScoresX', // `on List<Score>`
'enum Grade', // an enhanced enum's field
'file lib/more.dart', // a top-level variable's type
'function body', // a local's type, a cast, a type test
'constant scores', // `<Score>[]`
].sort());
expect(referrers('ScoreError')).toEqual(['function body']);
});

it('leave a constructor call to its instantiation, and an import prefix out', () => {
// `Score(1)` / `const Score(2)` instantiate; they add no reference of their own.
const body = cg.getNodesByQualifiedName('body')[0]!;
const refs = cg.getOutgoingEdgesFrom([body.id], ['references']).filter((e) => cg.getNode(e.target)!.name === 'Score');
expect([...new Set(refs.map((e) => e.line))].sort()).toEqual([26, 27]);
expect(referrers('Score', 'instantiates')).toEqual(['function body']);
const prefixRefs = cg.getUnresolvedReferencesInFile('lib/more.dart').filter((r) => r.referenceName === 'm');
expect(prefixRefs).toEqual([]);
});
});
Loading