@shrkcrft/boundaries 0.1.0-alpha.30 → 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 +8 -7
- 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 +30 -0
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +233 -27
- 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 +1 -0
- package/dist/extract/import-edges.d.ts.map +1 -1
- package/dist/extract/import-edges.js +20 -7
- package/dist/extract/inspect-source.d.ts +32 -3
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +50 -7
- package/dist/extract/parse-imports.d.ts +34 -12
- package/dist/extract/parse-imports.d.ts.map +1 -1
- package/dist/extract/parse-imports.js +119 -20
- 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.map +1 -1
- package/dist/generated/check-provenance.js +3 -1
- 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 +8 -0
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +25 -10
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -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 +74 -10
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +137 -31
- package/dist/wiring/evaluate-wiring.d.ts +79 -5
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +267 -12
- package/dist/wiring/explain-wiring.d.ts +49 -5
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +118 -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.js +2 -2
- package/dist/wiring/registration-graph.d.ts +12 -0
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +16 -5
- 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 +8 -0
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +12 -5
- package/dist/wiring/scan-wiring-files.d.ts +4 -2
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +52 -15
- package/dist/wiring/sink-imports.d.ts +11 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -1
- package/dist/wiring/sink-imports.js +11 -2
- 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,191 @@
|
|
|
1
|
+
import { globToRegex } from "./glob.js";
|
|
2
|
+
import { nodeBuiltinPackageNames } from "./node-builtin-package-names.js";
|
|
3
|
+
/**
|
|
4
|
+
* How a forbidden-import (or exception-target) pattern matches an import
|
|
5
|
+
* specifier — the pattern language of the boundary plane (round 11, 1.5).
|
|
6
|
+
*
|
|
7
|
+
* The generic glob matcher keeps `*` = "any chars except `/`" (glob.test.ts
|
|
8
|
+
* pins that), so a bare package pattern like `@scope/package-a` or
|
|
9
|
+
* `@scope/package-*` used to match ONLY the package entrypoint: every subpath
|
|
10
|
+
* import of the very same forbidden package (`@scope/package-a/sub`) escaped
|
|
11
|
+
* the fence, and the gate reported green over it. Package semantics live HERE,
|
|
12
|
+
* in the boundary evaluator's matcher, not in glob.ts.
|
|
13
|
+
*
|
|
14
|
+
* The rule, in one sentence: a pattern with no `**` and no trailing `/` is a
|
|
15
|
+
* PACKAGE pattern — it matches the specifier itself and everything under it
|
|
16
|
+
* (`<pattern>/**`), at a segment boundary, so `@scope/pkg` covers
|
|
17
|
+
* `@scope/pkg/deep` but never `@scope/pkg-legacy`. A pattern containing `**`
|
|
18
|
+
* already says how deep it reaches and is matched exactly as written. A rule
|
|
19
|
+
* that means "the entrypoint only" (barrel avoidance: forbid `lodash`, allow
|
|
20
|
+
* `lodash/get`) opts out with `forbiddenMatch: 'exact'`.
|
|
21
|
+
*/
|
|
22
|
+
/** Does `pattern` take package semantics (entrypoint + every subpath)? */
|
|
23
|
+
export function isPackagePattern(pattern) {
|
|
24
|
+
return !pattern.includes('**') && !pattern.endsWith('/');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Match `specifier` against `pattern`.
|
|
28
|
+
*
|
|
29
|
+
* Returns `'exact'` when the pattern matches the specifier as written,
|
|
30
|
+
* `'subpath'` when only the package semantics reached it (a deeper import of a
|
|
31
|
+
* package the pattern names), and `null` otherwise. `mode: 'exact'` disables
|
|
32
|
+
* the subpath half. The verdict label travels on the violation (`matchKind`)
|
|
33
|
+
* so a newly-reported subpath edge explains itself.
|
|
34
|
+
*/
|
|
35
|
+
export function matchImportPattern(specifier, pattern, mode = 'package') {
|
|
36
|
+
if (globToRegex(pattern).test(specifier))
|
|
37
|
+
return 'exact';
|
|
38
|
+
if (mode === 'package' && isPackagePattern(pattern) && globToRegex(`${pattern}/**`).test(specifier)) {
|
|
39
|
+
return 'subpath';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Why `pattern` cannot mean what its author wrote in a SPECIFIER list
|
|
45
|
+
* (`forbiddenImports`, `allowedImports`, `exceptions[].target`) matched under
|
|
46
|
+
* `mode` — or `undefined` for a well-formed pattern (round 12, R12-5.2). The
|
|
47
|
+
* one predicate the rule validator (every local AND pack rule file) and the
|
|
48
|
+
* evaluator's dead-unit reach both read.
|
|
49
|
+
*
|
|
50
|
+
* - `''` names no import.
|
|
51
|
+
* - A leading `!` is the NEGATION syntax of every glob list in this repo, but
|
|
52
|
+
* a specifier list takes none: compiled as written it matches only an
|
|
53
|
+
* import that itself starts with `!` (a webpack inline loader), so the
|
|
54
|
+
* carve-out an author meant silently matched nothing. (A literal
|
|
55
|
+
* inline-loader specifier is still reachable: start the pattern with `?`.)
|
|
56
|
+
* - Under package semantics a trailing `/` is the one spelling left literal
|
|
57
|
+
* ({@link isPackagePattern}): it matches only an import written WITH that
|
|
58
|
+
* slash (`'buffer/'`, the userland-polyfill idiom) — never the package, never
|
|
59
|
+
* its subpaths — while the gate reported ✓ over both. `mode: 'exact'` (and
|
|
60
|
+
* `allowedImports`, always exact) keeps it as the literal it then plainly is.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately NOT "can never match": `'buffer/'` is a real import specifier.
|
|
63
|
+
* The trailing-slash fix it names is lossless — the package spelling covers
|
|
64
|
+
* the slash spelling too (`matchImportPattern('buffer/', 'buffer')` is
|
|
65
|
+
* `subpath`), so following the advice never narrows a fence.
|
|
66
|
+
*/
|
|
67
|
+
export function importPatternDefect(pattern, mode = 'package') {
|
|
68
|
+
if (pattern.length === 0)
|
|
69
|
+
return 'an empty pattern names no import';
|
|
70
|
+
if (pattern.startsWith('!')) {
|
|
71
|
+
return ('negation is only supported in `from` — a specifier list takes no "!" (as written this matches only an import that itself starts with "!"). ' +
|
|
72
|
+
'forbiddenImports is checked before allowedImports, so allowedImports never re-admits a forbidden import: carve a subpath out with exceptions[{ path, target, reason }], ' +
|
|
73
|
+
"or set forbiddenMatch: 'exact' and list exactly what you forbid");
|
|
74
|
+
}
|
|
75
|
+
if (mode === 'package' && pattern.endsWith('/')) {
|
|
76
|
+
const bare = pattern.replace(/\/+$/, '');
|
|
77
|
+
const fix = bare.length === 0
|
|
78
|
+
? 'name a package or a path'
|
|
79
|
+
: isPackagePattern(bare)
|
|
80
|
+
? `write '${bare}' (the package and every subpath — '${pattern}' included) or '${bare}/**' (subpaths only)`
|
|
81
|
+
: `write '${bare}'`;
|
|
82
|
+
return `a trailing '/' matches only an import written with that slash, never the package or its subpaths — ${fix}; forbiddenMatch: 'exact' keeps '${pattern}' as a literal`;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Does every specifier `inner` matches also match `outer`? (round 12, R12-5.3 /
|
|
88
|
+
* R12-5.6). `outerMode` is how `outer` matches (the rule's `forbiddenMatch`);
|
|
89
|
+
* `innerMode` is how `inner` does — the same mode for a sibling forbidden
|
|
90
|
+
* pattern, `exact` for an `allowedImports` entry (never widened).
|
|
91
|
+
*
|
|
92
|
+
* Deliberately CONSERVATIVE — `true` only when the literal shape proves it,
|
|
93
|
+
* because a false "covered" would tell an author to delete a live pattern (or
|
|
94
|
+
* call a working allowance dead). Two proofs:
|
|
95
|
+
*
|
|
96
|
+
* 1. `inner` is a literal (no `*` / `?`) that `outer` matches — and, when
|
|
97
|
+
* `inner` itself widens to its subpaths (a package pattern under package
|
|
98
|
+
* semantics), `outer` widens too.
|
|
99
|
+
* 2. `outer` is a package pattern under package semantics, and it matches
|
|
100
|
+
* `inner`'s literal prefix up to one of its `/`: every specifier `inner`
|
|
101
|
+
* can match starts with that prefix plus `/`, a subpath `outer` covers.
|
|
102
|
+
*
|
|
103
|
+
* A pattern with an {@link importPatternDefect} proves nothing either way.
|
|
104
|
+
*/
|
|
105
|
+
export function importPatternSubsumes(outer, inner, outerMode = 'package', innerMode = outerMode) {
|
|
106
|
+
if (importPatternDefect(outer, outerMode) !== undefined)
|
|
107
|
+
return false;
|
|
108
|
+
if (importPatternDefect(inner, innerMode) !== undefined)
|
|
109
|
+
return false;
|
|
110
|
+
const outerWidens = outerMode === 'package' && isPackagePattern(outer);
|
|
111
|
+
const wild = inner.search(/[*?]/);
|
|
112
|
+
if (wild === -1) {
|
|
113
|
+
if (matchImportPattern(inner, outer, outerMode) === null)
|
|
114
|
+
return false;
|
|
115
|
+
const innerWidens = innerMode === 'package' && isPackagePattern(inner);
|
|
116
|
+
return !innerWidens || outerWidens;
|
|
117
|
+
}
|
|
118
|
+
if (!outerWidens)
|
|
119
|
+
return false;
|
|
120
|
+
const prefix = inner.slice(0, wild);
|
|
121
|
+
for (let k = prefix.indexOf('/'); k > 0; k = prefix.indexOf('/', k + 1)) {
|
|
122
|
+
if (matchImportPattern(prefix.slice(0, k), outer, 'package') !== null)
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/** Why the dead-unit judge never calls a relative specifier pattern dead — its went-live evidence and the marker refusal both quote it. */
|
|
128
|
+
export const RELATIVE_PATTERN_NEVER_DEAD = 'a relative pattern is never judged dead (it cannot be judged without an importing file)';
|
|
129
|
+
/** Why the dead-unit judge never calls a leading-`*` specifier pattern dead — its went-live evidence and the marker refusal both quote it. */
|
|
130
|
+
export const LEADING_WILDCARD_NEVER_DEAD = 'a leading wildcard could match any known package name, so it is never judged dead';
|
|
131
|
+
/**
|
|
132
|
+
* Could `pattern` match an import of package `name` (or one of its subpaths)?
|
|
133
|
+
* Deliberately permissive — a false "resolvable" costs a missed warning, a false
|
|
134
|
+
* "dead" would cry wolf on a legitimate guard. The evaluator's reach
|
|
135
|
+
* (`resolvedBy`) and {@link importPatternNeverJudgedDead} both read it.
|
|
136
|
+
*/
|
|
137
|
+
export function couldMatchPackageName(pattern, name, mode) {
|
|
138
|
+
if (matchImportPattern(name, pattern, mode) !== null)
|
|
139
|
+
return true;
|
|
140
|
+
const wild = pattern.search(/[*?]/);
|
|
141
|
+
if (wild === -1)
|
|
142
|
+
return pattern.startsWith(`${name}/`);
|
|
143
|
+
const prefix = pattern.slice(0, wild);
|
|
144
|
+
if (prefix.length === 0) {
|
|
145
|
+
// A leading `*` could be anything. A leading `?` is exactly ONE character:
|
|
146
|
+
// the literal after the `?` run must still fit the name at that offset —
|
|
147
|
+
// so `?!raw-loader!**` (an inline-loader pattern) can never match a package
|
|
148
|
+
// name, and one that matches no import is a dead unit, not "resolvable"
|
|
149
|
+
// through every known package (round 12 review, R12-DOC-1).
|
|
150
|
+
const lead = pattern.length - pattern.replace(/^\?+/, '').length;
|
|
151
|
+
if (lead === 0)
|
|
152
|
+
return true;
|
|
153
|
+
const literal = pattern.slice(lead).split(/[*?]/)[0] ?? '';
|
|
154
|
+
const rest = name.slice(lead);
|
|
155
|
+
return literal.length === 0 || rest.startsWith(literal) || literal.startsWith(`${rest}/`);
|
|
156
|
+
}
|
|
157
|
+
return name.startsWith(prefix) || prefix.startsWith(`${name}/`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Why the boundary dead-unit judge can NEVER call `pattern` dead, matched
|
|
161
|
+
* under `mode` — or `undefined` when it can (round 13, K5). Proved from the
|
|
162
|
+
* pattern alone, through the judge's own reach rules (`resolvedBy` in the
|
|
163
|
+
* evaluator): the judge calls a specifier pattern dead only when it resolves to
|
|
164
|
+
* nothing anywhere, and the orchestrator ALWAYS supplies the runtime builtins
|
|
165
|
+
* as known packages (`nodeBuiltinPackageNames`), so these always resolve:
|
|
166
|
+
*
|
|
167
|
+
* - a relative pattern (`./x`, `../legacy/**`) — never judged without an
|
|
168
|
+
* importing file;
|
|
169
|
+
* - a leading `*` — it could match any known package name, and a builtin is
|
|
170
|
+
* always known;
|
|
171
|
+
* - a pattern a runtime builtin module matches (`fs`, `node:*`,
|
|
172
|
+
* `fs/promises`) — that package always exists.
|
|
173
|
+
*
|
|
174
|
+
* A `{ pattern, expectEmpty: true }` marker on such a pattern could only ever
|
|
175
|
+
* read went-live, so the rule validator refuses it at load. A pattern with an
|
|
176
|
+
* {@link importPatternDefect} is answered by the defect instead (`undefined`).
|
|
177
|
+
*/
|
|
178
|
+
export function importPatternNeverJudgedDead(pattern, mode = 'package') {
|
|
179
|
+
if (importPatternDefect(pattern, mode) !== undefined)
|
|
180
|
+
return undefined;
|
|
181
|
+
if (pattern.startsWith('.'))
|
|
182
|
+
return RELATIVE_PATTERN_NEVER_DEAD;
|
|
183
|
+
if (pattern.startsWith('*'))
|
|
184
|
+
return LEADING_WILDCARD_NEVER_DEAD;
|
|
185
|
+
const builtins = nodeBuiltinPackageNames();
|
|
186
|
+
const builtin = builtins.find((b) => matchImportPattern(b, pattern, mode) !== null) ??
|
|
187
|
+
builtins.find((b) => couldMatchPackageName(pattern, b, mode));
|
|
188
|
+
return builtin !== undefined
|
|
189
|
+
? `'${builtin}' is a runtime builtin module — always a known package — so it always resolves and is never judged dead`
|
|
190
|
+
: undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every module the running runtime ships built in, as an import specifier:
|
|
3
|
+
* each bare name and its `node:` spelling (a name that already carries a
|
|
4
|
+
* scheme — `node:test`, `bun:ffi` — as listed).
|
|
5
|
+
*
|
|
6
|
+
* THE list the boundary plane treats as always-known packages (round 13): the
|
|
7
|
+
* orchestrator's known-package set (`collectKnownPackages`, which the dead-unit
|
|
8
|
+
* judge reads) and the load-time marker refusal (`importPatternNeverJudgedDead`)
|
|
9
|
+
* both read it, so the judge and the refusal can never disagree about whether a
|
|
10
|
+
* builtin-named pattern could ever be judged dead.
|
|
11
|
+
*/
|
|
12
|
+
export declare function nodeBuiltinPackageNames(): readonly string[];
|
|
13
|
+
//# sourceMappingURL=node-builtin-package-names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-builtin-package-names.d.ts","sourceRoot":"","sources":["../../src/scan/node-builtin-package-names.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,IAAI,SAAS,MAAM,EAAE,CAU3D"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { builtinModules } from 'node:module';
|
|
2
|
+
let cached;
|
|
3
|
+
/**
|
|
4
|
+
* Every module the running runtime ships built in, as an import specifier:
|
|
5
|
+
* each bare name and its `node:` spelling (a name that already carries a
|
|
6
|
+
* scheme — `node:test`, `bun:ffi` — as listed).
|
|
7
|
+
*
|
|
8
|
+
* THE list the boundary plane treats as always-known packages (round 13): the
|
|
9
|
+
* orchestrator's known-package set (`collectKnownPackages`, which the dead-unit
|
|
10
|
+
* judge reads) and the load-time marker refusal (`importPatternNeverJudgedDead`)
|
|
11
|
+
* both read it, so the judge and the refusal can never disagree about whether a
|
|
12
|
+
* builtin-named pattern could ever be judged dead.
|
|
13
|
+
*/
|
|
14
|
+
export function nodeBuiltinPackageNames() {
|
|
15
|
+
if (cached === undefined) {
|
|
16
|
+
const out = new Set();
|
|
17
|
+
for (const m of builtinModules) {
|
|
18
|
+
out.add(m);
|
|
19
|
+
if (!m.includes(':'))
|
|
20
|
+
out.add(`node:${m}`);
|
|
21
|
+
}
|
|
22
|
+
cached = [...out];
|
|
23
|
+
}
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import type { IUnreadFile } from '../util/unread-file.js';
|
|
1
2
|
export interface IScanImportsOptions {
|
|
2
3
|
projectRoot: string;
|
|
3
4
|
extraIgnore?: readonly string[];
|
|
4
|
-
/** When set, only files
|
|
5
|
+
/** When set, only files matching one of these globs (project-relative) are scanned. */
|
|
5
6
|
include?: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* Read imports from the RAW text, comments included (`check boundaries
|
|
9
|
+
* --include-comments`). Default `false`: a commented-out import, an import in
|
|
10
|
+
* a doc-comment code fence, or one inside a string literal is not an edge.
|
|
11
|
+
*/
|
|
12
|
+
includeComments?: boolean;
|
|
6
13
|
}
|
|
7
14
|
export interface IImportEdge {
|
|
8
|
-
/** Source file (relative to projectRoot). */
|
|
15
|
+
/** Source file (relative to projectRoot, `/`-separated). */
|
|
9
16
|
from: string;
|
|
10
17
|
/** Literal import specifier. */
|
|
11
18
|
importSpecifier: string;
|
|
12
|
-
/**
|
|
19
|
+
/** 1-based line of the statement's `import` / `export` / `require` keyword. */
|
|
13
20
|
line: number;
|
|
14
21
|
/**
|
|
15
22
|
* Heuristic resolution. v1 sets:
|
|
@@ -18,12 +25,38 @@ export interface IImportEdge {
|
|
|
18
25
|
* (We do not attempt tsconfig path-mapping resolution here.)
|
|
19
26
|
*/
|
|
20
27
|
kind: 'internal' | 'external';
|
|
28
|
+
/** True for `import type` / `export type … from` — still a real dependency edge. */
|
|
29
|
+
typeOnly?: boolean;
|
|
21
30
|
}
|
|
22
31
|
export interface IImportScanResult {
|
|
23
32
|
filesScanned: number;
|
|
24
33
|
edges: IImportEdge[];
|
|
25
34
|
warnings: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Every scanned source file (project-relative, `/`-separated), INCLUDING files
|
|
37
|
+
* with zero imports. A rule's scope is counted against this list, so a glob
|
|
38
|
+
* that matches only import-free files is still live. Optional for callers
|
|
39
|
+
* that hand-build a scan; the evaluator then falls back to the files that
|
|
40
|
+
* appear in `edges` (and says so in its coverage).
|
|
41
|
+
*/
|
|
42
|
+
files?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Source files the walk matched but could NOT read (a stat or read failure —
|
|
45
|
+
* permissions, or a file vanishing mid-scan), with the reason. They are NOT
|
|
46
|
+
* in `files` and NOT counted in `filesScanned`: a file whose imports were
|
|
47
|
+
* never read was not scanned. `runBoundaryCheck` folds each one into the
|
|
48
|
+
* coverage of every rule whose scope it is in (`readScopeCoverage`), so a
|
|
49
|
+
* rule over an unreadable file settles PARTIAL (2) — never "examined 1 of 1"
|
|
50
|
+
* over the file that held the violation.
|
|
51
|
+
*/
|
|
52
|
+
unread?: IUnreadFile[];
|
|
53
|
+
/** Every `package.json` the walk passed (project-relative) — workspace package names. */
|
|
54
|
+
manifestFiles?: string[];
|
|
55
|
+
/** Which text the edges were read from. */
|
|
56
|
+
zone?: 'code' | 'all';
|
|
26
57
|
}
|
|
58
|
+
/** Drop every memoised file (tests, or a caller that rewrote files in place within one tick). */
|
|
59
|
+
export declare function clearImportScanMemo(): void;
|
|
27
60
|
/**
|
|
28
61
|
* Walk the project root and return every detected import edge.
|
|
29
62
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scan-imports.d.ts","sourceRoot":"","sources":["../../src/scan/scan-imports.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAmB1D,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,uFAAuF;IACvF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACvB;AA8ED,iGAAiG;AACjG,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAyBD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,iBAAiB,CAyE3E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,SAAS;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CAmB7E"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import * as nodePath from 'node:path';
|
|
3
|
+
import { parseImportStatements } from "../extract/parse-imports.js";
|
|
4
|
+
import { UnreadFileReason } from "../util/unread-file-reason.js";
|
|
5
|
+
import { globMayMatchUnder, matchesAny } from "./glob.js";
|
|
3
6
|
const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
4
7
|
const DEFAULT_IGNORE = new Set([
|
|
5
8
|
'node_modules',
|
|
@@ -13,18 +16,6 @@ const DEFAULT_IGNORE = new Set([
|
|
|
13
16
|
'.tmp-pack',
|
|
14
17
|
'.tmp-smoke-consumer.txt',
|
|
15
18
|
]);
|
|
16
|
-
// Match `import ... from 'x'`, `export ... from 'x'`, `require('x')`,
|
|
17
|
-
// `import('x')` (static and dynamic). Captures the specifier as group 1.
|
|
18
|
-
//
|
|
19
|
-
// We DELIBERATELY use a single regex per kind rather than a real parser —
|
|
20
|
-
// boundary rules need stable behavior across syntaxes and a regex scanner is
|
|
21
|
-
// the simplest thing that works for v1. Comments and string escapes can fool
|
|
22
|
-
// it; we filter the lowest-hanging fruit (single-line // and /* */ stripped
|
|
23
|
-
// per line, but a `// import "x"` line is still ignored).
|
|
24
|
-
const IMPORT_RE = /(?:^|\s)(?:import|export)\s+[^'"`]*?from\s+['"]([^'"`]+)['"]/g;
|
|
25
|
-
const SIDE_EFFECT_IMPORT_RE = /(?:^|\s)import\s+['"]([^'"`]+)['"]/g;
|
|
26
|
-
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
|
|
27
|
-
const REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"`]+)['"]\s*\)/g;
|
|
28
19
|
function isIgnored(name, extraIgnore) {
|
|
29
20
|
if (DEFAULT_IGNORE.has(name))
|
|
30
21
|
return true;
|
|
@@ -39,7 +30,13 @@ function* walk(root, current, extraIgnore) {
|
|
|
39
30
|
try {
|
|
40
31
|
entries = readdirSync(current, { withFileTypes: true });
|
|
41
32
|
}
|
|
42
|
-
catch {
|
|
33
|
+
catch (e) {
|
|
34
|
+
// A directory that vanished mid-walk is out of scope; one that could not
|
|
35
|
+
// be listed (permissions) is in front of every rule that reaches under it.
|
|
36
|
+
const code = e.code;
|
|
37
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
38
|
+
yield { full: current, manifest: false, unlistable: e.message };
|
|
39
|
+
}
|
|
43
40
|
return;
|
|
44
41
|
}
|
|
45
42
|
for (const entry of entries) {
|
|
@@ -59,38 +56,55 @@ function* walk(root, current, extraIgnore) {
|
|
|
59
56
|
continue;
|
|
60
57
|
}
|
|
61
58
|
if (entry.isFile()) {
|
|
59
|
+
if (name === 'package.json') {
|
|
60
|
+
yield { full, manifest: true };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
62
63
|
const ext = nodePath.extname(name);
|
|
63
64
|
if (!SUPPORTED_EXTS.has(ext))
|
|
64
65
|
continue;
|
|
65
|
-
yield full;
|
|
66
|
+
yield { full, manifest: false };
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Per-process memo of the edges ONE file contributes, keyed on scan root + file
|
|
72
|
+
* + zone and validated by a stat fingerprint (size, mtime, ctime).
|
|
73
|
+
*
|
|
74
|
+
* `scanImports` runs several times per command — the architecture map calls it
|
|
75
|
+
* twice and impact analysis once for a single task-risk report — and for the
|
|
76
|
+
* whole life of the MCP server. Round 11's comment-aware parse (lex → blank
|
|
77
|
+
* comments → match) made re-reading and re-lexing every unchanged file on every
|
|
78
|
+
* call the dominant cost of those reports (~3x). An edited file changes its
|
|
79
|
+
* fingerprint and is re-parsed; an unchanged one is not. Edges are handed out
|
|
80
|
+
* as COPIES so a caller that decorates an edge can never corrupt the memo.
|
|
81
|
+
*/
|
|
82
|
+
const EDGE_MEMO = new Map();
|
|
83
|
+
const EDGE_MEMO_LIMIT = 50_000;
|
|
84
|
+
/** Drop every memoised file (tests, or a caller that rewrote files in place within one tick). */
|
|
85
|
+
export function clearImportScanMemo() {
|
|
86
|
+
EDGE_MEMO.clear();
|
|
76
87
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
function toPosix(rel) {
|
|
89
|
+
return nodePath.sep === '/' ? rel : rel.split(nodePath.sep).join('/');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The edges one file contributes — read through THE import parser
|
|
93
|
+
* (`parseImportStatements`), so `check boundaries` and the `import-edges` DSL
|
|
94
|
+
* extractor can never disagree about what a file imports. (Round 11 deleted the
|
|
95
|
+
* four private regexes that used to live here, together with a comment that
|
|
96
|
+
* claimed they stripped comments: no stripping code existed, a commented-out
|
|
97
|
+
* import was a violation, and every import after line 1 was reported one line
|
|
98
|
+
* early.)
|
|
99
|
+
*/
|
|
100
|
+
function extractImports(source, relPath, zone) {
|
|
101
|
+
return parseImportStatements(source, { zone }).map((p) => ({
|
|
102
|
+
from: relPath,
|
|
103
|
+
importSpecifier: p.specifier,
|
|
104
|
+
line: p.line,
|
|
105
|
+
kind: p.specifier.startsWith('.') ? 'internal' : 'external',
|
|
106
|
+
...(p.typeOnly ? { typeOnly: true } : {}),
|
|
107
|
+
}));
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
110
|
* Walk the project root and return every detected import edge.
|
|
@@ -98,27 +112,82 @@ function extractImports(source, relPath) {
|
|
|
98
112
|
export function scanImports(options) {
|
|
99
113
|
const root = nodePath.resolve(options.projectRoot);
|
|
100
114
|
const extraIgnore = new Set(options.extraIgnore ?? []);
|
|
115
|
+
const zone = options.includeComments === true ? 'all' : 'code';
|
|
116
|
+
const include = options.include && options.include.length > 0 ? options.include : undefined;
|
|
101
117
|
const result = {
|
|
102
118
|
filesScanned: 0,
|
|
103
119
|
edges: [],
|
|
104
120
|
warnings: [],
|
|
121
|
+
files: [],
|
|
122
|
+
unread: [],
|
|
123
|
+
manifestFiles: [],
|
|
124
|
+
zone,
|
|
105
125
|
};
|
|
106
126
|
if (!existsSync(root)) {
|
|
107
127
|
result.warnings.push(`scan root does not exist: ${root}`);
|
|
108
128
|
return result;
|
|
109
129
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
// A file counts as SCANNED only once its imports were actually read (or
|
|
131
|
+
// served from the memo). It used to be counted — and listed in `files`, the
|
|
132
|
+
// universe every rule's scope is measured against — before the read, so an
|
|
133
|
+
// unreadable file read as "examined 1 of 1" while its imports were never seen.
|
|
134
|
+
const unreadable = (rel, e) => {
|
|
135
|
+
result.warnings.push(`unreadable: ${rel} (${e.message})`);
|
|
136
|
+
result.unread.push({ path: rel, reason: UnreadFileReason.Unreadable });
|
|
137
|
+
};
|
|
138
|
+
for (const hit of walk(root, root, extraIgnore)) {
|
|
139
|
+
const rel = toPosix(nodePath.relative(root, hit.full));
|
|
140
|
+
if (hit.unlistable !== undefined) {
|
|
141
|
+
const dir = rel === '' ? './' : `${rel}/`;
|
|
142
|
+
if (include && !include.some((g) => globMayMatchUnder(g, dir)))
|
|
143
|
+
continue;
|
|
144
|
+
result.warnings.push(`unlistable directory: ${dir} (${hit.unlistable})`);
|
|
145
|
+
result.unread.push({ path: dir, reason: UnreadFileReason.UnreadableDirectory });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (hit.manifest) {
|
|
149
|
+
result.manifestFiles.push(rel);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (include && !matchesAny(rel, include))
|
|
153
|
+
continue;
|
|
154
|
+
let fingerprint;
|
|
155
|
+
try {
|
|
156
|
+
const st = statSync(hit.full);
|
|
157
|
+
fingerprint = `${st.size}:${st.mtimeMs}:${st.ctimeMs}`;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
// Deleted since the walk saw it: gone, so out of scope (the one reader's
|
|
161
|
+
// rule, `readMatchingFiles`). Anything else leaves it unexamined.
|
|
162
|
+
if (e.code !== 'ENOENT')
|
|
163
|
+
unreadable(rel, e);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const key = `${root}\0${hit.full}\0${zone}`;
|
|
167
|
+
const memo = EDGE_MEMO.get(key);
|
|
168
|
+
if (memo && memo.fingerprint === fingerprint) {
|
|
169
|
+
result.filesScanned += 1;
|
|
170
|
+
result.files.push(rel);
|
|
171
|
+
for (const e of memo.edges)
|
|
172
|
+
result.edges.push({ ...e });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
113
175
|
let source;
|
|
114
176
|
try {
|
|
115
|
-
source = readFileSync(
|
|
177
|
+
source = readFileSync(hit.full, 'utf8');
|
|
116
178
|
}
|
|
117
179
|
catch (e) {
|
|
118
|
-
|
|
180
|
+
unreadable(rel, e);
|
|
119
181
|
continue;
|
|
120
182
|
}
|
|
121
|
-
result.
|
|
183
|
+
result.filesScanned += 1;
|
|
184
|
+
result.files.push(rel);
|
|
185
|
+
const edges = extractImports(source, rel, zone);
|
|
186
|
+
if (EDGE_MEMO.size >= EDGE_MEMO_LIMIT)
|
|
187
|
+
EDGE_MEMO.clear();
|
|
188
|
+
EDGE_MEMO.set(key, { fingerprint, edges });
|
|
189
|
+
for (const e of edges)
|
|
190
|
+
result.edges.push({ ...e });
|
|
122
191
|
}
|
|
123
192
|
return result;
|
|
124
193
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One blank-run backtracking hazard found in a regex source by
|
|
3
|
+
* `findBlankRunHazards`.
|
|
4
|
+
*
|
|
5
|
+
* `leading`: an alternative can START a match at every offset of a blank run
|
|
6
|
+
* and then consume the rest of the run — O(run²) per blanked comment/string.
|
|
7
|
+
* `adjacent`: two whitespace-consuming quantifiers touch (only optional items
|
|
8
|
+
* between them), so the engine re-partitions every run between them.
|
|
9
|
+
*/
|
|
10
|
+
export interface IBlankRunHazard {
|
|
11
|
+
readonly shape: 'leading' | 'adjacent';
|
|
12
|
+
/** Offset of the offending construct in the regex source. */
|
|
13
|
+
readonly index: number;
|
|
14
|
+
/** The offending slice of the source. */
|
|
15
|
+
readonly fragment: string;
|
|
16
|
+
/** One sentence: what backtracks, and why a blanked buffer makes it expensive. */
|
|
17
|
+
readonly message: string;
|
|
18
|
+
/**
|
|
19
|
+
* The hazard's REACH: whether the flagged quantifier(s) consume newlines.
|
|
20
|
+
*
|
|
21
|
+
* A line-bounded hazard (`[ \t]*`, `.*?`, `[ ]*` — or an `adjacent` pair
|
|
22
|
+
* overlapping only on spaces) re-scans at most to the end of its line, so
|
|
23
|
+
* on a blanked buffer it costs Σ line-run², not Σ (multi-line run)². A
|
|
24
|
+
* cost prediction that ignores this skips files the pattern would scan in
|
|
25
|
+
* milliseconds.
|
|
26
|
+
*/
|
|
27
|
+
readonly crossesNewline: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* `leading` only: a match can start only at a LINE start (`(?:^|\n)\s*`),
|
|
30
|
+
* so the cost is (lines × run), not run².
|
|
31
|
+
*/
|
|
32
|
+
readonly fromLineStart?: boolean;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=blank-run-hazard-finding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blank-run-hazard-finding.d.ts","sourceRoot":"","sources":["../../src/util/blank-run-hazard-finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACvC,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IBlankRunHazard } from './blank-run-hazard-finding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Static lint: does this regex backtrack quadratically on a BLANKED buffer?
|
|
4
|
+
*
|
|
5
|
+
* A non-`all` `scan` zone blanks comments and strings into runs of spaces
|
|
6
|
+
* (newlines kept, so line numbers stay true). A pattern that is fast on real
|
|
7
|
+
* source becomes O(run²) on such a buffer when:
|
|
8
|
+
*
|
|
9
|
+
* - (leading) an alternative's first mandatory construct — after only
|
|
10
|
+
* optional groups or non-restricting zero-width atoms — is a
|
|
11
|
+
* whitespace-consuming quantifier. Every offset of a run is a start, and
|
|
12
|
+
* each start re-scans the rest of the run. A start restricted to newlines
|
|
13
|
+
* (`(?:^|\n)`) is only a hazard when the quantifier also crosses newlines
|
|
14
|
+
* (`\s*`); `[ \t]*` then stops at the end of its line.
|
|
15
|
+
* - (adjacent) two whitespace-consuming quantifiers touch, separated only by
|
|
16
|
+
* optional items or whitespace-capable atoms — `\s*(?:<x>)?\s*=`, a lazy
|
|
17
|
+
* `[^;'"]*?` next to `\s*` — so the engine re-partitions each run between
|
|
18
|
+
* them.
|
|
19
|
+
*
|
|
20
|
+
* Measured: one such pattern took 3 ms on raw text and 5,402 ms on the
|
|
21
|
+
* blanked buffer of the same file. This cannot be caught by tests over
|
|
22
|
+
* ordinary source; it is caught here, from the pattern text, before it runs.
|
|
23
|
+
*
|
|
24
|
+
* Deterministic and advisory: a small tokenizer over the regex source, no
|
|
25
|
+
* regex engine involved. It over-approximates (lookarounds are treated as
|
|
26
|
+
* transparent; flags are ignored), which is the right direction for a hint.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findBlankRunHazards(source: string): readonly IBlankRunHazard[];
|
|
29
|
+
//# sourceMappingURL=blank-run-hazard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blank-run-hazard.d.ts","sourceRoot":"","sources":["../../src/util/blank-run-hazard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE,CAY9E"}
|