gitnexus 1.6.13-rc.37 → 1.6.13-rc.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +4 -3
  2. package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +2 -2
  3. package/dist/core/ingestion/languages/dart/captures.js +17 -0
  4. package/dist/core/ingestion/languages/dart/import-target.d.ts +4 -5
  5. package/dist/core/ingestion/languages/dart/import-target.js +19 -17
  6. package/dist/core/ingestion/languages/dart/index.d.ts +4 -1
  7. package/dist/core/ingestion/languages/dart/index.js +4 -1
  8. package/dist/core/ingestion/languages/dart/package-config.d.ts +42 -0
  9. package/dist/core/ingestion/languages/dart/package-config.js +398 -0
  10. package/dist/core/ingestion/languages/dart/package-dependencies.d.ts +6 -0
  11. package/dist/core/ingestion/languages/dart/package-dependencies.js +86 -0
  12. package/dist/core/ingestion/languages/dart/package-uri.d.ts +7 -0
  13. package/dist/core/ingestion/languages/dart/package-uri.js +14 -0
  14. package/dist/core/ingestion/languages/dart/scope-resolver.js +7 -1
  15. package/dist/core/ingestion/languages/kotlin/captures.js +37 -0
  16. package/dist/core/ingestion/languages/python/captures.js +13 -0
  17. package/dist/core/ingestion/languages/ruby/captures.js +35 -0
  18. package/dist/core/ingestion/languages/swift/captures.js +11 -0
  19. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +6 -0
  20. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +6 -0
  21. package/dist/core/ingestion/utils/callable-flow-captures.d.ts +24 -0
  22. package/dist/core/ingestion/utils/callable-flow-captures.js +184 -20
  23. package/dist/mcp/local/local-backend.js +30 -7
  24. package/dist/storage/parse-cache.js +8 -1
  25. package/package.json +1 -1
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Kotlin's tree-sitter grammar (vendored, NOT an npm package — loaded via
5
5
  * `requireVendoredGrammar('tree-sitter-kotlin')`, exactly like tree-sitter-swift)
6
- * is field-less for control flow: NONE of the control-flow nodes expose
7
- * `childForFieldName` fields (verified by a real parse — every `fieldNameForChild`
8
- * came back null), so this visitor navigates purely by child TYPE and position.
6
+ * is mostly field-less for control flow (`when` / loops / `try` / `elvis_expression`
7
+ * expose no fields), so this visitor navigates purely by child TYPE and position.
8
+ * `if_expression` is the exception: it DOES field `condition` / `consequence` /
9
+ * `alternative` (#3354 — callable-flow capture reads them); position still works.
9
10
  * Every node-type literal below was grammar-validated against the vendored
10
11
  * tree-sitter-kotlin via the introspection probe before use (mandatory pre-step —
11
12
  * the grammar-literal CI gate maps `kotlin.ts → Kotlin` and fails on a wrong
@@ -46,8 +46,8 @@
46
46
  * - reads: `attribute` (fields `object`/`attribute`), `subscript` (fields
47
47
  * `value`/`subscript`), `call` (fields `function`/`arguments`),
48
48
  * `boolean_operator` (fields `left`/`operator`/`right`),
49
- * `conditional_expression` (ternary: consequent / condition / alternative in
50
- * source order), `parenthesized_expression`.
49
+ * `conditional_expression` (ternary, FIELDLESS: consequent, condition,
50
+ * alternative as positional children), `parenthesized_expression`.
51
51
  *
52
52
  * TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the TS / Go harvesters):
53
53
  * the CFG walk is NOT source-order, so resolving names against a scope stack
@@ -109,6 +109,23 @@ const DART_CALLABLE_CAPTURE_OPTIONS = {
109
109
  extractCallCallee: (node) => dartCallableCallee(node) ?? undefined,
110
110
  callSiteNode: (node) => dartCallableCallee(node) ?? undefined,
111
111
  callableProtocolMethods: new Set(['call']),
112
+ // tree-sitter-dart spells `a ?? b` as `if_null_expression(first:, second:)`
113
+ // and `c ? a : b` as a `conditional_expression` with `consequence` /
114
+ // `alternative` but NO `condition` field, so neither matches the shared
115
+ // field-based branch rule and only the last operand flowed (#3354).
116
+ valueAlternatives: (node) => {
117
+ if (node.type === 'if_null_expression') {
118
+ const first = node.childForFieldName('first');
119
+ const second = node.childForFieldName('second');
120
+ return first !== null && second !== null ? [first, second] : undefined;
121
+ }
122
+ if (node.type === 'conditional_expression') {
123
+ const consequence = node.childForFieldName('consequence');
124
+ const alternative = node.childForFieldName('alternative');
125
+ return consequence !== null && alternative !== null ? [consequence, alternative] : undefined;
126
+ }
127
+ return undefined;
128
+ },
112
129
  };
113
130
  function dartLexicalFunctionOwner(input) {
114
131
  let node = input;
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * `resolveImportTarget` adapter for the Dart `ScopeResolver`. Ports the
3
- * legacy-DAG Dart import logic (`import-resolvers/configs/dart.ts`):
3
+ * Dart import logic:
4
4
  *
5
5
  * - `dart:` SDK imports → `null` (external, no edge)
6
- * - `package:pkg/path` → `lib/path` (or bare `path`) matched
7
- * against the workspace file set
6
+ * - `package:pkg/path` → declared package's exact `lib/path`
8
7
  * - relative `'foo/bar.dart'` → resolved against the importer's dir
9
8
  * - `__heritage__:` markers → `null` (synthetic heritage carrier,
10
9
  * consumed by `emitDartHeritageEdges`)
11
10
  *
12
- * The `ScopeResolver` hook signature is `(targetRaw, fromFile, allFilePaths)`;
11
+ * Package identity comes from the workspace's pubspec resolution config;
13
12
  * `targetRaw` arrives already quote-stripped from `interpretDartImport`.
14
13
  */
15
- export declare function resolveDartImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>): string | readonly string[] | null;
14
+ export declare function resolveDartImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, resolutionConfig?: unknown): string | readonly string[] | null;
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * `resolveImportTarget` adapter for the Dart `ScopeResolver`. Ports the
3
- * legacy-DAG Dart import logic (`import-resolvers/configs/dart.ts`):
3
+ * Dart import logic:
4
4
  *
5
5
  * - `dart:` SDK imports → `null` (external, no edge)
6
- * - `package:pkg/path` → `lib/path` (or bare `path`) matched
7
- * against the workspace file set
6
+ * - `package:pkg/path` → declared package's exact `lib/path`
8
7
  * - relative `'foo/bar.dart'` → resolved against the importer's dir
9
8
  * - `__heritage__:` markers → `null` (synthetic heritage carrier,
10
9
  * consumed by `emitDartHeritageEdges`)
11
10
  *
12
- * The `ScopeResolver` hook signature is `(targetRaw, fromFile, allFilePaths)`;
11
+ * Package identity comes from the workspace's pubspec resolution config;
13
12
  * `targetRaw` arrives already quote-stripped from `interpretDartImport`.
14
13
  */
15
14
  import { perFileSet } from '../../import-resolvers/per-file-set.js';
16
15
  import { DART_HERITAGE_PREFIX } from './interpret.js';
16
+ import { DART_PACKAGE_SCHEME, dartPackageImportName } from './package-uri.js';
17
17
  const getDartFileIndex = perFileSet((allFilePaths) => {
18
18
  const byBasename = new Map();
19
19
  for (const fp of allFilePaths) {
@@ -60,7 +60,7 @@ function resolveRelative(rel, fromFile, allFilePaths) {
60
60
  // Suffix fallback for absolute/rooted workspace paths.
61
61
  return findByPathSuffix(allFilePaths, target);
62
62
  }
63
- export function resolveDartImportTarget(targetRaw, fromFile, allFilePaths) {
63
+ export function resolveDartImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig) {
64
64
  if (targetRaw.startsWith(DART_HERITAGE_PREFIX))
65
65
  return null;
66
66
  // `targetRaw` already arrives quote-stripped from `interpretDartImport`.
@@ -69,19 +69,21 @@ export function resolveDartImportTarget(targetRaw, fromFile, allFilePaths) {
69
69
  // Dart SDK imports never resolve to a repo file.
70
70
  if (targetRaw.startsWith('dart:'))
71
71
  return null;
72
- // `package:pkg/path.dart` → `lib/path.dart` (or bare `path.dart`).
73
- if (targetRaw.startsWith('package:')) {
74
- const slash = targetRaw.indexOf('/');
75
- if (slash === -1)
72
+ // A package URI never falls back to another package's same-named file.
73
+ if (targetRaw.startsWith(DART_PACKAGE_SCHEME)) {
74
+ const packageName = dartPackageImportName(targetRaw);
75
+ if (packageName === null)
76
76
  return null;
77
- const relPath = targetRaw.slice(slash + 1);
78
- // Candidate priority is load-bearing: `lib/<rel>` before bare `<rel>`.
79
- for (const candidate of [`lib/${relPath}`, relPath]) {
80
- const hit = findByPathSuffix(allFilePaths, candidate);
81
- if (hit !== null)
82
- return hit;
83
- }
84
- return null; // external package
77
+ const config = resolutionConfig;
78
+ const lib = config?.packages?.get(packageName);
79
+ if (lib === undefined)
80
+ return null;
81
+ const relPath = targetRaw.slice(DART_PACKAGE_SCHEME.length + packageName.length + 1);
82
+ if (/[\\%?#:]/.test(relPath) ||
83
+ relPath.split('/').some((part) => part === '' || part === '.' || part === '..'))
84
+ return null;
85
+ const candidate = `${lib}/${relPath}`;
86
+ return allFilePaths.has(candidate) ? candidate : null;
85
87
  }
86
88
  // Relative import.
87
89
  return resolveRelative(targetRaw, fromFile, allFilePaths);
@@ -9,7 +9,10 @@
9
9
  * - `query.ts` — tree-sitter scope query + lazy parser/query
10
10
  * - `captures.ts` — `emitDartScopeCaptures` orchestrator
11
11
  * - `interpret.ts` — capture-match → `ParsedImport` / `ParsedTypeBinding`
12
- * - `import-target.ts` — `(targetRaw, fromFile, allFilePaths) → file path`
12
+ * - `import-target.ts` — exact declared-package / relative import targets
13
+ * - `package-uri.ts` — shared `package:name/...` parse
14
+ * - `package-config.ts` — bounded, ignore-aware in-repo pubspec discovery
15
+ * - `package-dependencies.ts` — manifest dependencies for incremental writeback
13
16
  * - `receiver-binding.ts` — synthesize `this` / `super` type-bindings
14
17
  * - `signature-bindings.ts`— synthesize parameter / return type-bindings
15
18
  * - `arity.ts` — Dart arity compatibility (count-primary)
@@ -9,7 +9,10 @@
9
9
  * - `query.ts` — tree-sitter scope query + lazy parser/query
10
10
  * - `captures.ts` — `emitDartScopeCaptures` orchestrator
11
11
  * - `interpret.ts` — capture-match → `ParsedImport` / `ParsedTypeBinding`
12
- * - `import-target.ts` — `(targetRaw, fromFile, allFilePaths) → file path`
12
+ * - `import-target.ts` — exact declared-package / relative import targets
13
+ * - `package-uri.ts` — shared `package:name/...` parse
14
+ * - `package-config.ts` — bounded, ignore-aware in-repo pubspec discovery
15
+ * - `package-dependencies.ts` — manifest dependencies for incremental writeback
13
16
  * - `receiver-binding.ts` — synthesize `this` / `super` type-bindings
14
17
  * - `signature-bindings.ts`— synthesize parameter / return type-bindings
15
18
  * - `arity.ts` — Dart arity compatibility (count-primary)
@@ -0,0 +1,42 @@
1
+ import type { Dirent } from 'node:fs';
2
+ export interface DartPackageConfig {
3
+ readonly packages: ReadonlyMap<string, string>;
4
+ readonly manifestsByName: ReadonlyMap<string, readonly string[]>;
5
+ }
6
+ export interface DartPackageConfigOptions {
7
+ /** Test seam. Production calls omit it and use the module directory limit. */
8
+ readonly directoryLimit?: number;
9
+ /** Test seam. Production calls omit it and use the open-directory cap. */
10
+ readonly directoryDepthLimit?: number;
11
+ /** Test seam. Production calls omit it and use the per-directory entry cap. */
12
+ readonly directoryEntryLimit?: number;
13
+ /**
14
+ * Test seam. Production calls omit it. Invoked after the directory inode
15
+ * is listed and before its entries are opened.
16
+ */
17
+ readonly beforeEntryOpen?: (relativePath: string) => void | Promise<void>;
18
+ }
19
+ /** Linux anchors child opens. macOS verifies them. Every other platform does neither. */
20
+ export declare function pubspecWalkAnchored(): boolean;
21
+ export declare function directoryOpenFlags(): number;
22
+ /**
23
+ * Path that lists the directory inode already open on `fd`.
24
+ * Linux uses `/proc/self/fd/N` and macOS uses `/dev/fd/N`.
25
+ * Other platforms have no such path; callers refuse instead of listing by name.
26
+ */
27
+ export declare function descriptorDirectoryPath(fd: number): string | null;
28
+ /**
29
+ * One entry of the directory inode open on `fd`.
30
+ * Linux looks up `/proc/self/fd/N/<name>` in that inode. macOS `/dev/fd/N/<name>`
31
+ * does not resolve a child, so this returns null and the walker verifies the
32
+ * pinned parent chain instead. Every other platform returns null and is not walked.
33
+ */
34
+ export declare function descriptorEntryPath(fd: number, name: string): string | null;
35
+ /**
36
+ * Open `directory` without following a final symlink, then list that inode.
37
+ * A path swapped for a symlink after the parent listing fails this open
38
+ * (`ENOTDIR` / `ELOOP`) instead of being traversed.
39
+ */
40
+ export declare function readDirectoryNoFollow(directory: string): Promise<Dirent[]>;
41
+ /** Discover only in-repository packages; never follow dependency paths or symlinks. */
42
+ export declare function loadDartPackageConfig(repoPath: string, options?: DartPackageConfigOptions): Promise<DartPackageConfig>;
@@ -0,0 +1,398 @@
1
+ import { constants, lstat, open, opendir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { JSON_SCHEMA, load } from 'js-yaml';
4
+ import { createWatchIgnorePredicate } from '../../../../config/ignore-service.js';
5
+ import { logger } from '../../../logger.js';
6
+ import { getMaxFileSizeBytes } from '../../utils/max-file-size.js';
7
+ /** An incomplete walk cannot prove package names are unique. */
8
+ const DART_PUBSPEC_DIRECTORY_LIMIT = 20_000;
9
+ /**
10
+ * Directory descriptors one walk may hold at once. The visit budget is far
11
+ * above a process file-descriptor limit, so a deep chain is refused before
12
+ * the next open instead of failing later with EMFILE.
13
+ */
14
+ const DART_PUBSPEC_OPEN_DIRECTORY_LIMIT = 64;
15
+ /**
16
+ * Names read from one directory. `readdir` would retain every entry before the
17
+ * visit budget can run, so the walk counts names as it reads and stops there.
18
+ */
19
+ const DART_PUBSPEC_DIRECTORY_ENTRY_LIMIT = 100_000;
20
+ /**
21
+ * Read at most `maxManifestSize` bytes from a descriptor already opened
22
+ * with `O_NOFOLLOW`. A size check after the read rejects a file that grew
23
+ * past the captured bytes, including past the cap. The caller closes nothing;
24
+ * this function owns `handle`.
25
+ */
26
+ async function readManifestBounded(handle, maxManifestSize) {
27
+ try {
28
+ const info = await handle.stat();
29
+ if (!info.isFile())
30
+ return { ok: false, reason: 'read-pubspec' };
31
+ if (info.size > maxManifestSize)
32
+ return { ok: false, reason: 'manifest-size' };
33
+ const toRead = Math.min(maxManifestSize + 1, info.size + 1);
34
+ const buffer = Buffer.allocUnsafe(toRead);
35
+ let bytesRead = 0;
36
+ while (bytesRead < toRead) {
37
+ const chunk = await handle.read(buffer, bytesRead, toRead - bytesRead, bytesRead);
38
+ if (chunk.bytesRead === 0)
39
+ break;
40
+ bytesRead += chunk.bytesRead;
41
+ }
42
+ if (bytesRead > maxManifestSize)
43
+ return { ok: false, reason: 'manifest-size' };
44
+ const after = await handle.stat();
45
+ if (!after.isFile())
46
+ return { ok: false, reason: 'read-pubspec' };
47
+ if (after.size > maxManifestSize)
48
+ return { ok: false, reason: 'manifest-size' };
49
+ if (after.size !== bytesRead)
50
+ return { ok: false, reason: 'read-pubspec' };
51
+ return { ok: true, content: buffer.subarray(0, bytesRead).toString('utf8') };
52
+ }
53
+ catch {
54
+ return { ok: false, reason: 'read-pubspec' };
55
+ }
56
+ finally {
57
+ await handle.close();
58
+ }
59
+ }
60
+ function requireNoFollowFlag() {
61
+ const noFollow = constants.O_NOFOLLOW;
62
+ if (typeof noFollow !== 'number' || noFollow === 0) {
63
+ throw Object.assign(new Error('O_NOFOLLOW is unavailable'), { code: 'ENOTSUP' });
64
+ }
65
+ return noFollow;
66
+ }
67
+ /** Linux anchors child opens. macOS verifies them. Every other platform does neither. */
68
+ export function pubspecWalkAnchored() {
69
+ const noFollow = constants.O_NOFOLLOW;
70
+ return ((process.platform === 'linux' || process.platform === 'darwin') &&
71
+ typeof noFollow === 'number' &&
72
+ noFollow !== 0);
73
+ }
74
+ export function directoryOpenFlags() {
75
+ let flags = constants.O_RDONLY | requireNoFollowFlag();
76
+ if (typeof constants.O_DIRECTORY === 'number')
77
+ flags |= constants.O_DIRECTORY;
78
+ return flags;
79
+ }
80
+ /**
81
+ * Read-only, no-follow, and non-blocking. `O_NONBLOCK` does not change a
82
+ * regular-file read. Without it, a FIFO blocks inside `open` until a writer
83
+ * connects, so the later file-type check never runs.
84
+ */
85
+ function fileOpenFlags() {
86
+ const nonBlock = constants.O_NONBLOCK;
87
+ if (typeof nonBlock !== 'number' || nonBlock === 0) {
88
+ throw Object.assign(new Error('O_NONBLOCK is unavailable'), { code: 'ENOTSUP' });
89
+ }
90
+ return constants.O_RDONLY | requireNoFollowFlag() | nonBlock;
91
+ }
92
+ function directoryIdentity(stat) {
93
+ return `${stat.dev}:${stat.ino}:${stat.mode}`;
94
+ }
95
+ /**
96
+ * Path that lists the directory inode already open on `fd`.
97
+ * Linux uses `/proc/self/fd/N` and macOS uses `/dev/fd/N`.
98
+ * Other platforms have no such path; callers refuse instead of listing by name.
99
+ */
100
+ export function descriptorDirectoryPath(fd) {
101
+ if (process.platform === 'linux')
102
+ return `/proc/self/fd/${fd}`;
103
+ if (process.platform === 'darwin')
104
+ return `/dev/fd/${fd}`;
105
+ return null;
106
+ }
107
+ /**
108
+ * One entry of the directory inode open on `fd`.
109
+ * Linux looks up `/proc/self/fd/N/<name>` in that inode. macOS `/dev/fd/N/<name>`
110
+ * does not resolve a child, so this returns null and the walker verifies the
111
+ * pinned parent chain instead. Every other platform returns null and is not walked.
112
+ */
113
+ export function descriptorEntryPath(fd, name) {
114
+ if (process.platform !== 'linux')
115
+ return null;
116
+ if (!isSingleDirectoryEntry(name))
117
+ return null;
118
+ return `/proc/self/fd/${fd}/${name}`;
119
+ }
120
+ function isSingleDirectoryEntry(name) {
121
+ return (name !== '' && name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\0'));
122
+ }
123
+ async function openVerifiedDirectory(directory) {
124
+ const handle = await open(directory, directoryOpenFlags());
125
+ try {
126
+ const info = await handle.stat({ bigint: true });
127
+ if (!info.isDirectory()) {
128
+ throw Object.assign(new Error('not a directory'), { code: 'ENOTDIR' });
129
+ }
130
+ return { handle, identity: directoryIdentity(info) };
131
+ }
132
+ catch (error) {
133
+ await handle.close();
134
+ throw error;
135
+ }
136
+ }
137
+ /** Re-stat each pinned directory and its path. A replaced inode or a symlink fails the walk. */
138
+ async function assertPinnedChain(frames) {
139
+ for (const frame of frames) {
140
+ const pinned = await frame.handle.stat({ bigint: true });
141
+ if (!pinned.isDirectory() || directoryIdentity(pinned) !== frame.identity) {
142
+ throw Object.assign(new Error('parent descriptor changed'), { code: 'ELOOP' });
143
+ }
144
+ let lexical;
145
+ try {
146
+ lexical = await lstat(frame.absolute, { bigint: true });
147
+ }
148
+ catch {
149
+ throw Object.assign(new Error('parent path changed'), { code: 'ELOOP' });
150
+ }
151
+ if (lexical.isSymbolicLink() ||
152
+ !lexical.isDirectory() ||
153
+ directoryIdentity(lexical) !== frame.identity) {
154
+ throw Object.assign(new Error('parent path changed'), { code: 'ELOOP' });
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * macOS has no descriptor-relative child lookup. Re-check the pinned parents,
160
+ * open the child with O_NOFOLLOW, then re-check the parents. The opened
161
+ * descriptor is the only child metadata consulted.
162
+ */
163
+ async function openLexicalDirectory(frames, lexicalPath) {
164
+ await assertPinnedChain(frames);
165
+ const opened = await openVerifiedDirectory(lexicalPath);
166
+ try {
167
+ await assertPinnedChain(frames);
168
+ return opened;
169
+ }
170
+ catch (error) {
171
+ await opened.handle.close();
172
+ throw error;
173
+ }
174
+ }
175
+ async function openLexicalFile(frames, lexicalPath) {
176
+ await assertPinnedChain(frames);
177
+ const handle = await open(lexicalPath, fileOpenFlags());
178
+ try {
179
+ const opened = await handle.stat({ bigint: true });
180
+ if (!opened.isFile()) {
181
+ throw Object.assign(new Error('not a file'), { code: 'ELOOP' });
182
+ }
183
+ await assertPinnedChain(frames);
184
+ return handle;
185
+ }
186
+ catch (error) {
187
+ await handle.close();
188
+ throw error;
189
+ }
190
+ }
191
+ async function openChildDirectory(frames, parentFd, name, lexicalPath) {
192
+ const anchored = descriptorEntryPath(parentFd, name);
193
+ if (anchored !== null)
194
+ return openVerifiedDirectory(anchored);
195
+ if (process.platform === 'darwin')
196
+ return openLexicalDirectory(frames, lexicalPath);
197
+ throw Object.assign(new Error('no descriptor anchor'), { code: 'ENOTSUP' });
198
+ }
199
+ async function openChildFile(frames, parentFd, name, lexicalPath) {
200
+ const anchored = descriptorEntryPath(parentFd, name);
201
+ if (anchored !== null)
202
+ return open(anchored, fileOpenFlags());
203
+ if (process.platform === 'darwin')
204
+ return openLexicalFile(frames, lexicalPath);
205
+ throw Object.assign(new Error('no descriptor anchor'), { code: 'ENOTSUP' });
206
+ }
207
+ async function listOpenedDirectory(handle, entryLimit) {
208
+ const listing = descriptorDirectoryPath(handle.fd);
209
+ if (listing === null) {
210
+ throw Object.assign(new Error('no descriptor listing'), { code: 'ENOTSUP' });
211
+ }
212
+ const dir = await opendir(listing);
213
+ const entries = [];
214
+ try {
215
+ let count = 0;
216
+ while (true) {
217
+ const entry = await dir.read();
218
+ if (entry === null)
219
+ break;
220
+ count += 1;
221
+ if (count > entryLimit) {
222
+ throw Object.assign(new Error('directory entry limit'), { code: 'E2BIG' });
223
+ }
224
+ entries.push(entry);
225
+ }
226
+ }
227
+ finally {
228
+ await dir.close().catch(() => undefined);
229
+ }
230
+ return entries;
231
+ }
232
+ /**
233
+ * Open `directory` without following a final symlink, then list that inode.
234
+ * A path swapped for a symlink after the parent listing fails this open
235
+ * (`ENOTDIR` / `ELOOP`) instead of being traversed.
236
+ */
237
+ export async function readDirectoryNoFollow(directory) {
238
+ const opened = await openVerifiedDirectory(directory);
239
+ try {
240
+ return await listOpenedDirectory(opened.handle, DART_PUBSPEC_DIRECTORY_ENTRY_LIMIT);
241
+ }
242
+ finally {
243
+ await opened.handle.close();
244
+ }
245
+ }
246
+ /** Discover only in-repository packages; never follow dependency paths or symlinks. */
247
+ export async function loadDartPackageConfig(repoPath, options) {
248
+ const warn = (reason, relativePath = '.') => {
249
+ logger.warn({ reason, relativePath }, 'Dart pubspec discovery could not read a valid package declaration.');
250
+ };
251
+ const incomplete = (reason, relativePath = '.') => {
252
+ warn(reason, relativePath);
253
+ throw new Error(`Dart pubspec discovery failed (${reason}): ${relativePath}`);
254
+ };
255
+ if (!pubspecWalkAnchored()) {
256
+ warn('nofollow-anchor');
257
+ return { packages: new Map(), manifestsByName: new Map() };
258
+ }
259
+ let isIgnored;
260
+ try {
261
+ isIgnored = await createWatchIgnorePredicate(repoPath);
262
+ }
263
+ catch {
264
+ return incomplete('ignore-rules');
265
+ }
266
+ const packages = new Map();
267
+ const manifestsByName = new Map();
268
+ const maxManifestSize = Math.min(1024 * 1024, getMaxFileSizeBytes());
269
+ const directoryLimit = options?.directoryLimit ?? DART_PUBSPEC_DIRECTORY_LIMIT;
270
+ const directoryDepthLimit = options?.directoryDepthLimit ?? DART_PUBSPEC_OPEN_DIRECTORY_LIMIT;
271
+ const directoryEntryLimit = options?.directoryEntryLimit ?? DART_PUBSPEC_DIRECTORY_ENTRY_LIMIT;
272
+ const ambiguous = new Set();
273
+ const stack = [];
274
+ let visited = 0;
275
+ const closeStack = async () => {
276
+ const frames = stack.splice(0);
277
+ await Promise.all(frames.map((frame) => frame.handle.close().catch(() => undefined)));
278
+ };
279
+ const fillFrame = async (frame) => {
280
+ if (++visited > directoryLimit)
281
+ return incomplete('directory-limit', frame.relative || '.');
282
+ try {
283
+ frame.entries = await listOpenedDirectory(frame.handle, directoryEntryLimit);
284
+ }
285
+ catch (error) {
286
+ const reason = error.code === 'E2BIG' ? 'directory-entries' : 'read-directory';
287
+ return incomplete(reason, frame.relative || '.');
288
+ }
289
+ if (options?.beforeEntryOpen)
290
+ await options.beforeEntryOpen(frame.relative);
291
+ };
292
+ try {
293
+ let rootOpened;
294
+ try {
295
+ rootOpened = await openVerifiedDirectory(repoPath);
296
+ }
297
+ catch {
298
+ return incomplete('read-directory', '.');
299
+ }
300
+ const root = {
301
+ relative: '',
302
+ absolute: repoPath,
303
+ handle: rootOpened.handle,
304
+ identity: rootOpened.identity,
305
+ entries: [],
306
+ next: 0,
307
+ };
308
+ stack.push(root);
309
+ await fillFrame(root);
310
+ while (stack.length > 0) {
311
+ const frame = stack[stack.length - 1];
312
+ if (frame === undefined)
313
+ break;
314
+ if (frame.next >= frame.entries.length) {
315
+ stack.pop();
316
+ await frame.handle.close().catch(() => undefined);
317
+ continue;
318
+ }
319
+ const entry = frame.entries[frame.next];
320
+ frame.next += 1;
321
+ if (entry === undefined || !isSingleDirectoryEntry(entry.name) || entry.isSymbolicLink()) {
322
+ continue;
323
+ }
324
+ const childRelative = frame.relative ? `${frame.relative}/${entry.name}` : entry.name;
325
+ const entryPath = path.join(repoPath, childRelative);
326
+ if (entry.isDirectory()) {
327
+ if (entry.name.startsWith('.') || isIgnored(entryPath, true))
328
+ continue;
329
+ if (stack.length >= directoryDepthLimit) {
330
+ return incomplete('directory-depth', childRelative);
331
+ }
332
+ let childOpened;
333
+ try {
334
+ childOpened = await openChildDirectory(stack, frame.handle.fd, entry.name, entryPath);
335
+ }
336
+ catch {
337
+ return incomplete('read-directory', childRelative);
338
+ }
339
+ const child = {
340
+ relative: childRelative,
341
+ absolute: entryPath,
342
+ handle: childOpened.handle,
343
+ identity: childOpened.identity,
344
+ entries: [],
345
+ next: 0,
346
+ };
347
+ stack.push(child);
348
+ await fillFrame(child);
349
+ }
350
+ else if (entry.isFile() && entry.name === 'pubspec.yaml' && !isIgnored(entryPath, false)) {
351
+ const manifestPath = frame.relative ? `${frame.relative}/pubspec.yaml` : 'pubspec.yaml';
352
+ let manifestHandle;
353
+ try {
354
+ manifestHandle = await openChildFile(stack, frame.handle.fd, entry.name, entryPath);
355
+ }
356
+ catch {
357
+ return incomplete('read-pubspec', manifestPath);
358
+ }
359
+ const read = await readManifestBounded(manifestHandle, maxManifestSize);
360
+ if (read.ok === false)
361
+ return incomplete(read.reason, manifestPath);
362
+ try {
363
+ const manifest = load(read.content, {
364
+ schema: JSON_SCHEMA,
365
+ });
366
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest))
367
+ continue;
368
+ const name = manifest.name;
369
+ if (typeof name !== 'string' || !/^[a-z_][a-z0-9_]*$/.test(name))
370
+ continue;
371
+ const manifests = manifestsByName.get(name) ?? [];
372
+ manifests.push(manifestPath);
373
+ // Two deterministic witnesses suffice to prove ambiguity. Retaining
374
+ // more would create unbounded duplicate-package dependency fanout.
375
+ manifests.sort();
376
+ if (manifests.length > 2)
377
+ manifests.length = 2;
378
+ manifestsByName.set(name, manifests);
379
+ if (packages.has(name) || ambiguous.has(name)) {
380
+ packages.delete(name);
381
+ ambiguous.add(name);
382
+ }
383
+ else {
384
+ packages.set(name, frame.relative ? `${frame.relative}/lib` : 'lib');
385
+ }
386
+ }
387
+ catch {
388
+ // Invalid YAML cannot declare a package. Other valid packages remain usable.
389
+ warn('invalid-yaml', manifestPath);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ finally {
395
+ await closeStack();
396
+ }
397
+ return { packages, manifestsByName };
398
+ }
@@ -0,0 +1,6 @@
1
+ import type { ParsedFile } from '../../../../_shared/index.js';
2
+ import type { KnowledgeGraph } from '../../../graph/types.js';
3
+ import { DART_PACKAGE_IDENTITY_REASON } from '../../scope-resolution/graph-bridge/imports-to-edges.js';
4
+ export { DART_PACKAGE_IDENTITY_REASON };
5
+ /** Package identity is an input dependency, including unresolved/ambiguous imports. */
6
+ export declare function emitDartPackageDependencies(graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], resolutionConfig?: unknown): void;