mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
Merge 28e01b8722 into 6bb99767ff
This commit is contained in:
commit
c40cb71bdb
24 changed files with 1062 additions and 83 deletions
|
|
@ -298,42 +298,42 @@
|
|||
"imports": 3200,
|
||||
"resolved": 1153,
|
||||
"distinct_outcomes": 2987,
|
||||
"fingerprint": "318084f48ffa4eeae4a5b7fc25916d4ad78673d92dba62b7e462d1bb87ca553a"
|
||||
"fingerprint": "eb36bd1ccab5c4405cc79d48732b7aa272a90351db60664137663dde7b2db73c"
|
||||
},
|
||||
"large": {
|
||||
"files": 1600,
|
||||
"imports": 12800,
|
||||
"resolved": 4681,
|
||||
"distinct_outcomes": 11875,
|
||||
"fingerprint": "5151cd2498bd4b7698dc9309e2539977d306f9ba82a388c630c89b51fc4a3187"
|
||||
"fingerprint": "08c288bdeaa6f4518e7f5e32e3a8ac6265a5147ec366d37789db6cb573ed2bbe"
|
||||
},
|
||||
"deep": {
|
||||
"files": 400,
|
||||
"imports": 3200,
|
||||
"resolved": 1153,
|
||||
"distinct_outcomes": 2987,
|
||||
"fingerprint": "79776ec1c22afa619fd31aeb05dcac567723b436d0461a5782693b9e929f6f74"
|
||||
"fingerprint": "909c8444e0004692be0d0a8179f56173e87d790ad7aeea38bb54c647ce0d638f"
|
||||
},
|
||||
"collide": {
|
||||
"files": 400,
|
||||
"imports": 3200,
|
||||
"resolved": 1153,
|
||||
"distinct_outcomes": 2999,
|
||||
"fingerprint": "1b145a4c3b41ffc4efa26f74449c3d44646d6d59728163ac896fc0ee25c6d608"
|
||||
"fingerprint": "4625f0310442e60b4291fd9f5c9b90333f6e9175ab5b4c7c7eb2ecbd5def500d"
|
||||
},
|
||||
"collide_large": {
|
||||
"files": 1600,
|
||||
"imports": 12800,
|
||||
"resolved": 4681,
|
||||
"distinct_outcomes": 11948,
|
||||
"fingerprint": "b7e5303220b8fa64e85c7e17622961018316a10c5ef921a02584864309748b52"
|
||||
"fingerprint": "9e7aa3a503352b1f437a70e51ccfe020dff65da319c6082fb25b1ae4417c3531"
|
||||
},
|
||||
"fingerprint": "5151cd2498bd4b7698dc9309e2539977d306f9ba82a388c630c89b51fc4a3187",
|
||||
"fingerprint": "08c288bdeaa6f4518e7f5e32e3a8ac6265a5147ec366d37789db6cb573ed2bbe",
|
||||
"heap": {
|
||||
"files_small": 8000,
|
||||
"files_large": 32000,
|
||||
"path_segments": 11,
|
||||
"probe": "package:ext0/src/thing.dart"
|
||||
"probe": "./src/thing.dart"
|
||||
},
|
||||
"_measured": {
|
||||
"collide_ms": 1.511,
|
||||
|
|
|
|||
|
|
@ -208,12 +208,14 @@
|
|||
* workload — identical file, import and resolved counts — laid out the way
|
||||
* these languages are actually written: `svcN/internal/`, `SrcN/Models/`, a
|
||||
* `mod0.dart`/`mod0.rb` in every package. Measured on that shape the per-import
|
||||
* cost is NOT corpus-size-independent for the four resolvers that scan a
|
||||
* cost is NOT corpus-size-independent for the three resolvers that scan a
|
||||
* bucket:
|
||||
*
|
||||
* - go, csharp and java walk `PackageDirIndex.dirsByLastSegment[seg]`, which
|
||||
* now holds every directory;
|
||||
* - dart walks its basename bucket, which now holds every same-named file;
|
||||
* - dart formerly walked its basename bucket. Since #2963 its package-URI
|
||||
* arms use exact declared-package paths; the separate heap probe and
|
||||
* scan-count regressions still exercise its relative-path suffix index;
|
||||
* - ruby, kotlin, php and cobol answer from keyed maps and are collision-
|
||||
* IMMUNE, so their collide budgets are the linear ones — that immunity is
|
||||
* the assertion, and for cobol the arm is also the only one that reaches
|
||||
|
|
@ -849,7 +851,8 @@ function uniqueDir(lang, d, i) {
|
|||
// and breaks its same-workload invariant. C# therefore exercises the removed
|
||||
// rule through progressive stripping rather than through its primary query.
|
||||
if (lang === 'csharp') return d % 7 === 0 ? `src/Ns${d}/Sub/Ns${d}` : `src/Ns${d}`;
|
||||
if (lang === 'dart') return d % 3 === 0 ? `lib/feature${d}` : `pkg/feature${d}`;
|
||||
// Keep every synthetic local import valid under app's pubspec lib root.
|
||||
if (lang === 'dart') return `lib/feature${d}`;
|
||||
if (lang === 'kotlin') {
|
||||
return d % 7 === 0
|
||||
? `mod${d}/src/main/kotlin/com/example/pkg${d}/inner/pkg${d}`
|
||||
|
|
@ -1500,11 +1503,10 @@ function collideTarget(lang, { local, r, d, j, dirs }) {
|
|||
}
|
||||
if (lang === 'dart') {
|
||||
return local
|
||||
? `package:app/pkg${j % dirs}/lib/src/mod${Math.floor(j / dirs)}.dart`
|
||||
? `package:pkg${j % dirs}/src/mod${Math.floor(j / dirs)}.dart`
|
||||
: (r >>> 3) % 3 === 0
|
||||
? ['dart:core', 'dart:async', 'dart:io'][(r >>> 4) % 3]
|
||||
: // A repeated basename under a directory nothing carries: both
|
||||
// candidates walk the whole basename bucket and miss.
|
||||
: // Foreign package names must miss despite repeated local basenames.
|
||||
`package:ext${(r >>> 4) % 97}/other/mod${(r >>> 4) % 8}.dart`;
|
||||
}
|
||||
if (lang === 'kotlin') {
|
||||
|
|
@ -1740,6 +1742,16 @@ function buildRepo(lang, fileCount, pad = 0, shape = 'unique') {
|
|||
* hide their build from rep 2 onward and `fastest()` reports the minimum.
|
||||
*/
|
||||
function newPass(lang, files, pad = 0) {
|
||||
if (lang === 'dart') {
|
||||
// Synthetic pubspec declarations: app at the root, one package per
|
||||
// collision directory. Package imports no longer suffix-match (#2963).
|
||||
const packages = new Map([['app', joinBase(tsBaseUrlFor(pad), 'lib')]]);
|
||||
for (const file of files) {
|
||||
const match = /(?:^|\/)(pkg\d+)\/lib\//.exec(file);
|
||||
if (match) packages.set(match[1], joinBase(tsBaseUrlFor(pad), `${match[1]}/lib`));
|
||||
}
|
||||
return { allFilePaths: new Set(files), config: { packages } };
|
||||
}
|
||||
if (HEADER_EXTENSION[lang] !== undefined) {
|
||||
const sources = [];
|
||||
const headers = [];
|
||||
|
|
@ -1803,7 +1815,7 @@ function resolveAll(lang, files, imports, pad = 0) {
|
|||
function resolveOne(lang, from, target, pass) {
|
||||
const allFilePaths = pass.allFilePaths;
|
||||
if (lang === 'go') return resolveGoImportTarget(target, from, allFilePaths, GO_MODULE);
|
||||
if (lang === 'dart') return resolveDartImportTarget(target, from, allFilePaths);
|
||||
if (lang === 'dart') return resolveDartImportTarget(target, from, allFilePaths, pass.config);
|
||||
if (lang === 'ruby') return resolveRubyImportTarget(target, from, allFilePaths);
|
||||
if (lang === 'kotlin') {
|
||||
const parsedImport = {
|
||||
|
|
@ -2154,8 +2166,8 @@ const HEAP_PROBE_TARGET = {
|
|||
// end. Chosen from the miss family that reaches furthest into each cascade:
|
||||
// - `go` names a missing package inside GO_MODULE, which reaches the
|
||||
// package-directory lookup and forces `PackageDirIndex`;
|
||||
// - `dart` is an external package, so BOTH candidate paths miss and both
|
||||
// walk the basename bucket to completion;
|
||||
// - `dart` uses a relative miss to retain coverage of its basename index;
|
||||
// package imports now use exact membership and allocate no file index;
|
||||
// - `kotlin` misses after building its declared-package/module-binding index;
|
||||
// - `cobol` misses in both tier maps, `swift` in `byModule`, and `rust`
|
||||
// probes candidate paths and builds nothing — that last is the reading
|
||||
|
|
@ -2165,7 +2177,7 @@ const HEAP_PROBE_TARGET = {
|
|||
// bound compares like with like. `vue`'s is bare rather than `@/…`
|
||||
// because the alias branch rewrites to `src/` and would resolve.
|
||||
go: 'example.com/mod/repo0/pkg/util',
|
||||
dart: 'package:ext0/src/thing.dart',
|
||||
dart: './src/thing.dart',
|
||||
kotlin: 'com.ghost0.deep.Missing',
|
||||
cobol: 'VENDOR0',
|
||||
swift: 'ExternalPkg0',
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
/**
|
||||
* `resolveImportTarget` adapter for the Dart `ScopeResolver`. Ports the
|
||||
* legacy-DAG Dart import logic (`import-resolvers/configs/dart.ts`):
|
||||
* Dart import logic:
|
||||
*
|
||||
* - `dart:` SDK imports → `null` (external, no edge)
|
||||
* - `package:pkg/path` → `lib/path` (or bare `path`) matched
|
||||
* against the workspace file set
|
||||
* - `package:pkg/path` → declared package's exact `lib/path`
|
||||
* - relative `'foo/bar.dart'` → resolved against the importer's dir
|
||||
* - `__heritage__:` markers → `null` (synthetic heritage carrier,
|
||||
* consumed by `emitDartHeritageEdges`)
|
||||
*
|
||||
* The `ScopeResolver` hook signature is `(targetRaw, fromFile, allFilePaths)`;
|
||||
* Package identity comes from the workspace's pubspec resolution config;
|
||||
* `targetRaw` arrives already quote-stripped from `interpretDartImport`.
|
||||
*/
|
||||
|
||||
import { perFileSet } from '../../import-resolvers/per-file-set.js';
|
||||
import { DART_HERITAGE_PREFIX } from './interpret.js';
|
||||
import type { DartPackageConfig } from './package-config.js';
|
||||
|
||||
/**
|
||||
* Basename → files carrying it, in `allFilePaths` iteration order, memoized on
|
||||
* the Set's identity (#2879).
|
||||
*
|
||||
* Both resolution legs answered `fp === candidate || fp.endsWith('/' + candidate)`
|
||||
* with a full workspace scan, and the `package:` leg ran one scan PER candidate
|
||||
* — for an external package both candidates miss, so both scans always ran to
|
||||
* completion. The orchestrator passes the same Set to every import in a pass,
|
||||
* so the index is built once per run.
|
||||
* Relative-path suffix fallback uses this index instead of scanning the
|
||||
* workspace for each import. Package imports use exact Set membership only.
|
||||
*
|
||||
* Bucketing by basename is exact rather than a heuristic: a path satisfying
|
||||
* either arm of the match ends with `candidate`, so its last `/`-delimited
|
||||
|
|
@ -89,6 +86,7 @@ export function resolveDartImportTarget(
|
|||
targetRaw: string,
|
||||
fromFile: string,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
resolutionConfig?: unknown,
|
||||
): string | readonly string[] | null {
|
||||
if (targetRaw.startsWith(DART_HERITAGE_PREFIX)) return null;
|
||||
// `targetRaw` already arrives quote-stripped from `interpretDartImport`.
|
||||
|
|
@ -97,17 +95,22 @@ export function resolveDartImportTarget(
|
|||
// Dart SDK imports never resolve to a repo file.
|
||||
if (targetRaw.startsWith('dart:')) return null;
|
||||
|
||||
// `package:pkg/path.dart` → `lib/path.dart` (or bare `path.dart`).
|
||||
// A package URI never falls back to another package's same-named file.
|
||||
if (targetRaw.startsWith('package:')) {
|
||||
const slash = targetRaw.indexOf('/');
|
||||
if (slash === -1) return null;
|
||||
const packageName = targetRaw.slice('package:'.length, slash);
|
||||
const config = resolutionConfig as DartPackageConfig | undefined;
|
||||
const lib = config?.packages?.get(packageName);
|
||||
if (lib === undefined) return null;
|
||||
const relPath = targetRaw.slice(slash + 1);
|
||||
// Candidate priority is load-bearing: `lib/<rel>` before bare `<rel>`.
|
||||
for (const candidate of [`lib/${relPath}`, relPath]) {
|
||||
const hit = findByPathSuffix(allFilePaths, candidate);
|
||||
if (hit !== null) return hit;
|
||||
}
|
||||
return null; // external package
|
||||
if (
|
||||
/[\\%?#:]/.test(relPath) ||
|
||||
relPath.split('/').some((part) => part === '' || part === '.' || part === '..')
|
||||
)
|
||||
return null;
|
||||
const candidate = `${lib}/${relPath}`;
|
||||
return allFilePaths.has(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
// Relative import.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
* - `query.ts` — tree-sitter scope query + lazy parser/query
|
||||
* - `captures.ts` — `emitDartScopeCaptures` orchestrator
|
||||
* - `interpret.ts` — capture-match → `ParsedImport` / `ParsedTypeBinding`
|
||||
* - `import-target.ts` — `(targetRaw, fromFile, allFilePaths) → file path`
|
||||
* - `import-target.ts` — exact declared-package / relative import targets
|
||||
* - `package-config.ts` — bounded, ignore-aware in-repo pubspec discovery
|
||||
* - `package-dependencies.ts` — manifest dependencies for incremental writeback
|
||||
* - `receiver-binding.ts` — synthesize `this` / `super` type-bindings
|
||||
* - `signature-bindings.ts`— synthesize parameter / return type-bindings
|
||||
* - `arity.ts` — Dart arity compatibility (count-primary)
|
||||
|
|
|
|||
98
gitnexus/src/core/ingestion/languages/dart/package-config.ts
Normal file
98
gitnexus/src/core/ingestion/languages/dart/package-config.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { JSON_SCHEMA, load } from 'js-yaml';
|
||||
import { createWatchIgnorePredicate } from '../../../../config/ignore-service.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
import { getMaxFileSizeBytes } from '../../utils/max-file-size.js';
|
||||
|
||||
export interface DartPackageConfig {
|
||||
readonly packages: ReadonlyMap<string, string>;
|
||||
readonly manifestsByName: ReadonlyMap<string, readonly string[]>;
|
||||
}
|
||||
|
||||
/** Discover only in-repository packages; never follow dependency paths or symlinks. */
|
||||
export async function loadDartPackageConfig(repoPath: string): Promise<DartPackageConfig> {
|
||||
const warn = (reason: string, relativePath = '.'): void => {
|
||||
logger.warn(
|
||||
{ reason, relativePath },
|
||||
'Dart pubspec discovery could not read a valid package declaration.',
|
||||
);
|
||||
};
|
||||
const incomplete = (reason: string, relativePath = '.'): never => {
|
||||
warn(reason, relativePath);
|
||||
throw new Error(`Dart pubspec discovery failed (${reason}): ${relativePath}`);
|
||||
};
|
||||
let isIgnored;
|
||||
try {
|
||||
isIgnored = await createWatchIgnorePredicate(repoPath);
|
||||
} catch {
|
||||
return incomplete('ignore-rules');
|
||||
}
|
||||
const packages = new Map<string, string>();
|
||||
const manifestsByName = new Map<string, string[]>();
|
||||
const maxManifestSize = Math.min(1024 * 1024, getMaxFileSizeBytes());
|
||||
const ambiguous = new Set<string>();
|
||||
const pending = [''];
|
||||
let visited = 0;
|
||||
while (pending.length > 0) {
|
||||
// An incomplete scan cannot establish that package names are unique.
|
||||
if (++visited > 20_000) return incomplete('directory-limit');
|
||||
const relative = pending.pop();
|
||||
if (relative === undefined) break;
|
||||
const directory = path.join(repoPath, relative);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(directory, { withFileTypes: true });
|
||||
} catch {
|
||||
return incomplete('read-directory', relative || '.');
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name.startsWith('.') || isIgnored(entryPath, true)) continue;
|
||||
pending.push(relative ? `${relative}/${entry.name}` : entry.name);
|
||||
} else if (entry.isFile() && entry.name === 'pubspec.yaml' && !isIgnored(entryPath, false)) {
|
||||
const manifestPath = relative ? `${relative}/pubspec.yaml` : 'pubspec.yaml';
|
||||
let size: number;
|
||||
try {
|
||||
size = (await stat(entryPath)).size;
|
||||
} catch {
|
||||
return incomplete('read-pubspec', manifestPath);
|
||||
}
|
||||
if (size > maxManifestSize) return incomplete('manifest-size', manifestPath);
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(entryPath, 'utf8');
|
||||
} catch {
|
||||
return incomplete('read-pubspec', manifestPath);
|
||||
}
|
||||
try {
|
||||
const manifest: unknown = load(content, {
|
||||
schema: JSON_SCHEMA,
|
||||
});
|
||||
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest))
|
||||
continue;
|
||||
const name = (manifest as Record<string, unknown>).name;
|
||||
if (typeof name !== 'string' || !/^[a-z_][a-z0-9_]*$/.test(name)) continue;
|
||||
const manifests = manifestsByName.get(name) ?? [];
|
||||
manifests.push(manifestPath);
|
||||
// Two deterministic witnesses suffice to prove ambiguity. Retaining
|
||||
// more would create unbounded duplicate-package dependency fanout.
|
||||
manifests.sort();
|
||||
if (manifests.length > 2) manifests.length = 2;
|
||||
manifestsByName.set(name, manifests);
|
||||
if (packages.has(name) || ambiguous.has(name)) {
|
||||
packages.delete(name);
|
||||
ambiguous.add(name);
|
||||
} else {
|
||||
packages.set(name, relative ? `${relative}/lib` : 'lib');
|
||||
}
|
||||
} catch {
|
||||
// Invalid YAML cannot declare a package. Other valid packages remain usable.
|
||||
warn('invalid-yaml', manifestPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { packages, manifestsByName };
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import { generateId } from '../../../../lib/utils.js';
|
||||
import type { DartPackageConfig } from './package-config.js';
|
||||
|
||||
const MAX_DEPENDENCIES = 100_000;
|
||||
const MAX_TRAVERSALS = 1_000_000;
|
||||
|
||||
/** Package identity is an input dependency, including unresolved/ambiguous imports. */
|
||||
export function emitDartPackageDependencies(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
resolutionConfig?: unknown,
|
||||
): void {
|
||||
const config = resolutionConfig as DartPackageConfig | undefined;
|
||||
if (!config?.manifestsByName) return;
|
||||
|
||||
const consumersByManifest = new Map<string, Set<string>>();
|
||||
let dependencyCount = 0;
|
||||
const addConsumer = (consumers: Set<string>, sourceId: string): void => {
|
||||
if (consumers.has(sourceId)) return;
|
||||
if (++dependencyCount > MAX_DEPENDENCIES) {
|
||||
throw new Error(
|
||||
'Dart package dependency limit exceeded; narrow the workspace with .gitnexusignore.',
|
||||
);
|
||||
}
|
||||
consumers.add(sourceId);
|
||||
};
|
||||
const fileIds = new Set(parsedFiles.map((parsed) => generateId('File', parsed.filePath)));
|
||||
for (const parsed of parsedFiles) {
|
||||
const sourceId = generateId('File', parsed.filePath);
|
||||
if (!graph.getNode(sourceId)) continue;
|
||||
for (const imp of parsed.parsedImports) {
|
||||
const raw = imp.targetRaw;
|
||||
if (typeof raw !== 'string' || !raw.startsWith('package:')) continue;
|
||||
const slash = raw.indexOf('/');
|
||||
if (slash === -1) continue;
|
||||
for (const manifest of config.manifestsByName.get(raw.slice(8, slash)) ?? []) {
|
||||
let consumers = consumersByManifest.get(manifest);
|
||||
if (!consumers) consumersByManifest.set(manifest, (consumers = new Set()));
|
||||
addConsumer(consumers, sourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (consumersByManifest.size === 0) return;
|
||||
|
||||
// New manifests are not present in the old DB's importer closure. Carry
|
||||
// identity dependencies through the freshly resolved imports as well, so
|
||||
// one-hop incremental boundary expansion rewrites downstream consumers.
|
||||
const importers = new Map<string, Set<string>>();
|
||||
for (const edge of graph.iterRelationshipsByType('IMPORTS')) {
|
||||
if (!fileIds.has(edge.sourceId) || !fileIds.has(edge.targetId)) continue;
|
||||
let sources = importers.get(edge.targetId);
|
||||
if (!sources) importers.set(edge.targetId, (sources = new Set()));
|
||||
sources.add(edge.sourceId);
|
||||
}
|
||||
let traversals = 0;
|
||||
for (const [manifest, consumers] of consumersByManifest) {
|
||||
const targetId = generateId('File', manifest);
|
||||
if (!graph.getNode(targetId)) {
|
||||
throw new Error(`Dart package manifest is missing from the indexed file set: ${manifest}`);
|
||||
}
|
||||
// Set iteration visits additions and deduplicates cycles without recursion.
|
||||
for (const sourceId of consumers) {
|
||||
for (const importer of importers.get(sourceId) ?? []) {
|
||||
if (++traversals > MAX_TRAVERSALS) {
|
||||
throw new Error(
|
||||
'Dart package dependency traversal limit exceeded; narrow the workspace with .gitnexusignore.',
|
||||
);
|
||||
}
|
||||
addConsumer(consumers, importer);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate the complete closure before publishing any dependency edges.
|
||||
for (const [manifest, consumers] of consumersByManifest) {
|
||||
const targetId = generateId('File', manifest);
|
||||
for (const sourceId of consumers) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPORTS', `${sourceId}->${targetId}`),
|
||||
sourceId,
|
||||
targetId,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1,
|
||||
reason: 'dart-scope: package identity dependency',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,8 @@ import { typeApplicationArguments } from '../../utils/template-arguments.js';
|
|||
import type { HeritageTypeArgumentSink } from '../../scope-resolution/utils/generic-instantiation.js';
|
||||
import { expandDartWildcardNames } from './expand-wildcards.js';
|
||||
import { dartIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
|
||||
import { loadDartPackageConfig } from './package-config.js';
|
||||
import { emitDartPackageDependencies } from './package-dependencies.js';
|
||||
|
||||
interface ClassDefRef {
|
||||
readonly graphId: string;
|
||||
|
|
@ -199,8 +201,13 @@ export const dartScopeResolver: ScopeResolver = {
|
|||
languageProvider: dartProvider,
|
||||
importEdgeReason: 'dart-scope: import',
|
||||
|
||||
resolveImportTarget: (targetRaw, fromFile, allFilePaths) =>
|
||||
resolveDartImportTarget(targetRaw, fromFile, allFilePaths),
|
||||
loadResolutionConfig: loadDartPackageConfig,
|
||||
// Package dependencies use cached ParsedFile facts, never raw source text.
|
||||
postExtractSourceTextPolicy: 'uncached-files',
|
||||
emitPostResolutionEdges: (graph, parsedFiles, _nodeLookup, _indexes, ctx) =>
|
||||
emitDartPackageDependencies(graph, parsedFiles, ctx.resolutionConfig),
|
||||
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
|
||||
resolveDartImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
|
||||
|
||||
// Dart `import` is whole-library: every public top-level symbol of the
|
||||
// target enters scope. Enumerating them lets `propagateImportedReturnTypes`
|
||||
|
|
|
|||
4
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/http.dart
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/http.dart
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
void localHttpDecoy() {}
|
||||
void loadOwn() {}
|
||||
void loadData() {}
|
||||
void loadRelative() {}
|
||||
12
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/main.dart
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/main.dart
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:app/models.dart';
|
||||
import 'package:data/models.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:app/tool/run.dart' as tool;
|
||||
import 'dart:io';
|
||||
import './relative.dart';
|
||||
|
||||
void main() {
|
||||
loadOwn();
|
||||
loadData();
|
||||
loadRelative();
|
||||
}
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/models.dart
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/models.dart
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
void loadOwn() {}
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/relative.dart
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/lib/relative.dart
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
void loadRelative() {}
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/packages/data/lib/models.dart
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/packages/data/lib/models.dart
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
void loadData() {}
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/packages/data/pubspec.yaml
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/packages/data/pubspec.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
name: data
|
||||
5
gitnexus/test/fixtures/lang-resolution/dart-package-imports/pubspec.yaml
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/dart-package-imports/pubspec.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
name: app
|
||||
dependencies:
|
||||
http: ^1.0.0
|
||||
data:
|
||||
path: packages/data
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/tool/run.dart
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-package-imports/tool/run.dart
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
void nonLibraryDecoy() {}
|
||||
|
|
@ -29,10 +29,7 @@ const { resolveImportTarget } = dartScopeResolver;
|
|||
const FROM_FILE = 'lib/main.dart';
|
||||
|
||||
/**
|
||||
* A synthetic Dart package: many library files under `lib/src/`, plus the two
|
||||
* targets the `package:` leg addresses — one reachable as `lib/<rel>` and one
|
||||
* only as bare `<rel>`, which is the second candidate and therefore the leg
|
||||
* that used to run a second full scan for every external import.
|
||||
* A synthetic Dart package with library and non-library targets.
|
||||
*/
|
||||
function buildWorkspace(fileCount: number): CountingSet {
|
||||
const files: string[] = [];
|
||||
|
|
@ -52,13 +49,10 @@ describe('Dart import resolution — index reuse across imports (#2879)', () =>
|
|||
const resolved: (string | readonly string[] | null)[] = [];
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
// Three shapes: an in-package hit through `lib/<rel>`, a bare-`<rel>` hit
|
||||
// that only the SECOND candidate answers, and an external package whose
|
||||
// two candidates both miss — the case that used to cost two full
|
||||
// workspace scans per import.
|
||||
resolved.push(resolveImportTarget('package:app/models.dart', FROM_FILE, files));
|
||||
resolved.push(resolveImportTarget('package:app/tool/generate.dart', FROM_FILE, files));
|
||||
resolved.push(resolveImportTarget(`package:vendor${i}/ghost${i}.dart`, FROM_FILE, files));
|
||||
// Exact relative hits plus misses that must reuse the suffix index.
|
||||
resolved.push(resolveImportTarget('./models.dart', FROM_FILE, files));
|
||||
resolved.push(resolveImportTarget('../tool/generate.dart', FROM_FILE, files));
|
||||
resolved.push(resolveImportTarget(`vendor${i}/ghost${i}.dart`, FROM_FILE, files));
|
||||
}
|
||||
|
||||
expect(files.scans).toBe(1);
|
||||
|
|
@ -74,8 +68,8 @@ describe('Dart import resolution — index reuse across imports (#2879)', () =>
|
|||
expectDistinctFileSetsGetOwnIndex({
|
||||
resolveImportTarget,
|
||||
buildWorkspace: () => buildWorkspace(20),
|
||||
targetRaw: 'package:app/models.dart',
|
||||
fromFile: FROM_FILE,
|
||||
targetRaw: 'models.dart',
|
||||
fromFile: 'main.dart',
|
||||
resolutionConfig: undefined,
|
||||
expected: 'lib/models.dart',
|
||||
expectedScans: 1,
|
||||
|
|
@ -85,15 +79,14 @@ describe('Dart import resolution — index reuse across imports (#2879)', () =>
|
|||
it('still resolves real imports correctly (the perf test is not vacuous)', () => {
|
||||
const files = buildWorkspace(5);
|
||||
|
||||
// `package:` leg, first candidate: `lib/<rel>`.
|
||||
expect(resolveImportTarget('package:app/models.dart', FROM_FILE, files)).toBe(
|
||||
const config = { packages: new Map([['app', 'lib']]) };
|
||||
expect(resolveImportTarget('package:app/models.dart', FROM_FILE, files, config)).toBe(
|
||||
'lib/models.dart',
|
||||
);
|
||||
// `package:` leg, second candidate: bare `<rel>`, reached only after
|
||||
// `lib/<rel>` misses entirely.
|
||||
expect(resolveImportTarget('package:app/tool/generate.dart', FROM_FILE, files)).toBe(
|
||||
'tool/generate.dart',
|
||||
);
|
||||
// Package imports cannot reach files outside their declared lib directory.
|
||||
expect(
|
||||
resolveImportTarget('package:app/tool/generate.dart', FROM_FILE, files, config),
|
||||
).toBeNull();
|
||||
// Relative import against the importer's directory.
|
||||
expect(resolveImportTarget('src/util.dart', FROM_FILE, files)).toBe('lib/src/util.dart');
|
||||
expect(resolveImportTarget('./src/util.dart', FROM_FILE, files)).toBe('lib/src/util.dart');
|
||||
|
|
|
|||
|
|
@ -37,6 +37,40 @@ if (dartAvailable) {
|
|||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart pubspec package identity (#2963)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-package-imports'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('emits declared imports and their package identity dependencies', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS')
|
||||
.filter((edge) => edge.sourceFilePath === 'lib/main.dart')
|
||||
.map((edge) => edge.targetFilePath)
|
||||
.sort();
|
||||
expect(imports).toEqual([
|
||||
'lib/models.dart',
|
||||
'lib/relative.dart',
|
||||
'packages/data/lib/models.dart',
|
||||
'packages/data/pubspec.yaml',
|
||||
'pubspec.yaml',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['loadOwn', 'lib/models.dart'],
|
||||
['loadData', 'packages/data/lib/models.dart'],
|
||||
['loadRelative', 'lib/relative.dart'],
|
||||
])('resolves %s in the correct library', (name, file) => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(edge) => edge.sourceFilePath === 'lib/main.dart' && edge.target === name,
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.targetFilePath).toBe(file);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 8: Field-type resolution ──────────────────────────────────────
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart field-type resolution', () => {
|
||||
|
|
|
|||
221
gitnexus/test/unit/dart-package-dependencies.test.ts
Normal file
221
gitnexus/test/unit/dart-package-dependencies.test.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { ParsedFile, ScopeId } from 'gitnexus-shared';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
||||
import { generateId } from '../../src/lib/utils.js';
|
||||
import { emitDartPackageDependencies } from '../../src/core/ingestion/languages/dart/package-dependencies.js';
|
||||
import { computeEffectiveWriteSet } from '../../src/core/incremental/subgraph-extract.js';
|
||||
import { GraphEmitSink } from '../../src/core/lbug/graph-emit-sink.js';
|
||||
|
||||
function consumer(filePath: string, targets: string[]): ParsedFile {
|
||||
return {
|
||||
filePath,
|
||||
moduleScope: `module:${filePath}` as ScopeId,
|
||||
scopes: [],
|
||||
parsedImports: targets.map((targetRaw) => ({ kind: 'wildcard', targetRaw })),
|
||||
localDefs: [],
|
||||
referenceSites: [],
|
||||
};
|
||||
}
|
||||
|
||||
function graphWithFiles(paths: string[]) {
|
||||
const graph = createKnowledgeGraph();
|
||||
for (const filePath of paths) {
|
||||
graph.addNode({
|
||||
id: generateId('File', filePath),
|
||||
label: 'File',
|
||||
properties: { name: filePath, filePath },
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
describe('Dart package identity dependencies', () => {
|
||||
it.each([
|
||||
{ files: 400, packages: 251, dense: false, limit: 'dependency limit' },
|
||||
{ files: 150, packages: 100, dense: true, limit: 'dependency traversal limit' },
|
||||
])(
|
||||
'fails explicitly before partial emission at the $limit',
|
||||
({ files, packages, dense, limit }) => {
|
||||
const paths = Array.from({ length: files }, (_, i) => `f${i}.dart`);
|
||||
const manifests = Array.from({ length: packages }, (_, i) => `p${i}/pubspec.yaml`);
|
||||
const graph = graphWithFiles([...paths, ...manifests]);
|
||||
for (let i = 0; i < files; i++) {
|
||||
const targets = dense ? paths : [paths[(i + 1) % files]];
|
||||
for (const target of targets) {
|
||||
graph.addRelationship({
|
||||
id: `${paths[i]}->${target}`,
|
||||
sourceId: `File:${paths[i]}`,
|
||||
targetId: `File:${target}`,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1,
|
||||
reason: 'dart-scope: import',
|
||||
});
|
||||
}
|
||||
}
|
||||
const originalCount = graph.relationshipCount;
|
||||
expect(() =>
|
||||
emitDartPackageDependencies(
|
||||
graph,
|
||||
paths.map((file, i) => consumer(file, [`package:p${i % packages}/model.dart`])),
|
||||
{
|
||||
packages: new Map(),
|
||||
manifestsByName: new Map(manifests.map((file, i) => [`p${i}`, [file]])),
|
||||
},
|
||||
),
|
||||
).toThrow(`Dart package ${limit} exceeded`);
|
||||
expect(graph.relationshipCount).toBe(originalCount);
|
||||
},
|
||||
);
|
||||
|
||||
it('preserves transitive dependencies and idempotence through the streaming sink', async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'dart-package-stream-'));
|
||||
const graph = graphWithFiles(['a.dart', 'b.dart', 'pubspec.yaml']);
|
||||
const sink = new GraphEmitSink(graph, path.join(root, 'csv'));
|
||||
try {
|
||||
sink.beginStreaming();
|
||||
sink.addRelationship({
|
||||
id: 'IMPORTS:File:b.dart->File:a.dart',
|
||||
sourceId: 'File:b.dart',
|
||||
targetId: 'File:a.dart',
|
||||
type: 'IMPORTS',
|
||||
confidence: 1,
|
||||
reason: 'dart-scope: import',
|
||||
});
|
||||
const parsed = [
|
||||
consumer('a.dart', ['package:app/model.dart']),
|
||||
consumer('b.dart', ['./a.dart']),
|
||||
];
|
||||
const config = { packages: new Map(), manifestsByName: new Map([['app', ['pubspec.yaml']]]) };
|
||||
emitDartPackageDependencies(sink, parsed, config);
|
||||
emitDartPackageDependencies(sink, parsed, config);
|
||||
const edges: string[] = [];
|
||||
sink.forEachRelationshipFields((source, target, type, _confidence, reason) => {
|
||||
if (type === 'IMPORTS') edges.push(`${source}->${target}:${reason}`);
|
||||
});
|
||||
expect(edges.sort()).toEqual([
|
||||
'File:a.dart->File:pubspec.yaml:dart-scope: package identity dependency',
|
||||
'File:b.dart->File:a.dart:dart-scope: import',
|
||||
'File:b.dart->File:pubspec.yaml:dart-scope: package identity dependency',
|
||||
]);
|
||||
const manifest = sink.finalize();
|
||||
expect(manifest.totalRows).toBe(3);
|
||||
const csv = manifest.relsByPair.get('File|File');
|
||||
expect(csv?.rows).toBe(3);
|
||||
if (!csv) throw new Error('Missing File-to-File CSV');
|
||||
expect((await readFile(csv.csvPath, 'utf8')).trim().split('\n')).toHaveLength(4);
|
||||
expect(graph.relationshipCount).toBe(0);
|
||||
} finally {
|
||||
sink.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('tracks all duplicate candidates even when no package resolves, idempotently', () => {
|
||||
const graph = graphWithFiles(['main.dart', 'pubspec.yaml', 'nested/pubspec.yaml']);
|
||||
const parsed = [consumer('main.dart', ['package:app/a.dart', 'package:app/b.dart'])];
|
||||
const config = {
|
||||
packages: new Map(),
|
||||
manifestsByName: new Map([['app', ['pubspec.yaml', 'nested/pubspec.yaml']]]),
|
||||
};
|
||||
emitDartPackageDependencies(graph, parsed, config);
|
||||
emitDartPackageDependencies(graph, parsed, config);
|
||||
expect(graph.relationships.map((edge) => [edge.targetId, edge.reason])).toEqual([
|
||||
['File:pubspec.yaml', 'dart-scope: package identity dependency'],
|
||||
['File:nested/pubspec.yaml', 'dart-scope: package identity dependency'],
|
||||
]);
|
||||
expect(computeEffectiveWriteSet(graph, new Set(['nested/pubspec.yaml']))).toEqual(
|
||||
new Set(['nested/pubspec.yaml', 'main.dart']),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not connect unrelated packages, relative imports, SDK imports, or files without imports', () => {
|
||||
const graph = graphWithFiles([
|
||||
'a.dart',
|
||||
'b.dart',
|
||||
'c.dart',
|
||||
'pubspec.yaml',
|
||||
'other/pubspec.yaml',
|
||||
]);
|
||||
emitDartPackageDependencies(
|
||||
graph,
|
||||
[
|
||||
consumer('a.dart', ['package:app/missing.dart', 'package:external/http.dart']),
|
||||
consumer('b.dart', ['./a.dart', 'dart:core']),
|
||||
consumer('c.dart', []),
|
||||
],
|
||||
{
|
||||
packages: new Map(),
|
||||
manifestsByName: new Map([
|
||||
['app', ['pubspec.yaml']],
|
||||
['other', ['other/pubspec.yaml']],
|
||||
]),
|
||||
},
|
||||
);
|
||||
expect(graph.relationships.map((edge) => [edge.sourceId, edge.targetId])).toEqual([
|
||||
['File:a.dart', 'File:pubspec.yaml'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes transitive consumers in fresh-manifest invalidation and terminates on cycles', () => {
|
||||
const graph = graphWithFiles(['a.dart', 'b.dart', 'c.dart', 'pubspec.yaml']);
|
||||
for (const [source, target] of [
|
||||
['b.dart', 'a.dart'],
|
||||
['c.dart', 'b.dart'],
|
||||
['a.dart', 'c.dart'],
|
||||
]) {
|
||||
graph.addRelationship({
|
||||
id: `${source}->${target}`,
|
||||
sourceId: `File:${source}`,
|
||||
targetId: `File:${target}`,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1,
|
||||
reason: 'dart-scope: import',
|
||||
});
|
||||
}
|
||||
emitDartPackageDependencies(
|
||||
graph,
|
||||
[
|
||||
consumer('a.dart', ['package:app/a.dart']),
|
||||
consumer('b.dart', ['./a.dart']),
|
||||
consumer('c.dart', ['./b.dart']),
|
||||
],
|
||||
{ packages: new Map(), manifestsByName: new Map([['app', ['pubspec.yaml']]]) },
|
||||
);
|
||||
expect(
|
||||
graph.relationships
|
||||
.filter((edge) => edge.reason === 'dart-scope: package identity dependency')
|
||||
.map((edge) => [edge.sourceId, edge.targetId]),
|
||||
).toEqual([
|
||||
['File:a.dart', 'File:pubspec.yaml'],
|
||||
['File:b.dart', 'File:pubspec.yaml'],
|
||||
['File:c.dart', 'File:pubspec.yaml'],
|
||||
]);
|
||||
expect(computeEffectiveWriteSet(graph, new Set(['pubspec.yaml']))).toEqual(
|
||||
new Set(['pubspec.yaml', 'a.dart', 'b.dart', 'c.dart']),
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses to persist dependencies on manifests absent from the indexed graph', () => {
|
||||
const graph = graphWithFiles(['a.dart']);
|
||||
expect(() =>
|
||||
emitDartPackageDependencies(graph, [consumer('a.dart', ['package:app/a.dart'])], {
|
||||
packages: new Map(),
|
||||
manifestsByName: new Map([['app', ['pubspec.yaml']]]),
|
||||
}),
|
||||
).toThrow('Dart package manifest is missing from the indexed file set: pubspec.yaml');
|
||||
expect(graph.relationships).toEqual([]);
|
||||
});
|
||||
|
||||
it('emits only matching dependencies in a many-package workspace', () => {
|
||||
const paths = Array.from({ length: 200 }, (_, i) => `p${i}/pubspec.yaml`);
|
||||
const graph = graphWithFiles([...paths, 'main.dart']);
|
||||
emitDartPackageDependencies(graph, [consumer('main.dart', ['package:p199/a.dart'])], {
|
||||
packages: new Map(),
|
||||
manifestsByName: new Map(paths.map((filePath, i) => [`p${i}`, [filePath]])),
|
||||
});
|
||||
expect(graph.relationships.map((edge) => edge.targetId)).toEqual(['File:p199/pubspec.yaml']);
|
||||
});
|
||||
});
|
||||
341
gitnexus/test/unit/dart-package-imports.test.ts
Normal file
341
gitnexus/test/unit/dart-package-imports.test.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { dartScopeResolver } from '../../src/core/ingestion/languages/dart/scope-resolver.js';
|
||||
import { loadDartPackageConfig } from '../../src/core/ingestion/languages/dart/package-config.js';
|
||||
import { CountingSet } from '../helpers/counting-file-set.js';
|
||||
import { _captureLogger } from '../../src/core/logger.js';
|
||||
|
||||
const files = new Set(['lib/main.dart', 'lib/http.dart', 'lib/models.dart', 'tool/run.dart']);
|
||||
const config = { packages: new Map([['app', 'lib']]) };
|
||||
|
||||
describe('Dart package identity (#2963)', () => {
|
||||
it('does not resolve a pub dependency to a same-named local file', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:http/http.dart',
|
||||
'lib/main.dart',
|
||||
files,
|
||||
config,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('resolves this package through its declared lib directory', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:app/models.dart',
|
||||
'lib/main.dart',
|
||||
files,
|
||||
config,
|
||||
),
|
||||
).toBe('lib/models.dart');
|
||||
});
|
||||
|
||||
it('does not guess package identity when no pubspec config is available', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget('package:app/models.dart', 'lib/main.dart', files),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not fall back to non-library files', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:app/tool/run.dart',
|
||||
'lib/main.dart',
|
||||
files,
|
||||
config,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'',
|
||||
'../http.dart',
|
||||
'src/../../http.dart',
|
||||
'/http.dart',
|
||||
'src\\http.dart',
|
||||
'%2e%2e/http.dart',
|
||||
'http.dart?q',
|
||||
'http.dart#part',
|
||||
])('rejects unsupported package paths: %s', (target) => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
`package:app/${target}`,
|
||||
'lib/main.dart',
|
||||
files,
|
||||
config,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('uses exact package roots even with earlier same-suffix files', () => {
|
||||
const workspace = new Set([
|
||||
'decoy/lib/models.dart',
|
||||
'packages/data/lib/models.dart',
|
||||
'lib/models.dart',
|
||||
]);
|
||||
const monorepo = {
|
||||
packages: new Map([
|
||||
['app', 'lib'],
|
||||
['data', 'packages/data/lib'],
|
||||
]),
|
||||
};
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:data/models.dart',
|
||||
'lib/main.dart',
|
||||
workspace,
|
||||
monorepo,
|
||||
),
|
||||
).toBe('packages/data/lib/models.dart');
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:app/models.dart',
|
||||
'packages/data/lib/main.dart',
|
||||
workspace,
|
||||
monorepo,
|
||||
),
|
||||
).toBe('lib/models.dart');
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:missing/models.dart',
|
||||
'lib/main.dart',
|
||||
workspace,
|
||||
monorepo,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not suffix-match a missing file in a known package', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:app/models.dart',
|
||||
'lib/main.dart',
|
||||
new Set(['other/lib/models.dart']),
|
||||
config,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('uses no workspace scans for package hits or misses', () => {
|
||||
const workspace = new CountingSet(files);
|
||||
for (let i = 0; i < 200; i++) {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:app/models.dart',
|
||||
'lib/main.dart',
|
||||
workspace,
|
||||
config,
|
||||
),
|
||||
).toBe('lib/models.dart');
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
`package:external${i}/http.dart`,
|
||||
'lib/main.dart',
|
||||
workspace,
|
||||
config,
|
||||
),
|
||||
).toBeNull();
|
||||
}
|
||||
expect(workspace.scans).toBe(0);
|
||||
});
|
||||
|
||||
it('still ignores SDK imports and resolves relative paths without config', () => {
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget('dart:core', 'lib/main.dart', files, config),
|
||||
).toBeNull();
|
||||
expect(dartScopeResolver.resolveImportTarget('./models.dart', 'lib/main.dart', files)).toBe(
|
||||
'lib/models.dart',
|
||||
);
|
||||
expect(dartScopeResolver.resolveImportTarget('../tool/run.dart', 'lib/main.dart', files)).toBe(
|
||||
'tool/run.dart',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dart pubspec package discovery', () => {
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function fixture(manifests: Record<string, string>): Promise<string> {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'gitnexus-dart-pubspec-'));
|
||||
roots.push(root);
|
||||
for (const [relative, content] of Object.entries(manifests)) {
|
||||
const destination = path.join(root, relative);
|
||||
await mkdir(path.dirname(destination), { recursive: true });
|
||||
await writeFile(destination, content);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
it('loads root and nested package names through the production hook', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: "app" # root package\ndependencies:\n http: ^1.0.0\n',
|
||||
'packages/data/pubspec.yaml': 'name: data\n',
|
||||
'packages/unnamed/pubspec.yaml': 'description: no package name\n',
|
||||
});
|
||||
const loaded = await dartScopeResolver.loadResolutionConfig?.(root);
|
||||
expect(loaded).toMatchObject({
|
||||
packages: new Map([
|
||||
['app', 'lib'],
|
||||
['data', 'packages/data/lib'],
|
||||
]),
|
||||
});
|
||||
expect(
|
||||
dartScopeResolver.resolveImportTarget(
|
||||
'package:data/models.dart',
|
||||
'lib/main.dart',
|
||||
new Set(['packages/data/lib/models.dart']),
|
||||
loaded,
|
||||
),
|
||||
).toBe('packages/data/lib/models.dart');
|
||||
});
|
||||
|
||||
it('suppresses duplicate names even across three packages', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
'a/pubspec.yaml': 'name: repeated',
|
||||
'b/pubspec.yaml': 'name: repeated',
|
||||
'c/pubspec.yaml': 'name: repeated',
|
||||
});
|
||||
const loaded = await loadDartPackageConfig(root);
|
||||
expect(loaded.packages).toEqual(config.packages);
|
||||
expect(loaded.manifestsByName.get('repeated')).toEqual(['a/pubspec.yaml', 'b/pubspec.yaml']);
|
||||
});
|
||||
|
||||
it('accepts an underscore-prefixed package name', async () => {
|
||||
const root = await fixture({ 'pubspec.yaml': 'name: _app' });
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(new Map([['_app', 'lib']]));
|
||||
});
|
||||
|
||||
it.each(['.gitignore', '.gitnexusignore'])(
|
||||
'ignores duplicate names in directories excluded by %s',
|
||||
async (ignoreFile) => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
[ignoreFile]: 'backup/\n',
|
||||
'backup/pubspec.yaml': 'name: app',
|
||||
});
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
},
|
||||
);
|
||||
|
||||
it('honors explicitly re-included package directories', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
'.gitnexusignore': '!vendor/\n',
|
||||
'vendor/pubspec.yaml': 'name: local_vendor',
|
||||
});
|
||||
expect(await loadDartPackageConfig(root)).toMatchObject({
|
||||
packages: new Map([
|
||||
['app', 'lib'],
|
||||
['local_vendor', 'vendor/lib'],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['.gitignore', '.gitnexusignore'])(
|
||||
'does not read manifests excluded individually by %s',
|
||||
async (ignoreFile) => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
[ignoreFile]: 'backup/pubspec.yaml\nbroken/pubspec.yaml\n',
|
||||
'backup/pubspec.yaml': 'name: app',
|
||||
'broken/pubspec.yaml': 'name: [invalid',
|
||||
});
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
},
|
||||
);
|
||||
|
||||
it('excludes hidden directories just like the production file scanner', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
'.backup/pubspec.yaml': 'name: app',
|
||||
'.broken/pubspec.yaml': 'name: [invalid',
|
||||
});
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
});
|
||||
|
||||
it('reports invalid YAML without exposing contents or discarding valid packages', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
'nested/pubspec.yaml': 'name: [private-manifest-content',
|
||||
});
|
||||
const capture = _captureLogger();
|
||||
try {
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
expect(capture.records()).toEqual([
|
||||
expect.objectContaining({
|
||||
level: 40,
|
||||
reason: 'invalid-yaml',
|
||||
relativePath: 'nested/pubspec.yaml',
|
||||
msg: 'Dart pubspec discovery could not read a valid package declaration.',
|
||||
}),
|
||||
]);
|
||||
expect(capture.text()).not.toContain('private-manifest-content');
|
||||
expect(capture.text()).not.toContain(root);
|
||||
} finally {
|
||||
capture.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(['name: [bad', 'name: one\nname: two', '!!js/function function() {}'])(
|
||||
'does not interpret invalid YAML as a package declaration: %s',
|
||||
async (manifest) => {
|
||||
const root = await fixture({ 'pubspec.yaml': 'name: app', 'nested/pubspec.yaml': manifest });
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['null', '- name: app', 'name: 42', 'name: ../app', 'description: app'])(
|
||||
'does not infer a name from %s',
|
||||
async (manifest) => {
|
||||
expect(
|
||||
(await loadDartPackageConfig(await fixture({ 'pubspec.yaml': manifest }))).packages.size,
|
||||
).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not read generated or installed pubspecs', async () => {
|
||||
const root = await fixture({
|
||||
'pubspec.yaml': 'name: app',
|
||||
'.dart_tool/pubspec.yaml': 'name: app',
|
||||
'.pub-cache/pubspec.yaml': 'name: app',
|
||||
'node_modules/dependency/pubspec.yaml': 'name: app',
|
||||
});
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
});
|
||||
|
||||
it('does not follow directory links outside the repository', async () => {
|
||||
const outside = await fixture({ 'pubspec.yaml': 'name: app' });
|
||||
const root = await fixture({ 'pubspec.yaml': 'name: app' });
|
||||
await symlink(
|
||||
outside,
|
||||
path.join(root, 'linked'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir',
|
||||
);
|
||||
expect((await loadDartPackageConfig(root)).packages).toEqual(config.packages);
|
||||
});
|
||||
|
||||
it('returns no packages when no pubspec is present', async () => {
|
||||
expect((await loadDartPackageConfig(await fixture({}))).packages.size).toBe(0);
|
||||
});
|
||||
|
||||
it('refuses oversized manifests instead of parsing an unbounded document', async () => {
|
||||
const root = await fixture({ 'pubspec.yaml': `name: app\n#${'x'.repeat(1024 * 1024)}` });
|
||||
await expect(loadDartPackageConfig(root)).rejects.toThrow(
|
||||
'Dart pubspec discovery failed (manifest-size)',
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses an incomplete scan rather than persisting untracked identity changes', async () => {
|
||||
const root = await fixture({});
|
||||
await expect(loadDartPackageConfig(path.join(root, 'missing'))).rejects.toThrow(
|
||||
'Dart pubspec discovery failed (read-directory)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -43,6 +43,12 @@ import {
|
|||
} from '../helpers/embedding-seed.js';
|
||||
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE } from '../../src/core/analysis-features.js';
|
||||
import { SCHEMA_FINGERPRINT } from '../../src/core/lbug/schema.js';
|
||||
import {
|
||||
isLanguageAvailable,
|
||||
loadParser,
|
||||
loadLanguage,
|
||||
} from '../../src/core/tree-sitter/parser-loader.js';
|
||||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||
import {
|
||||
SPRING_AOP_FEATURE,
|
||||
SPRING_BEAN_INVENTORY_FEATURE,
|
||||
|
|
@ -1726,6 +1732,153 @@ describe('runFullAnalysis — incremental orchestration', () => {
|
|||
* 'exact-scan', which the unit-level wiring pin in
|
||||
* run-analyze-fts-repair.test.ts covers platform-independently.
|
||||
*/
|
||||
let dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
|
||||
if (dartAvailable) {
|
||||
try {
|
||||
await loadParser();
|
||||
await loadLanguage(SupportedLanguages.Dart);
|
||||
} catch {
|
||||
dartAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart pubspec-only incremental persistence (#2963)', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'rename',
|
||||
initial: 'name: app',
|
||||
next: 'name: renamed',
|
||||
file: 'pubspec.yaml',
|
||||
nextResolves: false,
|
||||
},
|
||||
{
|
||||
name: 'addition',
|
||||
initial: null,
|
||||
next: 'name: app',
|
||||
file: 'pubspec.yaml',
|
||||
nextResolves: true,
|
||||
},
|
||||
{
|
||||
name: 'duplicate',
|
||||
initial: 'name: app',
|
||||
next: 'name: app',
|
||||
file: 'nested/pubspec.yaml',
|
||||
nextResolves: false,
|
||||
},
|
||||
{
|
||||
name: 'repair',
|
||||
initial: 'name: [invalid',
|
||||
next: 'name: app',
|
||||
file: 'pubspec.yaml',
|
||||
nextResolves: true,
|
||||
},
|
||||
])(
|
||||
'persists $name and its reversal with forced-rebuild parity',
|
||||
async ({ initial, next, file, nextResolves }) => {
|
||||
const repo = await createTempDir();
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const options = { skipAgentsMd: true, skipFts: true };
|
||||
const progress = { onProgress: () => {} };
|
||||
const readEdges = async () => {
|
||||
await adapter.initLbug(getStoragePaths(repo.dbPath).lbugPath, { skipFts: true });
|
||||
try {
|
||||
return await adapter.executeQuery(
|
||||
`MATCH (a)-[r:CodeRelation]->(b) WHERE r.type IN ['IMPORTS', 'CALLS'] ` +
|
||||
`RETURN a.id AS source, b.id AS target, r.type AS type, r.reason AS reason, ` +
|
||||
`a.filePath AS sourceFile, b.filePath AS targetFile, b.name AS targetName ` +
|
||||
`ORDER BY source, target, type, reason`,
|
||||
);
|
||||
} finally {
|
||||
await adapter.closeLbug();
|
||||
}
|
||||
};
|
||||
try {
|
||||
await mkdir(path.join(repo.dbPath, 'lib'), { recursive: true });
|
||||
await mkdir(path.join(repo.dbPath, 'nested'), { recursive: true });
|
||||
await writeFile(path.join(repo.dbPath, '.gitignore'), '.gitnexus/\n');
|
||||
await writeFile(
|
||||
path.join(repo.dbPath, 'lib/model.dart'),
|
||||
'class Model { void save() {} }\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repo.dbPath, 'lib/bridge.dart'),
|
||||
"import 'package:app/model.dart';\nModel buildModel() => Model();\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repo.dbPath, 'lib/main.dart'),
|
||||
"import './bridge.dart';\nvoid run() { buildModel().save(); }\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repo.dbPath, 'nested/decoy.dart'),
|
||||
'class Model { void save() {} }\n',
|
||||
);
|
||||
if (initial !== null) await writeFile(path.join(repo.dbPath, 'pubspec.yaml'), initial);
|
||||
execSync('git init -q', { cwd: repo.dbPath, stdio: 'pipe' });
|
||||
gitCommitAll(repo.dbPath, 'Dart package fixture');
|
||||
await runFullAnalysis(repo.dbPath, options, progress);
|
||||
const initialEdges = await readEdges();
|
||||
expect(
|
||||
initialEdges.filter(
|
||||
(edge) =>
|
||||
edge.type === 'IMPORTS' &&
|
||||
edge.source === 'File:lib/bridge.dart' &&
|
||||
edge.target === 'File:lib/model.dart',
|
||||
),
|
||||
).toHaveLength(initial === 'name: app' ? 1 : 0);
|
||||
|
||||
for (const [content, resolves] of [
|
||||
[next, nextResolves],
|
||||
[file === 'pubspec.yaml' ? initial : null, initial === 'name: app'],
|
||||
] as const) {
|
||||
const target = path.join(repo.dbPath, file);
|
||||
if (content === null) await rm(target);
|
||||
else await writeFile(target, content);
|
||||
gitCommitAll(repo.dbPath, 'Update package identity');
|
||||
const incremental = await runFullAnalysis(repo.dbPath, options, progress);
|
||||
expect(incremental.incrementalStats?.writeMode).toBe('incremental');
|
||||
const incrementalEdges = await readEdges();
|
||||
const imports = incrementalEdges.filter(
|
||||
(edge) =>
|
||||
edge.type === 'IMPORTS' &&
|
||||
edge.source === 'File:lib/bridge.dart' &&
|
||||
edge.target === 'File:lib/model.dart',
|
||||
);
|
||||
expect(imports).toHaveLength(resolves ? 1 : 0);
|
||||
expect(
|
||||
incrementalEdges.filter(
|
||||
(edge) =>
|
||||
edge.type === 'CALLS' &&
|
||||
edge.sourceFile === 'lib/main.dart' &&
|
||||
edge.targetFile === 'lib/model.dart' &&
|
||||
edge.targetName === 'save',
|
||||
),
|
||||
).toHaveLength(resolves ? 1 : 0);
|
||||
const meta = await loadMeta(getStoragePaths(repo.dbPath).storagePath);
|
||||
expect(Object.hasOwn(meta?.fileHashes ?? {}, file)).toBe(content !== null);
|
||||
if (content === 'name: app') {
|
||||
expect(incrementalEdges).toContainEqual({
|
||||
source: 'File:lib/main.dart',
|
||||
target: `File:${file}`,
|
||||
type: 'IMPORTS',
|
||||
reason: 'dart-scope: package identity dependency',
|
||||
sourceFile: 'lib/main.dart',
|
||||
targetFile: file,
|
||||
targetName: 'pubspec.yaml',
|
||||
});
|
||||
}
|
||||
await runFullAnalysis(repo.dbPath, { ...options, force: true }, progress);
|
||||
expect(await readEdges()).toEqual(incrementalEdges);
|
||||
}
|
||||
} finally {
|
||||
await adapter.closeLbug();
|
||||
await repo.cleanup();
|
||||
}
|
||||
},
|
||||
300_000,
|
||||
);
|
||||
});
|
||||
|
||||
describe('runFullAnalysis — escalated wipe recreates the vector index (#2409, tri-review 4669518496 P1)', () => {
|
||||
let vectorAvailable = false;
|
||||
let skipWarned = false;
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|||
{
|
||||
files: ['lib/http.dart', 'lib/models.dart', 'lib/main.dart'],
|
||||
fromFile: 'lib/main.dart',
|
||||
resolutionConfig: undefined,
|
||||
resolutionConfig: { packages: new Map([['app', 'lib']]) },
|
||||
external: 'package:http/http.dart',
|
||||
decoy: 'lib/http.dart',
|
||||
reachesDecoy: 'package:app/http.dart',
|
||||
|
|
@ -416,7 +416,6 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|||
* TypeScript one, then deleting its line here.
|
||||
*/
|
||||
const KNOWN_GAPS: ReadonlyMap<SupportedLanguages, string> = new Map<SupportedLanguages, string>([
|
||||
[SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'],
|
||||
[SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'],
|
||||
[SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
* - C#'s `resolveDirectMatch` lets a whole-path match win over a suffix match
|
||||
* found EARLIER in iteration order, while `resolveByProgressiveStripping`
|
||||
* takes whichever comes first;
|
||||
* - Dart tries `lib/<rel>` fully before bare `<rel>`, and compares raw paths
|
||||
* (no backslash normalization) on both legs.
|
||||
* - Dart's relative suffix fallback compares raw workspace paths. Package
|
||||
* identity changed in #2963 and is covered by separate boundary tests.
|
||||
*
|
||||
* So this file keeps copies of the pre-change implementations and asserts the
|
||||
* new ones agree with them on a deterministic corpus built to force exactly
|
||||
|
|
@ -396,10 +396,12 @@ const GO_TARGETS = [
|
|||
const DART_TARGETS = [
|
||||
'',
|
||||
'dart:core',
|
||||
'package:app/models.dart',
|
||||
'package:app/src/models.dart',
|
||||
'package:app',
|
||||
'package:other/lib/util.dart',
|
||||
// Package identity intentionally changed in #2963; legacy parity applies
|
||||
// only to relative imports. Package boundaries have dedicated regressions.
|
||||
'../../lib/models.dart',
|
||||
'../../lib/src/models.dart',
|
||||
'../../models.dart',
|
||||
'../util.dart',
|
||||
'models.dart',
|
||||
'./models.dart',
|
||||
'../models.dart',
|
||||
|
|
@ -621,32 +623,28 @@ describe('import-target index hoist — output parity with the pre-change scans'
|
|||
},
|
||||
{
|
||||
lang: 'dart',
|
||||
why: '`lib/<rel>` beats bare `<rel>` even when the bare hit comes FIRST in Set order',
|
||||
why: 'relative suffix lookup keeps the importer directory prefix',
|
||||
files: ['a/models.dart', 'z/lib/models.dart'],
|
||||
target: 'package:app/models.dart',
|
||||
target: 'models.dart',
|
||||
},
|
||||
{
|
||||
lang: 'dart',
|
||||
why: 'bare `<rel>` is reached only after `lib/<rel>` misses entirely',
|
||||
why: 'relative root lookup reaches a nested suffix',
|
||||
files: ['a/models.dart'],
|
||||
target: 'package:app/models.dart',
|
||||
target: 'models.dart',
|
||||
fromFile: 'main.dart',
|
||||
},
|
||||
{
|
||||
lang: 'dart',
|
||||
why: 'paths are matched RAW — a backslash path is not normalized into a hit',
|
||||
files: ['win\\dir\\thing.dart'],
|
||||
target: 'package:app/dir/thing.dart',
|
||||
target: 'dir/thing.dart',
|
||||
},
|
||||
{
|
||||
// The negative case above pins the guard NEXT DOOR to the one it names:
|
||||
// the basename bucket lookup misses before the raw comparison is ever
|
||||
// consulted, so normalizing only the bucket key, or only the comparison,
|
||||
// still yields null and still matches. This positive twin puts the
|
||||
// backslashes in the TARGET so a hit depends on both halves staying raw.
|
||||
lang: 'dart',
|
||||
why: 'a backslash TARGET matches only because neither the bucket key nor the comparison normalizes',
|
||||
files: ['dir\\thing.dart'],
|
||||
target: 'package:app/dir\\thing.dart',
|
||||
why: 'relative targets normalize backslashes before exact membership lookup',
|
||||
files: ['lib/dir/thing.dart'],
|
||||
target: 'dir\\thing.dart',
|
||||
},
|
||||
{
|
||||
lang: 'dart',
|
||||
|
|
@ -745,7 +743,7 @@ describe('import-target index hoist — output parity with the pre-change scans'
|
|||
if (csharp(t, cs) !== null) hits.csharp++;
|
||||
}
|
||||
}
|
||||
// Measured on this corpus: go 366, dart 75, ruby 259, csharp 220. Ruby and
|
||||
// Historical corpus counts: go 366, dart 75, ruby 259, csharp 220. Ruby and
|
||||
// C# gained 40 each from the `win\dir\thing.<ext>` targets — one per repo,
|
||||
// which is also the floor those two arms now defend. #2881 moved go 364 ->
|
||||
// 366 and csharp 196 -> 220, from the corpus's `pkg/pkg`, `a/pkg/b/pkg` and
|
||||
|
|
@ -804,7 +802,7 @@ describe('import-target index hoist — built once per file set, not once per im
|
|||
it('dart builds one index for many imports (#2879)', () => {
|
||||
const files = countingCorpus(2, '.dart');
|
||||
for (let i = 0; i < 200; i++) {
|
||||
resolveDartImportTarget(`package:pkg${i}/ghost${i}.dart`, 'lib/main.dart', files);
|
||||
resolveDartImportTarget(`pkg${i}/ghost${i}.dart`, 'lib/main.dart', files);
|
||||
}
|
||||
expect(files.scans).toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -519,10 +519,9 @@ const FIXTURES: ReadonlyMap<SupportedLanguages, ImportTargetFixture> = new Map<
|
|||
files: ['lib/models.dart', 'tool/generate.dart', 'lib/main.dart'],
|
||||
fromFile: 'lib/main.dart',
|
||||
resolutionConfig: undefined,
|
||||
// An external package: both `lib/<rel>` and bare `<rel>` miss, which is
|
||||
// the two-scan case.
|
||||
missTarget: (i) => `package:vendor${i}/ghost.dart`,
|
||||
hitTarget: 'package:app/models.dart',
|
||||
// Package imports are exact lookups; relative misses exercise the index.
|
||||
missTarget: (i) => `vendor${i}/ghost.dart`,
|
||||
hitTarget: './models.dart',
|
||||
parsedImport: IGNORES_CONTEXT,
|
||||
minimumScans: 1,
|
||||
minimumParsedFileReads: 0,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,10 @@ describe('RETAINED_REL_TYPES tracks its readers', () => {
|
|||
// CALLS is read by taintSummaries, which is exactly why the sink answers a
|
||||
// COMPLETE read instead of retaining it — so it is a known exemption.
|
||||
readTypes.delete('CALLS');
|
||||
// Dart package invalidation reads IMPORTS endpoints through the sink's
|
||||
// complete typed iterator. dart-package-dependencies.test.ts exercises
|
||||
// transitive closure and idempotence with actual streamed IMPORTS rows.
|
||||
readTypes.delete('IMPORTS');
|
||||
|
||||
const missing = [...readTypes].filter((t) => !RETAINED_REL_TYPES.has(t as RelationshipType));
|
||||
expect(missing).toEqual([]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue