mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
9f69e47836
Issue #2338: a Dart getter read (`x.area`, `s.label`) linked nothing, so `codegraph callers area` was empty while `x.grow()` worked, and a member an extension adds to an ENUM (`s.shout()` through `extension ShapeInfo on Shape`) never resolved. Issue #2327: `callers Report` saw only parameter and return types; the type an extension is `on`, field types, and types named in bodies and initializers (`Future<Report?>.value(null)`, riverpod's `final reportProvider = Family<Report?, String>()`) recorded nothing. Causes: - The Dart body walker only emitted calls (a selector with an argument list); a member read is a selector without one, so it produced no ref. - For a typed receiver, resolveMethodOnType looks for `Type::member` and the type's supertype edges; an extension's member is `ShapeInfo::shout`. On a class receiver the call fell through to name matching, which found a unique `twice` by luck; on an enum the "type the project doesn't declare" guard (it only counts classes) returned null. - Type references were taken from function and method signatures only. Fix (TS extractor and the Rust kernel, mirrored): - A member read `x.area` / `x?.area` / `Type.getter` whose receiver is a plain name, and that is not a call, is sent as a `references` ref named `x.area` on the member. The resolver (matchDartMemberRead, ahead of every other strategy) types the receiver — a static read through a project type, a parameter or local in scope, else a field of the enclosing class — and links the getter that type reaches: declared on it or a type it extends, mixes in or implements, else added by an extension on one of those (an unnamed extension only in its own library). The edge is `calls`: a Dart getter is a method node, reading it runs it, and `calls` is what callers, impact and explore's flow follow (as Ruby's paren-less `x.area` and VB.NET's paren-less calls through #2355 already are). A field read, a getter of a type outside the project and an untyped receiver link nothing; there is no name-only fallback. Kotlin/Swift/C#/TS link no instance property reads today. - resolveMethodOnType, for Dart, falls back to a member an extension on the type (or a supertype) adds when no declaration of the type has it. - `references` refs for the type an extension is `on`, a field's declared type and initializer (from its class — Dart fields mint no nodes), an enum field, a top-level variable's type (from the file), a constant's initializer (from the constant), and every type a function body names: locals, generic arguments, casts, type tests, catch clauses. Skipped: a `new`/`const` constructor's class (already instantiates/calls), an import prefix (`p` in `p.Foo`), built-ins, and lowercase names, which in Dart are built-ins (`num`, `dynamic`) or error-recovery artifacts around syntax the grammar predates (dot shorthands). A class whose field holds its own type does not get a self-edge. Verification: __tests__/dart-getter-reads.test.ts (7 tests) and __tests__/dart-type-positions.test.ts (3 tests) fail 8/10 on main (both the TS path and main's kernel) and pass on both arms here; the 2 that pass on main are the controls. New kernel parity fixture TortureReadsTypes.dart (+CRLF); kernel-dart-parity passes with CODEGRAPH_KERNEL_EXPECT=1, and scripts/kernel-parity.mjs over bloc/riverpod/flutter-samples is 0 diffs (2,103 files byte-parity, 300 parse-error files defer as before). tsc and build clean; the issues' repros now list c/a/b for area/label/shout and Holder, ReportX, reportProvider and run for Report. Validation (kernel loaded, main vs this branch): nodes unchanged, 0 edges removed, no new self-loops. - felangel/bloc: 16,372 -> 18,276 edges (+550 getter-read calls, +1,354 type references) - rrousselGit/riverpod: 76,432 -> 81,909 (+598 getter reads, +13 extension-method calls, +4,866 type references) - flutter/samples: 26,133 -> 26,943 (+142 getter reads, +668 type refs) Spot-checked 16 getter reads and 16 type references against source. Index time within noise on riverpod (median of 5 interleaved runs); reads that link nothing are kept for re-resolution like failed calls (riverpod db 92.9 -> 103.2 MB). No EXTRACTION_VERSION bump: it already moved to 28 this release. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
5.0 KiB
TypeScript
166 lines
5.0 KiB
TypeScript
/**
|
|
* 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']);
|
|
});
|
|
});
|