@shrkcrft/boundaries 0.1.0-alpha.29 → 0.1.0-alpha.31
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/baseline/compute-baseline.d.ts +8 -0
- package/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +10 -8
- package/dist/baseline/diff-baseline.d.ts +28 -0
- package/dist/baseline/diff-baseline.d.ts.map +1 -1
- package/dist/baseline/diff-baseline.js +29 -0
- package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
- package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-finding.js +21 -0
- package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
- package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-kind.js +14 -0
- package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
- package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
- package/dist/evaluate/evaluate-boundaries.js +481 -53
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-unit-finding.js +1 -0
- package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
- package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
- package/dist/evaluate/settle-boundary-rule.js +73 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
- package/dist/extract/code-zones.d.ts +95 -3
- package/dist/extract/code-zones.d.ts.map +1 -1
- package/dist/extract/code-zones.js +313 -4
- package/dist/extract/extract-tokens.d.ts +49 -1
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +283 -25
- package/dist/extract/i-labeled-source.d.ts +12 -0
- package/dist/extract/i-labeled-source.d.ts.map +1 -0
- package/dist/extract/i-labeled-source.js +1 -0
- package/dist/extract/import-edges.d.ts +50 -0
- package/dist/extract/import-edges.d.ts.map +1 -0
- package/dist/extract/import-edges.js +170 -0
- package/dist/extract/inspect-source.d.ts +38 -3
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +56 -8
- package/dist/extract/parse-imports.d.ts +80 -0
- package/dist/extract/parse-imports.d.ts.map +1 -0
- package/dist/extract/parse-imports.js +254 -0
- package/dist/extract/scan-literals.d.ts +12 -1
- package/dist/extract/scan-literals.d.ts.map +1 -1
- package/dist/extract/scan-literals.js +15 -1
- package/dist/extract/source-liveness-request.d.ts +24 -0
- package/dist/extract/source-liveness-request.d.ts.map +1 -0
- package/dist/extract/source-liveness-request.js +45 -0
- package/dist/generated/check-provenance.d.ts +20 -2
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +27 -6
- package/dist/generated/read-regen-tree.d.ts +16 -0
- package/dist/generated/read-regen-tree.d.ts.map +1 -0
- package/dist/generated/read-regen-tree.js +61 -0
- package/dist/generated/scan-generated.d.ts +57 -3
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +111 -11
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -0
- package/dist/model/boundary-intended-empty-line.d.ts +10 -0
- package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
- package/dist/model/boundary-intended-empty-line.js +11 -0
- package/dist/model/boundary-markable-list.d.ts +15 -0
- package/dist/model/boundary-markable-list.d.ts.map +1 -0
- package/dist/model/boundary-markable-list.js +15 -0
- package/dist/model/boundary-rule-input-keys.d.ts +11 -0
- package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
- package/dist/model/boundary-rule-input-keys.js +29 -0
- package/dist/model/boundary-rule-input.d.ts +23 -0
- package/dist/model/boundary-rule-input.d.ts.map +1 -0
- package/dist/model/boundary-rule-input.js +1 -0
- package/dist/model/boundary-rule-key-problems.d.ts +11 -0
- package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-key-problems.js +36 -0
- package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
- package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-marker-problems.js +94 -0
- package/dist/model/boundary-rule-scope.d.ts +88 -0
- package/dist/model/boundary-rule-scope.d.ts.map +1 -0
- package/dist/model/boundary-rule-scope.js +175 -0
- package/dist/model/boundary-rule.d.ts +120 -5
- package/dist/model/boundary-rule.d.ts.map +1 -1
- package/dist/model/boundary-rule.js +148 -1
- package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
- package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
- package/dist/model/boundary-unit-problem-issue.js +16 -0
- package/dist/model/normalize-boundary-rule.d.ts +21 -0
- package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
- package/dist/model/normalize-boundary-rule.js +62 -0
- package/dist/policy/evaluate-policy.d.ts +80 -3
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +152 -19
- package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
- package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
- package/dist/policy/i-policy-rule-liveness.js +1 -0
- package/dist/policy/run-policy.d.ts +1 -1
- package/dist/policy/run-policy.d.ts.map +1 -1
- package/dist/policy/run-policy.js +171 -21
- package/dist/registry/load-boundary-rules.d.ts +29 -1
- package/dist/registry/load-boundary-rules.d.ts.map +1 -1
- package/dist/registry/load-boundary-rules.js +38 -8
- package/dist/scan/glob.d.ts +90 -0
- package/dist/scan/glob.d.ts.map +1 -1
- package/dist/scan/glob.js +205 -0
- package/dist/scan/i-glob-unit-measure.d.ts +22 -0
- package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
- package/dist/scan/i-glob-unit-measure.js +1 -0
- package/dist/scan/import-pattern.d.ts +109 -0
- package/dist/scan/import-pattern.d.ts.map +1 -0
- package/dist/scan/import-pattern.js +191 -0
- package/dist/scan/node-builtin-package-names.d.ts +13 -0
- package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
- package/dist/scan/node-builtin-package-names.js +25 -0
- package/dist/scan/scan-imports.d.ts +36 -3
- package/dist/scan/scan-imports.d.ts.map +1 -1
- package/dist/scan/scan-imports.js +114 -45
- package/dist/util/blank-run-hazard-finding.d.ts +34 -0
- package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
- package/dist/util/blank-run-hazard-finding.js +1 -0
- package/dist/util/blank-run-hazard.d.ts +29 -0
- package/dist/util/blank-run-hazard.d.ts.map +1 -0
- package/dist/util/blank-run-hazard.js +450 -0
- package/dist/util/dead-glob-units.d.ts +46 -0
- package/dist/util/dead-glob-units.d.ts.map +1 -0
- package/dist/util/dead-glob-units.js +116 -0
- package/dist/util/glob-list-liveness-input.d.ts +31 -0
- package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
- package/dist/util/glob-list-liveness-input.js +65 -0
- package/dist/util/i-dead-glob-unit.d.ts +24 -0
- package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
- package/dist/util/i-dead-glob-unit.js +1 -0
- package/dist/util/i-glob-list-units.d.ts +25 -0
- package/dist/util/i-glob-list-units.d.ts.map +1 -0
- package/dist/util/i-glob-list-units.js +1 -0
- package/dist/util/i-glob-liveness-list.d.ts +23 -0
- package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-list.js +1 -0
- package/dist/util/i-glob-liveness-request.d.ts +11 -0
- package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-request.js +1 -0
- package/dist/util/i-glob-negation.d.ts +13 -0
- package/dist/util/i-glob-negation.d.ts.map +1 -0
- package/dist/util/i-glob-negation.js +1 -0
- package/dist/util/matched-files.d.ts +22 -0
- package/dist/util/matched-files.d.ts.map +1 -0
- package/dist/util/matched-files.js +1 -0
- package/dist/util/negation-cause.d.ts +15 -0
- package/dist/util/negation-cause.d.ts.map +1 -0
- package/dist/util/negation-cause.js +21 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
- package/dist/util/plane-scan-exclude-dirs.js +22 -0
- package/dist/util/read-glob-list-liveness.d.ts +20 -0
- package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
- package/dist/util/read-glob-list-liveness.js +20 -0
- package/dist/util/read-scope-coverage.d.ts +90 -0
- package/dist/util/read-scope-coverage.d.ts.map +1 -0
- package/dist/util/read-scope-coverage.js +173 -0
- package/dist/util/read-scope.d.ts +14 -0
- package/dist/util/read-scope.d.ts.map +1 -0
- package/dist/util/read-scope.js +1 -0
- package/dist/util/read-selected-files.d.ts +16 -0
- package/dist/util/read-selected-files.d.ts.map +1 -0
- package/dist/util/read-selected-files.js +25 -0
- package/dist/util/settle-glob-lists.d.ts +12 -0
- package/dist/util/settle-glob-lists.d.ts.map +1 -0
- package/dist/util/settle-glob-lists.js +13 -0
- package/dist/util/unread-file-reason.d.ts +26 -0
- package/dist/util/unread-file-reason.d.ts.map +1 -0
- package/dist/util/unread-file-reason.js +26 -0
- package/dist/util/unread-file.d.ts +10 -0
- package/dist/util/unread-file.d.ts.map +1 -0
- package/dist/util/unread-file.js +1 -0
- package/dist/util/walk-files.d.ts +90 -7
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +206 -19
- package/dist/wiring/evaluate-wiring.d.ts +88 -7
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +276 -16
- package/dist/wiring/explain-wiring.d.ts +56 -14
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +136 -10
- package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
- package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/i-idiom-role-coverage.js +1 -0
- package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
- package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/i-registration-query-verdict.js +1 -0
- package/dist/wiring/i-registration-roles.d.ts +71 -0
- package/dist/wiring/i-registration-roles.d.ts.map +1 -0
- package/dist/wiring/i-registration-roles.js +1 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/measure-idiom-role-coverage.js +20 -0
- package/dist/wiring/measure-registration-roles.d.ts +25 -0
- package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
- package/dist/wiring/measure-registration-roles.js +124 -0
- package/dist/wiring/plan-wiring-fix.d.ts +16 -1
- package/dist/wiring/plan-wiring-fix.d.ts.map +1 -1
- package/dist/wiring/plan-wiring-fix.js +177 -4
- package/dist/wiring/registration-graph.d.ts +13 -1
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +22 -6
- package/dist/wiring/registration-query-verdict.d.ts +23 -0
- package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/registration-query-verdict.js +118 -0
- package/dist/wiring/registry-query.d.ts +9 -1
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +17 -6
- package/dist/wiring/scan-wiring-files.d.ts +5 -3
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +55 -14
- package/dist/wiring/sink-imports.d.ts +95 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -0
- package/dist/wiring/sink-imports.js +124 -0
- package/dist/wiring/trace-literal.d.ts +6 -0
- package/dist/wiring/trace-literal.d.ts.map +1 -1
- package/dist/wiring/trace-literal.js +5 -2
- package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
- package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
- package/dist/wiring/wiring-labeled-sources.js +20 -0
- package/package.json +2 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { globListSelects } from "../scan/glob.js";
|
|
2
|
+
import { resolveAliasCandidates } from "../scan/tsconfig-aliases.js";
|
|
3
|
+
import { safeCompile } from "../util/safe-regex.js";
|
|
4
|
+
import { parseImportStatementsWithMeta } from "./parse-imports.js";
|
|
5
|
+
/** Normalize a relative specifier against the importing file's directory. */
|
|
6
|
+
function resolveRelative(fromFile, specifier) {
|
|
7
|
+
const dir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : '';
|
|
8
|
+
const stack = dir.split('/').filter(Boolean);
|
|
9
|
+
for (const part of specifier.split('/')) {
|
|
10
|
+
if (part === '' || part === '.')
|
|
11
|
+
continue;
|
|
12
|
+
if (part === '..') {
|
|
13
|
+
stack.pop();
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
stack.push(part);
|
|
17
|
+
}
|
|
18
|
+
return stack.join('/');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a specifier the way the boundary engine does: relative paths against
|
|
22
|
+
* the importing file, bare specifiers through the tsconfig alias map, and
|
|
23
|
+
* anything left over kept as the literal package name.
|
|
24
|
+
*/
|
|
25
|
+
function resolveSpecifier(fromFile, specifier, aliases) {
|
|
26
|
+
if (specifier.startsWith('.')) {
|
|
27
|
+
return { specifier, path: resolveRelative(fromFile, specifier) };
|
|
28
|
+
}
|
|
29
|
+
if (aliases) {
|
|
30
|
+
const candidates = resolveAliasCandidates(specifier, aliases);
|
|
31
|
+
// The first candidate is the one the compiler would try first; keeping only
|
|
32
|
+
// it makes the emitted id deterministic rather than order-dependent.
|
|
33
|
+
if (candidates.length > 0)
|
|
34
|
+
return { specifier, path: candidates[0] };
|
|
35
|
+
}
|
|
36
|
+
return { specifier };
|
|
37
|
+
}
|
|
38
|
+
/** Whether a resolved specifier is one the rule's `to` selects. */
|
|
39
|
+
function matchesTarget(resolved, to, modulePattern) {
|
|
40
|
+
const hasSelector = to.module !== undefined || to.modulePattern !== undefined || (to.files?.length ?? 0) > 0;
|
|
41
|
+
// No target selector at all means "every import" — useful when the rule
|
|
42
|
+
// narrows by symbol alone.
|
|
43
|
+
if (!hasSelector)
|
|
44
|
+
return true;
|
|
45
|
+
if (to.module !== undefined) {
|
|
46
|
+
// Exact package, or a subpath of it: `@x/generated` selects
|
|
47
|
+
// `@x/generated` and `@x/generated/views` but never `@x/generated-legacy`.
|
|
48
|
+
if (resolved.specifier === to.module || resolved.specifier.startsWith(`${to.module}/`)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (modulePattern) {
|
|
53
|
+
modulePattern.lastIndex = 0;
|
|
54
|
+
if (modulePattern.test(resolved.specifier))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (to.files && to.files.length > 0 && resolved.path !== undefined) {
|
|
58
|
+
// `to.files` is a glob LIST like any other: a `!` entry subtracts.
|
|
59
|
+
if (globListSelects(resolved.path, to.files))
|
|
60
|
+
return true;
|
|
61
|
+
// Specifiers routinely omit the extension, so probe the usual endings
|
|
62
|
+
// rather than forcing every config to spell `*.{ts,tsx,…}` itself.
|
|
63
|
+
for (const ext of ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx']) {
|
|
64
|
+
if (globListSelects(resolved.path + ext, to.files))
|
|
65
|
+
return true;
|
|
66
|
+
if (globListSelects(`${resolved.path}/index${ext}`, to.files))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract the import edges a source selects.
|
|
74
|
+
*
|
|
75
|
+
* `files` is the CONSUMER glob (the `from` side): it reuses the field every
|
|
76
|
+
* other extractor uses, so `--changed-only` footprinting, the shared walk, and
|
|
77
|
+
* `$use` all work on it unchanged.
|
|
78
|
+
*/
|
|
79
|
+
export function extractImportEdges(source, files, context = {}) {
|
|
80
|
+
const to = source.to ?? {};
|
|
81
|
+
const emit = source.emit ?? 'edge';
|
|
82
|
+
let modulePattern;
|
|
83
|
+
if (to.modulePattern !== undefined) {
|
|
84
|
+
const compiled = safeCompile(to.modulePattern, to.modulePatternFlags);
|
|
85
|
+
if (compiled.error || !compiled.re)
|
|
86
|
+
return { sites: [], error: `to.modulePattern ${compiled.error}` };
|
|
87
|
+
modulePattern = compiled.re;
|
|
88
|
+
}
|
|
89
|
+
let symbolPattern;
|
|
90
|
+
if (to.match !== undefined) {
|
|
91
|
+
const compiled = safeCompile(to.match, to.matchFlags);
|
|
92
|
+
if (compiled.error || !compiled.re)
|
|
93
|
+
return { sites: [], error: `to.match ${compiled.error}` };
|
|
94
|
+
symbolPattern = compiled.re;
|
|
95
|
+
}
|
|
96
|
+
// Zoning is judged by where the statement's KEYWORD starts and whether its
|
|
97
|
+
// specifier is a real string — the same authority `check boundaries` uses
|
|
98
|
+
// (round 11, 6.1a#import-edges-scan). The extractor receives RAW content:
|
|
99
|
+
// pre-blanking with `scan: 'code'` used to erase every specifier (a string)
|
|
100
|
+
// and turn the rule vacuous. Unset → comment-aware, matching `check
|
|
101
|
+
// boundaries`; `scan: 'all'` is the raw escape hatch. (`strings` / `comments`
|
|
102
|
+
// are rejected at config load: an import statement is code.)
|
|
103
|
+
const zone = source.scan === 'all' ? 'all' : 'code';
|
|
104
|
+
const sites = [];
|
|
105
|
+
let blankedChars = 0;
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const parsed = parseImportStatementsWithMeta(file.content, { zone });
|
|
108
|
+
blankedChars += parsed.blankedChars;
|
|
109
|
+
for (const statement of parsed.statements) {
|
|
110
|
+
const resolved = resolveSpecifier(file.path, statement.specifier, context.tsconfigPaths);
|
|
111
|
+
if (!matchesTarget(resolved, to, modulePattern))
|
|
112
|
+
continue;
|
|
113
|
+
const symbols = statement.bindings
|
|
114
|
+
.map((b) => b.imported ?? b.local)
|
|
115
|
+
.filter((name) => {
|
|
116
|
+
if (!symbolPattern)
|
|
117
|
+
return true;
|
|
118
|
+
symbolPattern.lastIndex = 0;
|
|
119
|
+
return symbolPattern.test(name);
|
|
120
|
+
});
|
|
121
|
+
// A symbol filter that matched nothing means this statement is not a hit,
|
|
122
|
+
// even though its module was: the rule asked about specific symbols.
|
|
123
|
+
if (symbolPattern && symbols.length === 0)
|
|
124
|
+
continue;
|
|
125
|
+
if (emit === 'from') {
|
|
126
|
+
sites.push({ token: file.path, file: file.path, line: statement.line });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (symbols.length === 0) {
|
|
130
|
+
// A side-effect or type-only import binds no name; the edge is still a
|
|
131
|
+
// real dependency, so it is reported against the module it names.
|
|
132
|
+
if (emit === 'symbol')
|
|
133
|
+
continue;
|
|
134
|
+
sites.push({
|
|
135
|
+
token: `${file.path} → ${resolved.specifier}`,
|
|
136
|
+
file: file.path,
|
|
137
|
+
line: statement.line,
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const symbol of symbols) {
|
|
142
|
+
sites.push({
|
|
143
|
+
token: emit === 'symbol' ? symbol : `${file.path} → ${symbol}`,
|
|
144
|
+
file: file.path,
|
|
145
|
+
line: statement.line,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// The one place a user's intuition reliably trips: `to.files` matches an
|
|
151
|
+
// import's DIRECTLY-resolved path, and a symbol re-exported through a barrel
|
|
152
|
+
// resolves to the package entry, not the deep file. "Edges to files in
|
|
153
|
+
// generated/**" then finds nothing, correctly but unhelpfully. Detecting the
|
|
154
|
+
// exact shape of that dead end and naming the fix costs one conditional.
|
|
155
|
+
//
|
|
156
|
+
// Narrow on purpose. A rule that ALREADY targets by module has been told to
|
|
157
|
+
// use the thing it is using, which is worse than saying nothing — so the
|
|
158
|
+
// hint fires only when `to.files` is the sole target selector.
|
|
159
|
+
const targetsByFilesOnly = (to.files?.length ?? 0) > 0 && to.module === undefined && to.modulePattern === undefined;
|
|
160
|
+
if (sites.length === 0 && files.length > 0 && targetsByFilesOnly) {
|
|
161
|
+
return {
|
|
162
|
+
sites,
|
|
163
|
+
hint: '0 edges via `to.files` — that matches an import\'s DIRECTLY-resolved path, so a symbol ' +
|
|
164
|
+
're-exported through a barrel/package resolves to the package entry, not the deep file. ' +
|
|
165
|
+
'Target by `to.module` + `to.match` instead.',
|
|
166
|
+
blankedChars,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { sites, blankedChars };
|
|
170
|
+
}
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IWiringSource } from '@shrkcrft/core';
|
|
2
|
+
import type { IGlobListUnits } from '../util/i-glob-list-units.js';
|
|
3
|
+
import type { IUnreadFile } from '../util/unread-file.js';
|
|
2
4
|
import { type IExtractedSite } from './extract-tokens.js';
|
|
3
5
|
/** What one source actually resolved to against the live tree. */
|
|
4
6
|
export interface ISourceInspection {
|
|
5
|
-
/** Files the globs matched (after the shared walk's skip rules). */
|
|
7
|
+
/** Files the globs matched and the reader READ (after the shared walk's skip rules). */
|
|
6
8
|
readonly filesScanned: number;
|
|
9
|
+
/**
|
|
10
|
+
* Files the globs matched that the reader did NOT read (over the read cap,
|
|
11
|
+
* or unreadable). A source with any is not "matched nothing" and not fully
|
|
12
|
+
* examined: its coverage names them (`readScopeCoverage`).
|
|
13
|
+
*/
|
|
14
|
+
readonly unread: readonly IUnreadFile[];
|
|
7
15
|
/** Distinct ids extracted, sorted. */
|
|
8
16
|
readonly ids: readonly string[];
|
|
9
17
|
/** Every capture site, in stable (file, line) order. */
|
|
10
18
|
readonly sites: readonly IExtractedSite[];
|
|
11
19
|
/** Set when the source is misconfigured. */
|
|
12
20
|
readonly error?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A diagnosis for a zero-match that is technically correct but almost
|
|
23
|
+
* certainly not what the author meant — surfaced alongside the loud skip so
|
|
24
|
+
* the dead end explains itself.
|
|
25
|
+
*/
|
|
26
|
+
readonly hint?: string;
|
|
13
27
|
}
|
|
14
28
|
/**
|
|
15
29
|
* Resolve ONE source against the tree and report what it matched.
|
|
@@ -20,5 +34,26 @@ export interface ISourceInspection {
|
|
|
20
34
|
* extract?" has one answer computed one way. A rule that matched 0 is then a
|
|
21
35
|
* fact the tooling can report, not something an author has to notice.
|
|
22
36
|
*/
|
|
23
|
-
export declare function inspectSource(projectRoot: string,
|
|
37
|
+
export declare function inspectSource(projectRoot: string, rawSource: IWiringSource, excludeDirs?: readonly string[]): ISourceInspection;
|
|
38
|
+
/**
|
|
39
|
+
* ONE source's glob units: the dead ones (each with its reason), the live
|
|
40
|
+
* negations with what each excludes, and how many globs were checked — through
|
|
41
|
+
* the one dead-unit decision, `globListUnits`.
|
|
42
|
+
*
|
|
43
|
+
* It reads the same walk {@link inspectSource} reads (inside
|
|
44
|
+
* `withFileReadCache` a second call over the same globs is a memo hit), so the
|
|
45
|
+
* answer is about the files the source actually SEES — after `SKIP_DIRS` and
|
|
46
|
+
* `excludeDirs`. The walk is the list's POSITIVE set (every file an inclusion
|
|
47
|
+
* glob matched), so a negation is judged by what it removes from it, never by
|
|
48
|
+
* what it "matches". A file over the read cap was still MATCHED, so a glob that
|
|
49
|
+
* matched only such a file is not dead. Only `files[]` walk globs count: an
|
|
50
|
+
* `import-edges` `to.files` matches resolved specifiers, not walked files.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sourceGlobUnits(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): IGlobListUnits;
|
|
53
|
+
/**
|
|
54
|
+
* The globs of ONE source that do nothing — a dead unit inside a rule whose
|
|
55
|
+
* other globs may still keep it connected. The labels of
|
|
56
|
+
* {@link sourceGlobUnits}`.dead` (a dead negation keeps its `!`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function sourceDeadGlobs(projectRoot: string, source: IWiringSource, excludeDirs?: readonly string[]): readonly string[];
|
|
24
59
|
//# sourceMappingURL=inspect-source.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGzE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,aAAa,EACxB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAyBnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAEhB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,SAAS,MAAM,EAAE,CAEnB"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { normalizeWiringSource } from '@shrkcrft/core';
|
|
2
|
+
import { readSelectedFiles } from "../util/read-selected-files.js";
|
|
3
|
+
import { readGlobListUnits } from "../util/dead-glob-units.js";
|
|
3
4
|
import { extractTokens } from "./extract-tokens.js";
|
|
5
|
+
import { loadTsconfigPaths } from "../scan/tsconfig-aliases.js";
|
|
4
6
|
/**
|
|
5
7
|
* Resolve ONE source against the tree and report what it matched.
|
|
6
8
|
*
|
|
@@ -10,18 +12,64 @@ import { extractTokens } from "./extract-tokens.js";
|
|
|
10
12
|
* extract?" has one answer computed one way. A rule that matched 0 is then a
|
|
11
13
|
* fact the tooling can report, not something an author has to notice.
|
|
12
14
|
*/
|
|
13
|
-
export function inspectSource(projectRoot,
|
|
15
|
+
export function inspectSource(projectRoot, rawSource, excludeDirs = []) {
|
|
16
|
+
// The engine entry normalises idempotently (round 13): a loaded source comes
|
|
17
|
+
// back equal; a hand-built one with a `{ pattern, expectEmpty }` entry reads
|
|
18
|
+
// its pattern as the glob, and a malformed entry is a misconfiguration —
|
|
19
|
+
// never a glob reader handed an object.
|
|
20
|
+
const normalized = normalizeWiringSource(rawSource);
|
|
21
|
+
if (!normalized.ok)
|
|
22
|
+
return { filesScanned: 0, unread: [], ids: [], sites: [], error: normalized.error.message };
|
|
23
|
+
const source = normalized.value;
|
|
14
24
|
const globs = source.files ?? [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const res = extractTokens(source, files);
|
|
25
|
+
const { files, unread } = sourceFiles(projectRoot, globs, excludeDirs);
|
|
26
|
+
// `import-edges` resolves alias specifiers the way the compiler would, so it
|
|
27
|
+
// needs the project's tsconfig paths. Loading it here (rather than inside the
|
|
28
|
+
// extractor) keeps every extractor a pure function of its inputs.
|
|
29
|
+
const res = extractTokens(source, files, { tsconfigPaths: loadTsconfigPaths(projectRoot) });
|
|
20
30
|
const sites = [...res.sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.token.localeCompare(b.token));
|
|
21
31
|
return {
|
|
22
32
|
filesScanned: files.length,
|
|
33
|
+
unread,
|
|
23
34
|
ids: [...new Set(sites.map((s) => s.token))].sort(),
|
|
24
35
|
sites,
|
|
25
36
|
...(res.error ? { error: res.error } : {}),
|
|
37
|
+
...(res.hint ? { hint: res.hint } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* ONE source's glob units: the dead ones (each with its reason), the live
|
|
42
|
+
* negations with what each excludes, and how many globs were checked — through
|
|
43
|
+
* the one dead-unit decision, `globListUnits`.
|
|
44
|
+
*
|
|
45
|
+
* It reads the same walk {@link inspectSource} reads (inside
|
|
46
|
+
* `withFileReadCache` a second call over the same globs is a memo hit), so the
|
|
47
|
+
* answer is about the files the source actually SEES — after `SKIP_DIRS` and
|
|
48
|
+
* `excludeDirs`. The walk is the list's POSITIVE set (every file an inclusion
|
|
49
|
+
* glob matched), so a negation is judged by what it removes from it, never by
|
|
50
|
+
* what it "matches". A file over the read cap was still MATCHED, so a glob that
|
|
51
|
+
* matched only such a file is not dead. Only `files[]` walk globs count: an
|
|
52
|
+
* `import-edges` `to.files` matches resolved specifiers, not walked files.
|
|
53
|
+
*/
|
|
54
|
+
export function sourceGlobUnits(projectRoot, source, excludeDirs = []) {
|
|
55
|
+
return readGlobListUnits(projectRoot, source.files ?? [], new Set(excludeDirs));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The globs of ONE source that do nothing — a dead unit inside a rule whose
|
|
59
|
+
* other globs may still keep it connected. The labels of
|
|
60
|
+
* {@link sourceGlobUnits}`.dead` (a dead negation keeps its `!`).
|
|
61
|
+
*/
|
|
62
|
+
export function sourceDeadGlobs(projectRoot, source, excludeDirs = []) {
|
|
63
|
+
return sourceGlobUnits(projectRoot, source, excludeDirs).dead.map((u) => u.glob);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The files a source's glob list SELECTS (an inclusion glob matches, no
|
|
67
|
+
* negation does), after the shared walk's skip rules: read, and unread.
|
|
68
|
+
*/
|
|
69
|
+
function sourceFiles(projectRoot, globs, excludeDirs) {
|
|
70
|
+
const selected = readSelectedFiles(projectRoot, globs, new Set(excludeDirs));
|
|
71
|
+
return {
|
|
72
|
+
files: [...selected.files.entries()].map(([path, content]) => ({ path, content })),
|
|
73
|
+
unread: [...selected.unread],
|
|
26
74
|
};
|
|
27
75
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which text an import statement may be read from.
|
|
3
|
+
*
|
|
4
|
+
* - `code` (the default): the statement's keyword must start in executable
|
|
5
|
+
* code and its specifier must be a real string literal; comments are
|
|
6
|
+
* blanked before matching, so an apostrophe inside a comment within a
|
|
7
|
+
* multi-line clause can no longer cut a real import short.
|
|
8
|
+
* - `all`: the raw file text — every byte, comments included. The explicit
|
|
9
|
+
* escape hatch (`check boundaries --include-comments`, `scan: 'all'`).
|
|
10
|
+
*/
|
|
11
|
+
export type ImportParseZone = 'code' | 'all';
|
|
12
|
+
/** Options for {@link parseImportStatements}. */
|
|
13
|
+
export interface IParseImportsOptions {
|
|
14
|
+
/** Default `code`. */
|
|
15
|
+
readonly zone?: ImportParseZone;
|
|
16
|
+
}
|
|
17
|
+
/** How a name entered scope through an import. */
|
|
18
|
+
export type ImportBindingKind = 'named' | 'default' | 'namespace';
|
|
19
|
+
/** One name bound by an import statement. */
|
|
20
|
+
export interface IImportBinding {
|
|
21
|
+
/** The name as used in the file (after any `as` alias). */
|
|
22
|
+
readonly local: string;
|
|
23
|
+
readonly kind: ImportBindingKind;
|
|
24
|
+
/** The exported name, for a named import (differs from `local` when aliased). */
|
|
25
|
+
readonly imported?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* How the dependency was expressed.
|
|
29
|
+
*
|
|
30
|
+
* `--fix` only ever extends the STATIC import block, so it filters on this;
|
|
31
|
+
* the dependency graph counts every kind, because a fence that missed
|
|
32
|
+
* `await import('../secret')` would have a hole exactly where someone
|
|
33
|
+
* motivated would put one.
|
|
34
|
+
*/
|
|
35
|
+
export type ImportStatementKind = 'import' | 'reexport' | 'side-effect' | 'dynamic' | 'require';
|
|
36
|
+
/** One parsed dependency statement. */
|
|
37
|
+
export interface IParsedImport {
|
|
38
|
+
readonly specifier: string;
|
|
39
|
+
readonly bindings: readonly IImportBinding[];
|
|
40
|
+
/** 1-based line of the statement's start. */
|
|
41
|
+
readonly line: number;
|
|
42
|
+
/** Character offset of the statement's start. */
|
|
43
|
+
readonly index: number;
|
|
44
|
+
/** The matched statement text, for style detection (quotes, semicolons). */
|
|
45
|
+
readonly raw: string;
|
|
46
|
+
/** True for `import type { … }` — binds no runtime value. */
|
|
47
|
+
readonly typeOnly: boolean;
|
|
48
|
+
readonly kind: ImportStatementKind;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every import / re-export statement in `content`, with the names it binds.
|
|
52
|
+
*
|
|
53
|
+
* A `import type { … }` statement is returned with `typeOnly: true` rather than
|
|
54
|
+
* dropped: the `--fix` planner must ignore it (it binds no value), while the
|
|
55
|
+
* dependency graph legitimately counts it as an edge. Returning it and letting
|
|
56
|
+
* each caller decide keeps that judgement at the call site instead of baking
|
|
57
|
+
* one feature's answer into the shared parser.
|
|
58
|
+
*
|
|
59
|
+
* In the default `code` zone a statement is kept only when its keyword starts
|
|
60
|
+
* in a code zone AND its specifier quote opens a real string literal — the
|
|
61
|
+
* same {@link zoneKeepsAt} authority the policy plane and the extraction DSL
|
|
62
|
+
* use. Offsets, lines and `raw` always refer to the ORIGINAL text (blanking
|
|
63
|
+
* preserves length), so callers that splice `content` stay correct.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseImportStatements(content: string, options?: IParseImportsOptions): IParsedImport[];
|
|
66
|
+
/**
|
|
67
|
+
* {@link parseImportStatements}, plus how many characters the zone BLANKED
|
|
68
|
+
* before matching: the comment characters under `code`, `0` under `all`.
|
|
69
|
+
*
|
|
70
|
+
* The `import-edges` extractor zones itself (a pre-blanked buffer would erase
|
|
71
|
+
* every specifier), so this is the figure it reports as its `blankedChars` —
|
|
72
|
+
* the number `wiring explain` / `gates explain` print as
|
|
73
|
+
* `scan: code (N chars blanked)`. Without it the note claimed a zone removed
|
|
74
|
+
* nothing while it had dropped a commented-out edge.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseImportStatementsWithMeta(content: string, options?: IParseImportsOptions): {
|
|
77
|
+
readonly statements: IParsedImport[];
|
|
78
|
+
readonly blankedChars: number;
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=parse-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-imports.d.ts","sourceRoot":"","sources":["../../src/extract/parse-imports.ts"],"names":[],"mappings":"AA8BA;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7C,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,sBAAsB;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;CACjC;AAED,kDAAkD;AAClD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAElE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhG,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CACpC;AAsID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,aAAa,EAAE,CAEjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC;IAAE,QAAQ,CAAC,UAAU,EAAE,aAAa,EAAE,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CA4EzE"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE import-statement parser.
|
|
3
|
+
*
|
|
4
|
+
* Two features need to read import statements out of source text: the
|
|
5
|
+
* `--fix` planner (does this sink bind the token, and how?) and the
|
|
6
|
+
* `import-edges` extractor (which file imports what, from where?). A second
|
|
7
|
+
* parser would drift from the first, and the two would then disagree about
|
|
8
|
+
* what a file imports — which is precisely the class of silent disagreement
|
|
9
|
+
* this engine exists to eliminate. So both consume this.
|
|
10
|
+
*
|
|
11
|
+
* It is a lexer, not a compiler: it answers what the text says, which is the
|
|
12
|
+
* largest thing that stays honest across `.ts`, `.tsx`, `.mts` and plain JS
|
|
13
|
+
* with one code path.
|
|
14
|
+
*
|
|
15
|
+
* Round 11 (6.1a) made it the ONLY import extractor: `scanImports` (every
|
|
16
|
+
* boundary check, drift, impact, review packet…) and the `import-edges` DSL
|
|
17
|
+
* extractor both read through here, so "what does this file import" has one
|
|
18
|
+
* answer. By default it reads CODE only — a commented-out import, an import in
|
|
19
|
+
* a doc-comment code fence, or `"import x from 'y'"` inside a string is not a
|
|
20
|
+
* dependency. Measured against the TypeScript oracle over this repo (1716
|
|
21
|
+
* files): 79 phantom edges and 1 real miss before, 0 / 0 after.
|
|
22
|
+
*/
|
|
23
|
+
import { blankZoneKinds, lexCodeZones, zoneContaining, zoneKeepsAt, } from "./code-zones.js";
|
|
24
|
+
/**
|
|
25
|
+
* Matches one import/re-export statement up to its specifier.
|
|
26
|
+
*
|
|
27
|
+
* The clause may span lines (a multi-line `{ … }` block is normal) but never
|
|
28
|
+
* contains a `;`, a quote or a BACKTICK, which is what keeps the lazy match
|
|
29
|
+
* from running past the end of its own statement into the next one. No real
|
|
30
|
+
* import clause holds a backtick; prose does — a doc comment's
|
|
31
|
+
* `` `shrk import <format> --populate` `` used to open a clause that ran on to
|
|
32
|
+
* the next real `from '…'`, so the raw reading (`--include-comments`,
|
|
33
|
+
* `scan: 'all'`) filed that import on the comment's line. The old scanImports
|
|
34
|
+
* regex excluded the backtick too; raw-mode lines match it again.
|
|
35
|
+
*
|
|
36
|
+
* `export … from '…'` is matched too: a re-export is an edge in the dependency
|
|
37
|
+
* graph exactly like an import, and a barrel that is invisible here would make
|
|
38
|
+
* every consumer behind it invisible as well.
|
|
39
|
+
*
|
|
40
|
+
* Linear by construction (round 11, 6.1(b)): the old form
|
|
41
|
+
* `\s+([^;'"]*?)\s*from` put three whitespace-consuming quantifiers side by
|
|
42
|
+
* side, which is fine on raw source and O(run²) once comments are blanked into
|
|
43
|
+
* long runs of spaces (6 / 24 / 94 / 371 ms as a JSDoc block doubled). Here a
|
|
44
|
+
* single `\s` separates the keyword from ONE lazy class, and `\bfrom` needs no
|
|
45
|
+
* whitespace quantifier in front of it, so each keyword costs one forward scan
|
|
46
|
+
* to its statement's end. Verified byte-identical to the old form on the raw
|
|
47
|
+
* text of every file in this repo. Do not reintroduce adjacent `\s*`/`\s+`
|
|
48
|
+
* around a lazy class — `r75-import-scan-zones.test.ts` times it.
|
|
49
|
+
*/
|
|
50
|
+
const IMPORT_STATEMENT = /\b(import|export)\s([^;'"`]*?)\bfrom\s*['"]([^'"]+)['"]\s*;?/g;
|
|
51
|
+
/**
|
|
52
|
+
* The specifier-only forms, which bind no name: a side-effect import, a dynamic
|
|
53
|
+
* `import()`, and `require()`. Each is a real dependency edge — the boundary
|
|
54
|
+
* engine counts all three — even though none of them introduces a binding.
|
|
55
|
+
*/
|
|
56
|
+
const SIDE_EFFECT_IMPORT = /\bimport\s*['"]([^'"]+)['"]\s*;?/g;
|
|
57
|
+
const DYNAMIC_IMPORT = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
58
|
+
const REQUIRE_CALL = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
59
|
+
/** Split a clause on commas that are OUTSIDE a `{ … }` block. */
|
|
60
|
+
function splitTopLevel(clause) {
|
|
61
|
+
const out = [];
|
|
62
|
+
let depth = 0;
|
|
63
|
+
let current = '';
|
|
64
|
+
for (const ch of clause) {
|
|
65
|
+
if (ch === '{')
|
|
66
|
+
depth += 1;
|
|
67
|
+
if (ch === '}')
|
|
68
|
+
depth -= 1;
|
|
69
|
+
if (ch === ',' && depth === 0) {
|
|
70
|
+
out.push(current);
|
|
71
|
+
current = '';
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
current += ch;
|
|
75
|
+
}
|
|
76
|
+
if (current.trim() !== '')
|
|
77
|
+
out.push(current);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Parse the clause between `import`/`export` and `from`. */
|
|
81
|
+
function parseClause(clause) {
|
|
82
|
+
const bindings = [];
|
|
83
|
+
for (const part of splitTopLevel(clause.trim())) {
|
|
84
|
+
const piece = part.trim();
|
|
85
|
+
if (piece === '')
|
|
86
|
+
continue;
|
|
87
|
+
if (piece.startsWith('*')) {
|
|
88
|
+
const m = /^\*\s*as\s+([A-Za-z_$][\w$]*)$/.exec(piece);
|
|
89
|
+
if (m)
|
|
90
|
+
bindings.push({ local: m[1], kind: 'namespace' });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (piece.startsWith('{')) {
|
|
94
|
+
const inner = piece.replace(/^\{/, '').replace(/\}$/, '');
|
|
95
|
+
for (const spec of inner.split(',')) {
|
|
96
|
+
const s = spec.trim();
|
|
97
|
+
// An inline `type` specifier binds no runtime value.
|
|
98
|
+
if (s === '' || /^type\b/.test(s))
|
|
99
|
+
continue;
|
|
100
|
+
const aliased = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(s);
|
|
101
|
+
if (aliased) {
|
|
102
|
+
bindings.push({ local: aliased[2], kind: 'named', imported: aliased[1] });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (/^[A-Za-z_$][\w$]*$/.test(s)) {
|
|
106
|
+
bindings.push({ local: s, kind: 'named', imported: s });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (/^[A-Za-z_$][\w$]*$/.test(piece)) {
|
|
112
|
+
bindings.push({ local: piece, kind: 'default' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return bindings;
|
|
116
|
+
}
|
|
117
|
+
/** Offsets of every `\n` in `content`, for O(log n) line lookups. */
|
|
118
|
+
function newlineOffsets(content) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (let i = content.indexOf('\n'); i !== -1; i = content.indexOf('\n', i + 1))
|
|
121
|
+
out.push(i);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 1-based line of `index`: one plus the number of newlines BEFORE it. The
|
|
126
|
+
* index is the keyword's own offset — the old `scanImports` regex consumed the
|
|
127
|
+
* preceding `\n` with `(?:^|\s)`, so every import after line 1 was reported
|
|
128
|
+
* one line early (round 11, 6.1a#line-numbers).
|
|
129
|
+
*/
|
|
130
|
+
function lineAt(newlines, index) {
|
|
131
|
+
let lo = 0;
|
|
132
|
+
let hi = newlines.length;
|
|
133
|
+
while (lo < hi) {
|
|
134
|
+
const mid = (lo + hi) >> 1;
|
|
135
|
+
if (newlines[mid] < index)
|
|
136
|
+
lo = mid + 1;
|
|
137
|
+
else
|
|
138
|
+
hi = mid;
|
|
139
|
+
}
|
|
140
|
+
return lo + 1;
|
|
141
|
+
}
|
|
142
|
+
/** Zones blanked before matching in `code` mode: comments only — the specifier is a string. */
|
|
143
|
+
const COMMENT_ZONES = new Set(['comment']);
|
|
144
|
+
/**
|
|
145
|
+
* Offset (within the match) of the specifier's OPENING quote. Every pattern
|
|
146
|
+
* ends `<quote><specifier><quote>` followed only by whitespace, `;` or `)`,
|
|
147
|
+
* and no clause may contain a quote, so the last quote in the match closes the
|
|
148
|
+
* specifier.
|
|
149
|
+
*/
|
|
150
|
+
function specifierQuoteOffset(match, specifier) {
|
|
151
|
+
const closing = Math.max(match.lastIndexOf("'"), match.lastIndexOf('"'));
|
|
152
|
+
return closing - specifier.length - 1;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Every import / re-export statement in `content`, with the names it binds.
|
|
156
|
+
*
|
|
157
|
+
* A `import type { … }` statement is returned with `typeOnly: true` rather than
|
|
158
|
+
* dropped: the `--fix` planner must ignore it (it binds no value), while the
|
|
159
|
+
* dependency graph legitimately counts it as an edge. Returning it and letting
|
|
160
|
+
* each caller decide keeps that judgement at the call site instead of baking
|
|
161
|
+
* one feature's answer into the shared parser.
|
|
162
|
+
*
|
|
163
|
+
* In the default `code` zone a statement is kept only when its keyword starts
|
|
164
|
+
* in a code zone AND its specifier quote opens a real string literal — the
|
|
165
|
+
* same {@link zoneKeepsAt} authority the policy plane and the extraction DSL
|
|
166
|
+
* use. Offsets, lines and `raw` always refer to the ORIGINAL text (blanking
|
|
167
|
+
* preserves length), so callers that splice `content` stay correct.
|
|
168
|
+
*/
|
|
169
|
+
export function parseImportStatements(content, options = {}) {
|
|
170
|
+
return parseImportStatementsWithMeta(content, options).statements;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* {@link parseImportStatements}, plus how many characters the zone BLANKED
|
|
174
|
+
* before matching: the comment characters under `code`, `0` under `all`.
|
|
175
|
+
*
|
|
176
|
+
* The `import-edges` extractor zones itself (a pre-blanked buffer would erase
|
|
177
|
+
* every specifier), so this is the figure it reports as its `blankedChars` —
|
|
178
|
+
* the number `wiring explain` / `gates explain` print as
|
|
179
|
+
* `scan: code (N chars blanked)`. Without it the note claimed a zone removed
|
|
180
|
+
* nothing while it had dropped a commented-out edge.
|
|
181
|
+
*/
|
|
182
|
+
export function parseImportStatementsWithMeta(content, options = {}) {
|
|
183
|
+
const zone = options.zone ?? 'code';
|
|
184
|
+
const zones = zone === 'code' ? lexCodeZones(content) : undefined;
|
|
185
|
+
const blanked = zones ? blankZoneKinds(content, zones, COMMENT_ZONES) : undefined;
|
|
186
|
+
const text = blanked ? blanked.content : content;
|
|
187
|
+
// Built on first use: a file whose statements are all zoned out (or that has
|
|
188
|
+
// none) never needs a line table.
|
|
189
|
+
let newlines;
|
|
190
|
+
const lineOfIndex = (index) => lineAt((newlines ??= newlineOffsets(content)), index);
|
|
191
|
+
const keep = (index, match, specifier) => {
|
|
192
|
+
if (!zones)
|
|
193
|
+
return true;
|
|
194
|
+
const quote = index + specifierQuoteOffset(match, specifier);
|
|
195
|
+
// The specifier's quote must OPEN a plain string literal. "Inside a string"
|
|
196
|
+
// is not enough: `export const t = \`import z from 'w'\`` starts with a
|
|
197
|
+
// code `export`, and its `'w'` sits inside the template literal.
|
|
198
|
+
const specZone = zoneContaining(zones, quote);
|
|
199
|
+
return (zoneKeepsAt('code', zones, index) &&
|
|
200
|
+
zoneKeepsAt('strings', zones, quote) &&
|
|
201
|
+
specZone !== undefined &&
|
|
202
|
+
specZone.start === quote &&
|
|
203
|
+
specZone.template !== true);
|
|
204
|
+
};
|
|
205
|
+
const out = [];
|
|
206
|
+
const claimed = [];
|
|
207
|
+
let m;
|
|
208
|
+
IMPORT_STATEMENT.lastIndex = 0;
|
|
209
|
+
while ((m = IMPORT_STATEMENT.exec(text)) !== null) {
|
|
210
|
+
const specifier = m[3];
|
|
211
|
+
if (!keep(m.index, m[0], specifier))
|
|
212
|
+
continue;
|
|
213
|
+
const clause = m[2];
|
|
214
|
+
const typeOnly = /^type\b/.test(clause.trim());
|
|
215
|
+
claimed.push({ start: m.index, end: m.index + m[0].length });
|
|
216
|
+
out.push({
|
|
217
|
+
specifier,
|
|
218
|
+
bindings: typeOnly ? [] : parseClause(clause),
|
|
219
|
+
line: lineOfIndex(m.index),
|
|
220
|
+
index: m.index,
|
|
221
|
+
raw: content.slice(m.index, m.index + m[0].length),
|
|
222
|
+
typeOnly,
|
|
223
|
+
kind: m[1] === 'export' ? 'reexport' : 'import',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// A `import x from 'y'` also contains the text `import … 'y'`, so the
|
|
227
|
+
// specifier-only patterns would re-report it. Skipping anything inside an
|
|
228
|
+
// already-claimed span keeps each dependency counted exactly once.
|
|
229
|
+
const overlapsClaimed = (start) => claimed.some((c) => start >= c.start && start < c.end);
|
|
230
|
+
for (const [re, kind] of [
|
|
231
|
+
[SIDE_EFFECT_IMPORT, 'side-effect'],
|
|
232
|
+
[DYNAMIC_IMPORT, 'dynamic'],
|
|
233
|
+
[REQUIRE_CALL, 'require'],
|
|
234
|
+
]) {
|
|
235
|
+
re.lastIndex = 0;
|
|
236
|
+
while ((m = re.exec(text)) !== null) {
|
|
237
|
+
if (overlapsClaimed(m.index))
|
|
238
|
+
continue;
|
|
239
|
+
const specifier = m[1];
|
|
240
|
+
if (!keep(m.index, m[0], specifier))
|
|
241
|
+
continue;
|
|
242
|
+
out.push({
|
|
243
|
+
specifier,
|
|
244
|
+
bindings: [],
|
|
245
|
+
line: lineOfIndex(m.index),
|
|
246
|
+
index: m.index,
|
|
247
|
+
raw: content.slice(m.index, m.index + m[0].length),
|
|
248
|
+
typeOnly: false,
|
|
249
|
+
kind,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { statements: out.sort((a, b) => a.index - b.index), blankedChars: blanked?.blankedChars ?? 0 };
|
|
254
|
+
}
|