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.
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +4 -3
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +2 -2
- package/dist/core/ingestion/languages/dart/captures.js +17 -0
- package/dist/core/ingestion/languages/dart/import-target.d.ts +4 -5
- package/dist/core/ingestion/languages/dart/import-target.js +19 -17
- package/dist/core/ingestion/languages/dart/index.d.ts +4 -1
- package/dist/core/ingestion/languages/dart/index.js +4 -1
- package/dist/core/ingestion/languages/dart/package-config.d.ts +42 -0
- package/dist/core/ingestion/languages/dart/package-config.js +398 -0
- package/dist/core/ingestion/languages/dart/package-dependencies.d.ts +6 -0
- package/dist/core/ingestion/languages/dart/package-dependencies.js +86 -0
- package/dist/core/ingestion/languages/dart/package-uri.d.ts +7 -0
- package/dist/core/ingestion/languages/dart/package-uri.js +14 -0
- package/dist/core/ingestion/languages/dart/scope-resolver.js +7 -1
- package/dist/core/ingestion/languages/kotlin/captures.js +37 -0
- package/dist/core/ingestion/languages/python/captures.js +13 -0
- package/dist/core/ingestion/languages/ruby/captures.js +35 -0
- package/dist/core/ingestion/languages/swift/captures.js +11 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +6 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +6 -0
- package/dist/core/ingestion/utils/callable-flow-captures.d.ts +24 -0
- package/dist/core/ingestion/utils/callable-flow-captures.js +184 -20
- package/dist/mcp/local/local-backend.js +30 -7
- package/dist/storage/parse-cache.js +8 -1
- 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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
50
|
-
*
|
|
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
|
-
*
|
|
3
|
+
* Dart import logic:
|
|
4
4
|
*
|
|
5
5
|
* - `dart:` SDK imports → `null` (external, no edge)
|
|
6
|
-
* - `package:pkg/path` →
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
3
|
+
* Dart import logic:
|
|
4
4
|
*
|
|
5
5
|
* - `dart:` SDK imports → `null` (external, no edge)
|
|
6
|
-
* - `package:pkg/path` →
|
|
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
|
-
*
|
|
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
|
-
//
|
|
73
|
-
if (targetRaw.startsWith(
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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` —
|
|
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` —
|
|
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;
|