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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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)
- In Dart, a `typedef` now links to the types it is built from, like `Report` in `typedef ReportCallback = void Function(Report r);` or `typedef Reports = List<Report>;`, so `callers` and impact for a model class also reach the callback and collection types defined on it. An old-style typedef with a return type, like `typedef Widget ItemBuilder(BuildContext context);`, is now indexed under its own name instead of its return type's, so code that only uses `Widget` no longer links to it. Thanks @mg-mg-mg for the report. (#2327)
- In Go, calls through a method receiver or parameter of an unexported type, the usual shape of gRPC and HTTP handlers (`s.service.AddItem()` inside `func (s *server) Create()`), now resolve, and always within that type's own package: another package's `server` with a same-named method no longer takes the call, and a method the type gets from a struct or interface it embeds is found too. Re-index Go projects after upgrading. Thanks @GoDiao for the report and the fix. (#2323)
- In Go, a call made through a type assertion, like `srv.(KVServer).Range(ctx, in)` in generated gRPC code, `c.Reader.(*pipe).Close()` or `v.(storage.Store).Fetch(key)`, now links to that method of the asserted type, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, it linked to whichever method shared the name, so every gRPC handler pointed at the `Unimplemented…Server` stub instead of the server interface. Re-index Go projects after upgrading.
- In Go, a call through a variable that holds the result of a type assertion, like `st.Fetch(key)` after `st := v.(storage.Store)` or `rc.Read(p)` after `rc, ok := v.(ReadCloser)`, now links to that method of the asserted type, as a call made through the assertion itself does, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, such a call linked to whichever method shared the name or to nothing, and a variable named like a standard-library package, like grpc-go's `parser, ok := builder.(balancer.ConfigParser)`, was taken for that package. Re-index Go projects after upgrading.
Expand Down
312 changes: 312 additions & 0 deletions __tests__/dart-typedef-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
/**
* A Dart typedef references the types it aliases (#2327).
*
* In
*
* class Report { final int score; const Report(this.score); }
* typedef ReportCallback = void Function(Report r);
*
* `callers Report` did not list `ReportCallback`. Both extractors minted the
* typedef's node and nothing else: the grammar gives `type_alias` no fields,
* so the aliased type was never read, and impact on a model type missed every
* callback type built on it. The legacy form with a return type, `typedef
* Report LegacyMaker(int n);`, was named after that return type, the first
* type name in it, so a `type_alias Report` stood beside the class.
*
* A typedef now references every type its aliased type names, from the
* typedef, on each type's name: a function type's return and parameter types,
* generic arguments, record fields, a type parameter's bound, and the legacy
* form's return and parameter types. A type parameter, the typedef's own or a
* generic function type's, means that parameter and never a project type of
* its name; an import prefix names no type; and the legacy form is named by
* its own name.
*
* Runs against the native kernel (when built) and the wasm extractor, which
* must agree.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult, Node } 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';

/** Every typedef shape: new style and legacy, generic, prefixed, annotated, wrapped. */
const SOURCE = `import 'model.dart' as m;

class Report {
final int score;
const Report(this.score);
}

typedef ReportCallback = void Function(Report r);
typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef ReportsByName = Map<String, Report>;
typedef Results = Future<List<Report>?> Function(int page);
typedef void LegacyCallback(Report r);
typedef Report LegacyMaker(int n);
typedef List<Report> LegacyLoader([Report? seed]);
typedef m.Report PrefixedLegacy(m.Report r);
typedef Mapper<T> = T Function(Report r);
typedef Picker = T Function<T>(T a, Report r);
typedef T LegacyPicker<T>(T a, Report r);
typedef Bounded<X extends Report> = List<X>;
typedef Sdk = Future<String>;
typedef Dynamic = dynamic Function(int n);
typedef Prefixed = m.Report;
typedef Named = void Function({required Report report, int? n});
typedef Pair = (Report, {m.Report other});
typedef Nested = void Function(Report Function(int) make);
/// Documented.
@Deprecated('Use ReportCallback')
typedef OldCallback = void Function(Report r);
typedef Formatted =
Map<String, Report> Function(
Report first,
Report second,
);
`;

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

describe('a Dart typedef references the types it aliases (#2327)', () => {
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' | 'wasm', source: string): ExtractionResult {
if (backend === 'wasm') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource('lib/typedefs.dart', source, 'dart');
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract('lib/typedefs.dart', source, 'dart');
expect(result, 'kernel extraction').not.toBeNull();
return result!;
}

/** `<typedef> <type>` for every `references` ref out of a typedef, sorted. */
function typedefRefs(result: ExtractionResult): string[] {
const byId = new Map(result.nodes.map((n) => [n.id, n]));
return result.unresolvedReferences
.filter((r) => r.referenceKind === 'references' && byId.get(r.fromNodeId)?.kind === 'type_alias')
.map((r) => `${byId.get(r.fromNodeId)!.name} ${r.referenceName}`)
.sort();
}

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

for (const crlf of [false, true]) {
const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s);
const label = crlf ? ' (CRLF)' : '';

it.each(backends)(`a legacy typedef is named by its own name, not its return type: %s${label}`, (backend) => {
const result = extract(backend, eol(SOURCE));
expect(result.nodes.filter((n) => n.kind === 'type_alias').map((n) => n.name)).toEqual([
'ReportCallback',
'ReportMaker',
'Reports',
'ReportsByName',
'Results',
'LegacyCallback',
'LegacyMaker',
'LegacyLoader',
'PrefixedLegacy',
'Mapper',
'Picker',
'LegacyPicker',
'Bounded',
'Sdk',
'Dynamic',
'Prefixed',
'Named',
'Pair',
'Nested',
'OldCallback',
'Formatted',
]);
// An annotated typedef keeps the dartdoc above its annotation.
expect(result.nodes.find((n) => n.name === 'OldCallback')!.docstring).toBe('Documented.');
});

it.each(backends)(`every type the alias names is referenced from the typedef: %s${label}`, (backend) => {
const source = eol(SOURCE);
const result = extract(backend, source);
// Built-ins (`void`, `int`, `String`, `dynamic`) are no references, an
// import prefix (`m`) is none, and neither is a type parameter (`T`,
// `X`), declared by the typedef or by a generic function type.
expect(typedefRefs(result)).toEqual([
'Bounded List',
'Bounded Report',
'Formatted Map',
'Formatted Report',
'Formatted Report',
'Formatted Report',
'LegacyCallback Report',
'LegacyLoader List',
'LegacyLoader Report',
'LegacyLoader Report',
'LegacyMaker Report',
'LegacyPicker Report',
'Mapper Report',
'Named Report',
'Nested Report',
'OldCallback Report',
'Pair Report',
'Pair Report',
'Picker Report',
'Prefixed Report',
'PrefixedLegacy Report',
'PrefixedLegacy Report',
'ReportCallback Report',
'ReportMaker Report',
'Reports List',
'Reports Report',
'ReportsByName Map',
'ReportsByName Report',
'Results Future',
'Results List',
'Results Report',
'Sdk Future',
]);
// Each sits on the type's name, where resolution reads an import prefix back.
const lines = source.split(/\r?\n/);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
for (const r of result.unresolvedReferences.filter((x) => byId.get(x.fromNodeId)?.kind === 'type_alias')) {
expect(lines[r.line - 1]!.startsWith(r.referenceName, r.column), `${r.referenceName} at ${r.line}:${r.column}`).toBe(true);
}
// The file takes none of them: no type is credited twice.
const file = result.nodes.find((n) => n.kind === 'file')!;
expect(result.unresolvedReferences.filter((r) => r.fromNodeId === file.id && r.referenceKind === 'references')).toEqual([]);
});
}
});

describe('an indexed Dart library links a typedef to the types it aliases (#2327)', () => {
const FILES: Record<string, string> = {
'pubspec.yaml': 'name: repro\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
// The issue's repro.
'lib/report.dart': 'class Report {\n final int score;\n const Report(this.score);\n}\n',
'lib/callback.dart': "import 'report.dart';\n\ntypedef ReportCallback = void Function(Report r);\n",
// A namesake reached only through the `m.` prefix.
'lib/model.dart': 'class Report {}\n',
// Project types named like the type parameters, imported where those are declared.
'lib/params.dart': 'class T {}\n\nclass X {}\n',
// A project `Future` the typedefs' library does not import.
'lib/async.dart': 'class Future {}\n',
'lib/typedefs.dart': `import 'model.dart' as m;
import 'params.dart';
import 'report.dart';

typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef Report LegacyMaker(int n);
typedef Mapper<T> = T Function(Report r);
typedef Picker = T Function<T>(T a, Report r);
typedef Bounded<X extends Report> = List<X>;
typedef Sdk = Future<String>;
typedef Prefixed = m.Report;
typedef Pair = (Report, {m.Report other});
`,
};

let root = '';
let cg: CodeGraph | undefined;
let kernel: string | undefined;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-typedef-refs-'));
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);
}
kernel = process.env.CODEGRAPH_KERNEL;
});

afterEach(() => {
cg?.close();
cg = undefined;
fs.rmSync(root, { recursive: true, force: true });
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
else process.env.CODEGRAPH_KERNEL = kernel;
});

it.each(['default', 'wasm'])('callers of a type list the typedefs built on it (%s)', async (backend) => {
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
else delete process.env.CODEGRAPH_KERNEL;
cg = await CodeGraph.init(root, { index: true });
const graph = cg;
const node = (file: string, name: string): Node => {
const found = graph.getNodesInFile(file).filter((n) => n.name === name && n.kind !== 'import');
expect(found, `${name} in ${file}`).toHaveLength(1);
return found[0]!;
};
/** `kind file:qualifiedName` of each `references` target of the typedef `name`, sorted. */
const referenced = (name: string): string[] =>
[...new Set(graph
.getOutgoingEdgesFrom([node('lib/typedefs.dart', name).id], ['references'])
.map((e) => graph.getNode(e.target)!)
.map((n) => `${n.kind} ${n.filePath}:${n.qualifiedName}`))]
.sort();

// `callers Report` lists every typedef that names it, the issue's first.
const callers = graph.getCallers(node('lib/report.dart', 'Report').id).map(({ node: n }) => `${n.kind} ${n.filePath}:${n.name}`);
expect(callers.filter((c) => c.startsWith('type_alias')).sort()).toEqual([
'type_alias lib/callback.dart:ReportCallback',
'type_alias lib/typedefs.dart:Bounded',
'type_alias lib/typedefs.dart:LegacyMaker',
'type_alias lib/typedefs.dart:Mapper',
'type_alias lib/typedefs.dart:Pair',
'type_alias lib/typedefs.dart:Picker',
'type_alias lib/typedefs.dart:ReportMaker',
'type_alias lib/typedefs.dart:Reports',
]);

// A prefixed type is the one its import names.
expect(referenced('Prefixed')).toEqual(['class lib/model.dart:Report']);
expect(referenced('Pair')).toEqual(['class lib/model.dart:Report', 'class lib/report.dart:Report']);
// A type parameter is not the project's `T` or `X`, though both are imported.
expect(referenced('Mapper')).toEqual(['class lib/report.dart:Report']);
expect(referenced('Picker')).toEqual(['class lib/report.dart:Report']);
expect(referenced('Bounded')).toEqual(['class lib/report.dart:Report']);
expect(graph.getCallers(node('lib/params.dart', 'T').id)).toEqual([]);
expect(graph.getCallers(node('lib/params.dart', 'X').id)).toEqual([]);
// The SDK's Future is not a project class the library does not import.
expect(referenced('Sdk')).toEqual([]);
expect(graph.getCallers(node('lib/async.dart', 'Future').id)).toEqual([]);
// The legacy typedef is named LegacyMaker; nothing else is named Report there.
expect(graph.getNodesInFile('lib/typedefs.dart').filter((n) => n.name === 'Report')).toEqual([]);
expect(referenced('LegacyMaker')).toEqual(['class lib/report.dart:Report']);
}, 60_000);
});
78 changes: 78 additions & 0 deletions __tests__/fixtures/kernel-parity/TortureTypedefs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Typedefs reference the types they alias (#2327): every new-style and legacy
// shape, type parameters (the typedef's own and a generic function type's),
// import prefixes, annotations, comments and wrapped declarations.
import 'package:meta/meta.dart';
import 'model.dart' as m;
import 'model.dart' as M;

class Report {
final int score;
const Report(this.score);
}

class T {}

void tag(Object o) {}

// New style.
typedef ReportCallback = void Function(Report r);
typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef ReportsByName = Map<String, Report>;
typedef Results = Future<List<Report>?> Function(int page);
typedef Opt = Report?;
typedef MaybeMaker = Report Function()?;
typedef Pair = (Report, int);
typedef NamedPair = ({Report first, m.Report second});
typedef Named = void Function({required Report report, int? n});
typedef Positional = void Function([Report? report]);
typedef Unnamed = void Function(Report, m.Report);
typedef Nested = void Function(void Function(Report) inner);
typedef Maker = void Function(Report Function() make);
typedef Dynamic = dynamic Function(Object? o);
typedef Void = void;
typedef Fn = Function;
typedef Prefixed = m.Report;
typedef UpperPrefixed = M.Report;
typedef PrefixedGeneric = m.Box<m.Report>;

// Type parameters: the typedef's own, a bound, a generic function type's.
typedef Mapper<T> = T Function(Report r);
typedef Bounded<X extends Report> = List<X>;
typedef SelfBounded<C extends Comparable<C>> = int Function(C a, C b);
typedef Picker = T Function<T>(T a, Report r);
typedef Outer<S> = S Function<U>(U u, S s, T t);
typedef Shadow<T> = m.T Function(T t);
typedef TwoParams<K, V extends Report> = Map<K, List<V>>;

// Legacy.
typedef void LegacyCallback(Report r);
typedef Report LegacyMaker(int n);
typedef List<Report> LegacyLoader([Report? seed]);
typedef m.Report PrefixedLegacy(m.Report r);
typedef Report? LegacyOptional({Report? fallback});
typedef void Function(Report) LegacyFnReturn(int n);
typedef T LegacyPicker<T>(T a, Report r);
typedef void LegacyFnParam(Report make(int n));
typedef void LegacyGenericFnParam(S make<S>(S s, Report r));
typedef void LegacyCovariant(covariant Report r, final Report s);

// Annotated, documented, commented, wrapped.
/// Documented.
@Deprecated('Use ReportCallback')
typedef OldCallback = void Function(Report r);
@visibleForTesting
@Tag(tag)
typedef Tagged = Report Function(/* the report */ Report r);
typedef Commented = // why
Report Function(Report r);
typedef Wrapped =
Map<String, Report> Function(
Report first,
Report second,
);

class Tag {
const Tag(this.f);
final Object f;
}
Loading