Files
codegraph/src/resolution/dart-libraries.ts
T
Colby MchenryandClaude Opus 5.5 83f54a1853 fix(dart): a library links the files its part directives name (#2391)
A Dart `part 'x.g.dart';` now mints an `import` node (name = the URI,
signature = the directive) and an `imports` ref from the library's file
node, in both extractors. The ref resolves through `dartDirectiveFile` by
the rules an import or export follows, so the library's file links the
part's file. A library and its parts are one library, so a change to a
part, generated or hand-written, now reaches the library and what imports
it in `codegraph affected` and the files listed as using the part. Before,
neither `part` nor `part of` left a trace, and the two files were joined
only by whatever symbol edges happened to cross between them: riverpod's
framework.dart, which holds nothing but `part` lines, by none.

`part of` still records nothing: the library's edge already joins the two
files, and a part -> library edge would list every pair on both import
rails of both files and make each a file 2-cycle.

The kernel's directive arm now goes on into the directive's children, as
the TS ladder does. Its `import_or_export` arm used to return first, so an
annotated directive's arguments (`@Tag(f) import 'a.dart';`) were fn-ref
candidates on the wasm path only.

Validation on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2
with the kernel loaded, base 837a1866:
- bloc: +53 import nodes, +53 contains, +53 imports (library -> part).
- riverpod: +294 import nodes, +294 contains, +258 imports, +36 refs left
  unresolved (generated parts that are not committed).
- Nothing removed; kernel and wasm dumps byte-identical; parity sweeps
  0 diffs. Every added edge sits on a `part` line whose URI names the
  target, and the target's `part of` names the library back (311/311).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:16:55 +00:00

797 lines
31 KiB
TypeScript

/**
* Which files' top-level declarations a Dart file can name without a prefix.
*
* A Dart library is a file and its `part`s. Its top-level scope holds its own
* declarations, private `_` names included, and every name exported by a
* library it imports without a prefix: that library's own public declarations
* and, through its `export`s, other libraries'. Every import and export is
* narrowed by its `show` / `hide` combinators. A conditional import (`import
* 'stub.dart' if (dart.library.io) 'io.dart'`) brings in every alternative.
* `dart:` libraries and packages outside the repository (flutter, test,
* mockito, …) export nothing of the project.
*
* `package:<name>/<path>` is `<root>/lib/<path>` of the package whose
* pubspec.yaml says `name: <name>`: the nearest pubspec.yaml above an indexed
* .dart file, never above the project root. When packages share a name
* (example apps), the importing file's own package wins, then the one
* enclosing it, then the one its package depends on by `path:`, else all of
* them count. An `import`, `export` or `part` edge links the one file its URI
* names (`dartDirectiveFile`).
*
* Directives are read from the head of each file at resolution time, not from
* what extraction recorded of them. Where the library cannot be
* established — a `part of` naming no library this can find, a file it cannot
* read, a file in no package — every file counts as visible, so the rule only
* narrows where the library is known.
*/
import * as path from 'path';
import type { ResolutionContext } from './types';
/** The names an import's or export's `show` / `hide` combinators let through; `show` null lets every name through. */
export interface NameFilter {
readonly show: ReadonlySet<string> | null;
readonly hide: ReadonlySet<string> | null;
/** Identifies equal filters, so a namespace keeps each once. */
readonly key: string;
}
const EVERY_NAME: NameFilter = { show: null, hide: null, key: '*|' };
function makeFilter(show: ReadonlySet<string> | null, hide: ReadonlySet<string> | null): NameFilter {
const hidden = hide && hide.size > 0 ? hide : null;
if (show === null && hidden === null) return EVERY_NAME;
return { show, hide: hidden, key: `${show ? [...show].sort().join(',') : '*'}|${hidden ? [...hidden].sort().join(',') : ''}` };
}
/** `a`, then `b`: a name gets through when both let it. */
function composeFilters(a: NameFilter, b: NameFilter): NameFilter {
if (a === EVERY_NAME) return b;
if (b === EVERY_NAME) return a;
const show = a.show && b.show ? new Set([...a.show].filter((n) => b.show!.has(n))) : (a.show ?? b.show);
const hide = a.hide && b.hide ? new Set([...a.hide, ...b.hide]) : (a.hide ?? b.hide);
return makeFilter(show, hide);
}
function lets(filter: NameFilter, name: string): boolean {
return (filter.show === null || filter.show.has(name)) && (filter.hide === null || !filter.hide.has(name));
}
/** An `import` or `export` directive. */
export interface DartNamespaceDirective {
/** The URI, then each conditional alternative (`if (dart.library.io) 'io.dart'`). */
uris: string[];
/** The prefix of `import '…' as p`, or null. */
prefix: string | null;
filter: NameFilter;
}
/** The directives at the head of a Dart file. */
export interface DartDirectives {
/** `library a.b;` gives `a.b`, `library;` gives '', no directive null. */
libraryName: string | null;
partOf: { uri: string } | { name: string } | null;
parts: string[];
imports: DartNamespaceDirective[];
exports: DartNamespaceDirective[];
}
const DIRECTIVE_KEYWORDS: ReadonlySet<string> = new Set(['library', 'import', 'export', 'part']);
const IDENTIFIER = /[A-Za-z_$][\w$]*/y;
/** The index past the whitespace and comments at `i` (Dart block comments nest). */
function skipTrivia(src: string, i: number): number {
const n = src.length;
while (i < n) {
const c = src.charCodeAt(i);
if (c === 32 || (c >= 9 && c <= 13) || c === 0xfeff) {
i++;
} else if (c === 47 && src.charCodeAt(i + 1) === 47) {
const newline = src.indexOf('\n', i + 2);
i = newline < 0 ? n : newline + 1;
} else if (c === 47 && src.charCodeAt(i + 1) === 42) {
let depth = 1;
i += 2;
while (i < n && depth > 0) {
if (src.charCodeAt(i) === 47 && src.charCodeAt(i + 1) === 42) {
depth++;
i += 2;
} else if (src.charCodeAt(i) === 42 && src.charCodeAt(i + 1) === 47) {
depth--;
i += 2;
} else {
i++;
}
}
} else {
break;
}
}
return i;
}
/** The string literal at `i` — `'…'`, `"…"`, triple-quoted or raw — as its value and the index past it, or null. */
function readString(src: string, i: number): { value: string; end: number } | null {
let raw = false;
if (src[i] === 'r' && (src[i + 1] === "'" || src[i + 1] === '"')) {
raw = true;
i++;
}
const quote = src[i];
if (quote !== "'" && quote !== '"') return null;
const close = src.startsWith(quote.repeat(3), i) ? quote.repeat(3) : quote;
let j = i + close.length;
let value = '';
while (j < src.length) {
if (src.startsWith(close, j)) return { value, end: j + close.length };
const c = src[j]!;
if (c === '\n' && close.length === 1) return null;
if (c === '\\' && !raw) {
value += src[j + 1] ?? '';
j += 2;
} else if (c === '$' && !raw && src[j + 1] === '{') {
let depth = 1;
j += 2;
while (j < src.length && depth > 0) {
const inner = readString(src, j);
if (inner) {
j = inner.end;
continue;
}
if (src[j] === '{') depth++;
else if (src[j] === '}') depth--;
j++;
}
} else {
value += c;
j++;
}
}
return null;
}
/** The index past the balanced `open` … `close` group at `i`, strings and comments skipped; -1 if it never closes. */
function skipGroup(src: string, i: number, open: string, close: string): number {
let depth = 0;
while (i < src.length) {
i = skipTrivia(src, i);
const literal = readString(src, i);
if (literal) {
i = literal.end;
continue;
}
const c = src[i];
if (c === open) depth++;
else if (c === close && --depth === 0) return i + 1;
i++;
}
return -1;
}
/** The index past the metadata annotation at `i` (`@TestOn('vm')`, `@Tags(['slow'])`, `@JS()`). */
function skipAnnotation(src: string, i: number): number {
i++;
for (;;) {
i = skipTrivia(src, i);
IDENTIFIER.lastIndex = i;
const name = IDENTIFIER.exec(src);
if (!name) return i;
i += name[0].length;
const next = skipTrivia(src, i);
if (src[next] !== '.') break;
i = next + 1;
}
i = skipTrivia(src, i);
if (src[i] === '<') {
const end = skipGroup(src, i, '<', '>');
if (end < 0) return src.length;
i = skipTrivia(src, end);
}
if (src[i] === '(') {
const end = skipGroup(src, i, '(', ')');
return end < 0 ? src.length : end;
}
return i;
}
type DirectiveToken =
| { kind: 'string'; value: string }
| { kind: 'word'; value: string }
| { kind: 'condition' }
| { kind: 'punctuation'; value: string };
/**
* The tokens of a directive, from just past its keyword to its `;`, and the
* index past that `;` — or null when a body (`{`, `=>`) comes first, as in a
* function named `part`, or the file ends.
*/
function readDirectiveTokens(src: string, i: number): { tokens: DirectiveToken[]; end: number } | null {
const tokens: DirectiveToken[] = [];
for (;;) {
i = skipTrivia(src, i);
if (i >= src.length || tokens.length > 2000) return null;
const c = src[i]!;
if (c === ';') return { tokens, end: i + 1 };
if (c === '{' || c === '}' || (c === '=' && src[i + 1] === '>')) return null;
const literal = readString(src, i);
if (literal) {
tokens.push({ kind: 'string', value: literal.value });
i = literal.end;
continue;
}
if (c === '(') {
// A configuration condition: `(dart.library.io)`, `(dart.library.js_interop == 'true')`.
i = skipGroup(src, i, '(', ')');
if (i < 0) return null;
tokens.push({ kind: 'condition' });
continue;
}
IDENTIFIER.lastIndex = i;
const word = IDENTIFIER.exec(src);
if (word) {
tokens.push({ kind: 'word', value: word[0] });
i += word[0].length;
continue;
}
tokens.push({ kind: 'punctuation', value: c });
i++;
}
}
/** A dotted library name (`app.named`), as the words and dots it is written with. */
function dottedName(tokens: readonly DirectiveToken[]): string | null {
let name = '';
for (const t of tokens) {
if (t.kind === 'word' || (t.kind === 'punctuation' && t.value === '.')) name += t.value;
else return null;
}
return name;
}
/** Record the directive `keyword` with `tokens` in `out`; false when the tokens are no such directive. */
function addDirective(out: DartDirectives, keyword: string, tokens: DirectiveToken[]): boolean {
const first = tokens[0];
if (keyword === 'library') {
const name = dottedName(tokens);
if (name === null) return false;
out.libraryName = name;
return true;
}
if (keyword === 'part') {
if (first?.kind === 'word' && first.value === 'of') {
const target = tokens[1];
const name = target?.kind === 'string' ? null : dottedName(tokens.slice(1));
if (target?.kind === 'string') out.partOf = { uri: target.value };
else if (name) out.partOf = { name };
else return false;
return true;
}
if (first?.kind !== 'string') return false;
out.parts.push(first.value);
return true;
}
if (first?.kind !== 'string') return false;
const uris = [first.value];
let prefix: string | null = null;
let filter = EVERY_NAME;
let k = 1;
// Adjacent literals are one string: `'package:a/' 'b.dart'`.
for (let t = tokens[k]; t?.kind === 'string'; t = tokens[++k]) uris[0] += t.value;
while (k < tokens.length) {
const t = tokens[k]!;
const next = tokens[k + 1];
const uri = tokens[k + 2];
if (t.kind === 'word' && t.value === 'if' && next?.kind === 'condition' && uri?.kind === 'string') {
uris.push(uri.value);
k += 3;
} else if (t.kind === 'word' && t.value === 'as' && next?.kind === 'word') {
prefix = next.value;
k += 2;
} else if (t.kind === 'word' && (t.value === 'show' || t.value === 'hide')) {
const names = new Set<string>();
k++;
while (tokens[k]?.kind === 'word') {
names.add((tokens[k] as { value: string }).value);
const comma = tokens[k + 1];
if (comma?.kind !== 'punctuation' || comma.value !== ',') {
k++;
break;
}
k += 2;
}
filter = composeFilters(filter, t.value === 'show' ? makeFilter(names, null) : makeFilter(null, names));
} else {
// `deferred`, and anything this does not model.
k++;
}
}
(keyword === 'import' ? out.imports : out.exports).push({ uris, prefix, filter });
return true;
}
/**
* The `library`, `part`, `part of`, `import` and `export` directives at the
* head of a Dart source file — past its script tag, comments and metadata,
* up to its first declaration.
*/
export function readDartDirectives(source: string): DartDirectives {
const out: DartDirectives = { libraryName: null, partOf: null, parts: [], imports: [], exports: [] };
let i = source.charCodeAt(0) === 0xfeff ? 1 : 0;
if (source.startsWith('#!', i)) {
const newline = source.indexOf('\n', i);
i = newline < 0 ? source.length : newline + 1;
}
for (;;) {
i = skipTrivia(source, i);
if (i >= source.length) break;
if (source[i] === '@') {
i = skipAnnotation(source, i);
continue;
}
IDENTIFIER.lastIndex = i;
const keyword = IDENTIFIER.exec(source)?.[0];
if (keyword === undefined || !DIRECTIVE_KEYWORDS.has(keyword)) break;
const directive = readDirectiveTokens(source, i + keyword.length);
if (!directive || !addDirective(out, keyword, directive.tokens)) break;
i = directive.end;
}
return out;
}
/** A pub package of the project: its pubspec.yaml's `name`, and the directory holding it ('' for the project root). */
interface PubPackage {
name: string;
root: string;
}
/** What a file's imports and its library's own files make visible. */
interface FileScope {
/** The library's files: the library, its parts, and the file itself. */
own: ReadonlySet<string>;
/** The files whose imports apply here: the file, and for a part each file up to its library. */
importers: readonly string[];
/** By import prefix ('' for none), the libraries those imports name, each with the import's filter (read on first use). */
imported: Map<string, Array<{ filter: NameFilter; library: string }>> | null;
/** By prefix and declaring file, the filters through which the imports bring in its names (read on first use). */
visible: Map<string, readonly NameFilter[]>;
}
interface Memo {
directives: Map<string, DartDirectives | null>;
packageOfDir: Map<string, PubPackage | null>;
packages: Map<string, PubPackage[]> | null;
/** By package root and dependency name, the directories its `path:` dependencies of that name point at. */
pathDependencies: Map<string, readonly string[]>;
libraryNames: Map<string, string[]> | null;
libraryOf: Map<string, string | null>;
/** A part's parent: the file its `part of` names. */
parentOf: Map<string, string>;
members: Map<string, ReadonlySet<string>>;
exports: Map<string, ReadonlyMap<string, readonly NameFilter[]>>;
scopes: Map<string, FileScope | null>;
prefixes: Map<string, ReadonlySet<string>>;
}
const memos = new WeakMap<ResolutionContext, Memo>();
function memoFor(context: ResolutionContext): Memo {
let memo = memos.get(context);
if (!memo) {
memo = {
directives: new Map(),
packageOfDir: new Map(),
packages: null,
pathDependencies: new Map(),
libraryNames: null,
libraryOf: new Map(),
parentOf: new Map(),
members: new Map(),
exports: new Map(),
scopes: new Map(),
prefixes: new Map(),
};
memos.set(context, memo);
}
return memo;
}
/** Drop the memos (see ReferenceResolver.clearCaches). */
export function clearDartLibraryMemos(context: ResolutionContext): void {
memos.delete(context);
}
function parentDir(file: string): string {
const cut = file.lastIndexOf('/');
return cut < 0 ? '' : file.slice(0, cut);
}
function directivesOf(file: string, context: ResolutionContext, memo: Memo): DartDirectives | null {
const hit = memo.directives.get(file);
if (hit !== undefined) return hit;
const source = context.readFile(file);
const directives = source === null ? null : readDartDirectives(source);
memo.directives.set(file, directives);
return directives;
}
const PUBSPEC_NAME = /^name[ \t]*:[ \t]*['"]?([A-Za-z_]\w*)/m;
/** The package of the nearest pubspec.yaml at or above project-relative `dir`, never above the project root. */
function packageOfDir(dir: string, context: ResolutionContext, memo: Memo): PubPackage | null {
const walked: string[] = [];
let found: PubPackage | null | undefined;
for (let d = dir; ; d = parentDir(d)) {
found = memo.packageOfDir.get(d);
if (found !== undefined) break;
walked.push(d);
const pubspec = d ? `${d}/pubspec.yaml` : 'pubspec.yaml';
const name = context.fileExists(pubspec) ? PUBSPEC_NAME.exec(context.readFile(pubspec) ?? '')?.[1] : undefined;
if (name) {
found = { name, root: d };
break;
}
if (d === '') {
found = null;
break;
}
}
for (const d of walked) memo.packageOfDir.set(d, found);
return found;
}
/** The project's packages named `name`: the packages that hold an indexed .dart file. */
function packagesNamed(name: string, context: ResolutionContext, memo: Memo): readonly PubPackage[] {
if (!memo.packages) {
const byName = new Map<string, PubPackage[]>();
const seen = new Set<PubPackage>();
for (const file of context.getAllFiles()) {
if (!file.endsWith('.dart')) continue;
const pkg = packageOfDir(parentDir(file), context, memo);
if (!pkg || seen.has(pkg)) continue;
seen.add(pkg);
const list = byName.get(pkg.name);
if (list) list.push(pkg);
else byName.set(pkg.name, [pkg]);
}
memo.packages = byName;
}
return memo.packages.get(name) ?? [];
}
/**
* The `path:` a pubspec gives its dependency `name`, written as a block
* (`name:`, then `path: x` below it) or a flow mapping (`name: {path: x}`).
* A git dependency's `path:` (inside its `git:` mapping) is a path in that
* repository, so only `path:` directly under the name counts.
*/
function yamlPathDependencies(text: string, name: string): string[] {
const out: string[] = [];
const lines = text.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const key = /^([ \t]+)(['"]?)([A-Za-z_]\w*)\2[ \t]*:(?:[ \t]+(.*))?$/.exec(lines[i]!);
if (!key || key[3] !== name) continue;
const value = (key[4] ?? '').replace(/(?:^|[ \t])#.*$/, '').trim();
if (value !== '') {
const flow = /^\{[^{}]*\}$/.test(value)
? /[{,][ \t]*path[ \t]*:[ \t]*(?:"([^"]*)"|'([^']*)'|([^,}\s]+))/.exec(value)
: null;
if (flow) out.push(flow[1] ?? flow[2] ?? flow[3]!);
continue;
}
let childIndent = -1;
for (let j = i + 1; j < lines.length; j++) {
const line = lines[j]!;
if (/^[ \t]*(?:#.*)?$/.test(line)) continue;
const indent = /^[ \t]*/.exec(line)![0].length;
if (indent <= key[1]!.length) break;
if (childIndent < 0) childIndent = indent;
const dep = indent === childIndent ? /^[ \t]*path[ \t]*:[ \t]*(?:"([^"]*)"|'([^']*)'|([^\s#]+))/.exec(line) : null;
if (dep) out.push(dep[1] ?? dep[2] ?? dep[3]!);
}
}
return out;
}
/** The project directories `pkg`'s pubspec_overrides.yaml and pubspec.yaml give as the `path:` of its dependency `name`. */
function pathDependencies(pkg: PubPackage, name: string, context: ResolutionContext, memo: Memo): readonly string[] {
const key = `${pkg.root}\0${name}`;
const hit = memo.pathDependencies.get(key);
if (hit) return hit;
const dirs: string[] = [];
for (const file of ['pubspec_overrides.yaml', 'pubspec.yaml']) {
const pubspec = pkg.root ? `${pkg.root}/${file}` : file;
const text = context.fileExists(pubspec) ? context.readFile(pubspec) : null;
for (const written of text ? yamlPathDependencies(text, name) : []) {
const dep = written.replace(/\\/g, '/');
if (dep.startsWith('/') || /^[A-Za-z]:/.test(dep)) continue;
const dir = path.posix.normalize(path.posix.join(pkg.root || '.', dep)).replace(/\/+$/, '');
if (dir === '..' || dir.startsWith('../')) continue;
const root = dir === '.' ? '' : dir;
if (!dirs.includes(root)) dirs.push(root);
}
}
memo.pathDependencies.set(key, dirs);
return dirs;
}
/** The project files a URI written in `from` names: none for `dart:` and other schemes, or a package outside the project. */
function resolveDartUri(from: string, uri: string, context: ResolutionContext, memo: Memo): string[] {
if (uri.startsWith('package:')) {
const slash = uri.indexOf('/');
if (slash < 0) return [];
const name = uri.slice('package:'.length, slash);
const rest = uri.slice(slash + 1);
let chosen = packagesNamed(name, context, memo);
if (chosen.length > 1) {
const own = packageOfDir(parentDir(from), context, memo);
const enclosing = chosen.filter((p) => p.root === '' || from.startsWith(`${p.root}/`));
if (own && own.name === name) chosen = [own];
else if (enclosing.length > 0) chosen = [enclosing.reduce((a, b) => (b.root.length > a.root.length ? b : a))];
else if (own) {
// Example apps that each keep a same-named package (bloc's two
// authentication_repository) name theirs as a path dependency.
const dirs = pathDependencies(own, name, context, memo);
const declared = chosen.filter((p) => dirs.includes(p.root));
if (declared.length === 1) chosen = declared;
}
}
return chosen.map((p) => path.posix.normalize(p.root ? `${p.root}/lib/${rest}` : `lib/${rest}`));
}
if (/^[A-Za-z][\w+.-]*:/.test(uri)) return [];
const joined = path.posix.normalize(path.posix.join(parentDir(from) || '.', uri));
return joined === '..' || joined.startsWith('../') || joined.startsWith('/') ? [] : [joined];
}
/**
* The project file a Dart `import`, `export` or `part` in `from` names by
* `uri`, or null: a `dart:` library, a package from outside the project, a
* path out of it, or a package name the project repeats with nothing to say
* which.
*/
export function dartDirectiveFile(from: string, uri: string, context: ResolutionContext): string | null {
const files = resolveDartUri(from, uri, context, memoFor(context));
return files.length === 1 ? files[0]! : null;
}
/** The files declaring `library <name>;`. */
function libraryFilesNamed(name: string, context: ResolutionContext, memo: Memo): readonly string[] {
if (!memo.libraryNames) {
const byName = new Map<string, string[]>();
for (const file of context.getAllFiles()) {
if (!file.endsWith('.dart') || context.fileContains?.(file, 'library') === false) continue;
const declared = directivesOf(file, context, memo)?.libraryName;
if (!declared) continue;
const list = byName.get(declared);
if (list) list.push(file);
else byName.set(declared, [file]);
}
memo.libraryNames = byName;
}
return memo.libraryNames.get(name) ?? [];
}
/** The file a part's `part of` names, or null when it names none this can find. */
function partParent(file: string, partOf: NonNullable<DartDirectives['partOf']>, context: ResolutionContext, memo: Memo): string | null {
const candidates = 'uri' in partOf
? resolveDartUri(file, partOf.uri, context, memo).filter((f) => directivesOf(f, context, memo) !== null)
: libraryFilesNamed(partOf.name, context, memo);
if (candidates.length <= 1) return candidates[0] ?? null;
const listing = candidates.filter((c) =>
(directivesOf(c, context, memo)?.parts ?? []).some((uri) => resolveDartUri(c, uri, context, memo).includes(file)));
return listing.length === 1 ? listing[0]! : null;
}
/** The library file `file` belongs to: itself, or for a part the library its `part of` chain ends at; null when unknown. */
function libraryOf(file: string, context: ResolutionContext, memo: Memo): string | null {
const hit = memo.libraryOf.get(file);
if (hit !== undefined) return hit;
// A part cycle ends at null.
memo.libraryOf.set(file, null);
const directives = directivesOf(file, context, memo);
let library: string | null = null;
if (directives && !directives.partOf) {
library = file;
} else if (directives?.partOf) {
const parent = partParent(file, directives.partOf, context, memo);
if (parent !== null) {
memo.parentOf.set(file, parent);
library = libraryOf(parent, context, memo);
}
}
memo.libraryOf.set(file, library);
return library;
}
/** A library's files: the library and its parts, theirs included. */
function membersOf(library: string, context: ResolutionContext, memo: Memo): ReadonlySet<string> {
const hit = memo.members.get(library);
if (hit) return hit;
const files = new Set<string>([library]);
const queue = [library];
while (queue.length > 0 && files.size < 1000) {
const file = queue.shift()!;
for (const uri of directivesOf(file, context, memo)?.parts ?? []) {
for (const part of resolveDartUri(file, uri, context, memo)) {
if (files.has(part)) continue;
files.add(part);
queue.push(part);
}
}
}
memo.members.set(library, files);
return files;
}
function addFilter(namespace: Map<string, NameFilter[]>, file: string, filter: NameFilter): void {
const filters = namespace.get(file);
if (!filters) namespace.set(file, [filter]);
else if (filters[0] === EVERY_NAME) return;
else if (filter === EVERY_NAME) filters.splice(0, filters.length, EVERY_NAME);
else if (!filters.some((f) => f.key === filter.key)) filters.push(filter);
}
/**
* The files whose public names `library` exports, each with the filters on
* the way: its own files, and what its `export`s reach. `low` is the
* shallowest library still on `stack` that the walk came back to; a result
* that leans on one is incomplete until that library finishes, so it is kept
* only when the walk came back to nothing shallower than this library.
*/
function exportsOf(
library: string,
context: ResolutionContext,
memo: Memo,
stack: Map<string, number>,
): { namespace: ReadonlyMap<string, readonly NameFilter[]>; low: number } {
const hit = memo.exports.get(library);
if (hit) return { namespace: hit, low: Infinity };
const depth = stack.size;
stack.set(library, depth);
const namespace = new Map<string, NameFilter[]>();
let low = Infinity;
const members = membersOf(library, context, memo);
for (const file of members) addFilter(namespace, file, EVERY_NAME);
for (const file of members) {
for (const directive of directivesOf(file, context, memo)?.exports ?? []) {
for (const uri of directive.uris) {
for (const target of resolveDartUri(file, uri, context, memo)) {
const exported = libraryOf(target, context, memo);
if (exported === null) continue;
const onStack = stack.get(exported);
if (onStack !== undefined) {
low = Math.min(low, onStack);
continue;
}
const inner = exportsOf(exported, context, memo, stack);
low = Math.min(low, inner.low);
for (const [f, filters] of inner.namespace) {
for (const filter of filters) addFilter(namespace, f, composeFilters(directive.filter, filter));
}
}
}
}
}
stack.delete(library);
if (low >= depth) memo.exports.set(library, namespace);
return { namespace, low };
}
/**
* What `file` can see, or null when that is unknown: its library cannot be
* found, or no pubspec.yaml above it says which package its `package:`
* imports start from (a mason brick's template, a project indexed below its
* package root).
*/
function scopeOf(file: string, context: ResolutionContext, memo: Memo): FileScope | null {
const hit = memo.scopes.get(file);
if (hit !== undefined) return hit;
const library = packageOfDir(parentDir(file), context, memo) === null ? null : libraryOf(file, context, memo);
let scope: FileScope | null = null;
if (library !== null) {
const own = new Set(membersOf(library, context, memo));
own.add(file);
const importers: string[] = [];
for (let f: string | undefined = file; f !== undefined && importers.length < 32; f = memo.parentOf.get(f)) {
importers.push(f);
if (f === library) break;
}
scope = { own, importers, imported: null, visible: new Map() };
}
memo.scopes.set(file, scope);
return scope;
}
/**
* The filters through which the imports written with `prefix` ('' for none)
* bring `declFile`'s names in — empty when they bring in none of them — or
* null when no import has that prefix.
*/
function importedFilters(scope: FileScope, prefix: string, declFile: string, context: ResolutionContext, memo: Memo): readonly NameFilter[] | null {
if (!scope.imported) {
scope.imported = new Map();
for (const importer of scope.importers) {
for (const directive of directivesOf(importer, context, memo)?.imports ?? []) {
const key = directive.prefix ?? '';
const libraries = scope.imported.get(key) ?? [];
scope.imported.set(key, libraries);
for (const uri of directive.uris) {
for (const target of resolveDartUri(importer, uri, context, memo)) {
const library = libraryOf(target, context, memo);
if (library !== null) libraries.push({ filter: directive.filter, library });
}
}
}
}
}
const libraries = scope.imported.get(prefix);
if (!libraries) return prefix === '' ? [] : null;
const key = `${prefix}\0${declFile}`;
const hit = scope.visible.get(key);
if (hit) return hit;
const through = new Map<string, NameFilter[]>();
for (const { filter, library } of libraries) {
for (const exported of exportsOf(library, context, memo, new Map()).namespace.get(declFile) ?? []) {
addFilter(through, declFile, composeFilters(filter, exported));
}
}
const filters = through.get(declFile) ?? [];
scope.visible.set(key, filters);
return filters;
}
/**
* Whether the library of `fromFile` can name `name`, declared at the top level
* of `declFile`, without a prefix: `declFile` is one of the library's own
* files, or a library it imports unprefixed exports the name. True when the
* library of `fromFile` cannot be established.
*/
export function dartLibrarySees(fromFile: string, declFile: string, name: string, context: ResolutionContext): boolean {
if (fromFile === declFile) return true;
const memo = memoFor(context);
const scope = scopeOf(fromFile, context, memo);
if (!scope || scope.own.has(declFile)) return true;
if (name.startsWith('_')) return false;
return (importedFilters(scope, '', declFile, context, memo) ?? []).some((f) => lets(f, name));
}
/**
* Whether `prefix.name` in `fromFile` can mean `name` declared at the top
* level of `declFile`: a library imported `as prefix` exports it. True when
* no import of the file's library has that prefix, or the library is unknown.
*/
export function dartPrefixSees(fromFile: string, prefix: string, declFile: string, name: string, context: ResolutionContext): boolean {
const memo = memoFor(context);
const scope = scopeOf(fromFile, context, memo);
if (!scope) return true;
const filters = importedFilters(scope, prefix, declFile, context, memo);
return filters === null || (!name.startsWith('_') && filters.some((f) => lets(f, name)));
}
/**
* The import prefixes written in `fromFile` can stand for — the `p` of
* `import '…' as p` and `deferred as p` — read from the imports of its
* library, which a part (`part of …`) takes from the file it is part of.
*/
export function dartImportPrefixes(fromFile: string, context: ResolutionContext): ReadonlySet<string> {
const memo = memoFor(context);
const hit = memo.prefixes.get(fromFile);
if (hit) return hit;
const prefixes = new Set<string>();
// Records each part's parent, a library in no package included.
libraryOf(fromFile, context, memo);
let file: string | undefined = fromFile;
for (let hops = 0; file !== undefined && hops < 32; file = memo.parentOf.get(file), hops++) {
for (const directive of directivesOf(file, context, memo)?.imports ?? []) {
if (directive.prefix !== null) prefixes.add(directive.prefix);
}
}
memo.prefixes.set(fromFile, prefixes);
return prefixes;
}
/** Whether `declFile` is a file of the library `fromFile` belongs to (false when that library is unknown). */
export function inSameDartLibrary(fromFile: string, declFile: string, context: ResolutionContext): boolean {
if (fromFile === declFile) return true;
const scope = scopeOf(fromFile, context, memoFor(context));
return scope !== null && scope.own.has(declFile);
}