@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,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static lint: does this regex backtrack quadratically on a BLANKED buffer?
|
|
3
|
+
*
|
|
4
|
+
* A non-`all` `scan` zone blanks comments and strings into runs of spaces
|
|
5
|
+
* (newlines kept, so line numbers stay true). A pattern that is fast on real
|
|
6
|
+
* source becomes O(run²) on such a buffer when:
|
|
7
|
+
*
|
|
8
|
+
* - (leading) an alternative's first mandatory construct — after only
|
|
9
|
+
* optional groups or non-restricting zero-width atoms — is a
|
|
10
|
+
* whitespace-consuming quantifier. Every offset of a run is a start, and
|
|
11
|
+
* each start re-scans the rest of the run. A start restricted to newlines
|
|
12
|
+
* (`(?:^|\n)`) is only a hazard when the quantifier also crosses newlines
|
|
13
|
+
* (`\s*`); `[ \t]*` then stops at the end of its line.
|
|
14
|
+
* - (adjacent) two whitespace-consuming quantifiers touch, separated only by
|
|
15
|
+
* optional items or whitespace-capable atoms — `\s*(?:<x>)?\s*=`, a lazy
|
|
16
|
+
* `[^;'"]*?` next to `\s*` — so the engine re-partitions each run between
|
|
17
|
+
* them.
|
|
18
|
+
*
|
|
19
|
+
* Measured: one such pattern took 3 ms on raw text and 5,402 ms on the
|
|
20
|
+
* blanked buffer of the same file. This cannot be caught by tests over
|
|
21
|
+
* ordinary source; it is caught here, from the pattern text, before it runs.
|
|
22
|
+
*
|
|
23
|
+
* Deterministic and advisory: a small tokenizer over the regex source, no
|
|
24
|
+
* regex engine involved. It over-approximates (lookarounds are treated as
|
|
25
|
+
* transparent; flags are ignored), which is the right direction for a hint.
|
|
26
|
+
*/
|
|
27
|
+
export function findBlankRunHazards(source) {
|
|
28
|
+
const { alts } = parseAlternatives(source, 0);
|
|
29
|
+
const found = new Map();
|
|
30
|
+
const report = (h) => {
|
|
31
|
+
const key = `${h.shape}@${h.index}`;
|
|
32
|
+
const prev = found.get(key);
|
|
33
|
+
// A repeated group is analysed twice; keep the WIDEST reach seen, so a
|
|
34
|
+
// newline-crossing hazard is never under-reported as line-bounded.
|
|
35
|
+
if (!prev || (h.crossesNewline && !prev.crossesNewline))
|
|
36
|
+
found.set(key, h);
|
|
37
|
+
};
|
|
38
|
+
for (const alt of alts)
|
|
39
|
+
analyzeSeq(source, alt, { start: START_ANYWHERE, pending: null }, report);
|
|
40
|
+
return [...found.values()].sort((a, b) => a.index - b.index || a.shape.localeCompare(b.shape));
|
|
41
|
+
}
|
|
42
|
+
const NONE = { space: false, newline: false };
|
|
43
|
+
const BOTH = { space: true, newline: true };
|
|
44
|
+
/** A quantifier whose upper bound exceeds this is treated as unbounded. */
|
|
45
|
+
const UNBOUNDED_ABOVE = 256;
|
|
46
|
+
/** Where a match may still START inside a blank run, given what the pattern consumed so far. */
|
|
47
|
+
const START_RESTRICTED = 0;
|
|
48
|
+
const START_LINE = 1;
|
|
49
|
+
const START_ANYWHERE = 2;
|
|
50
|
+
// ── parser ──────────────────────────────────────────────────────────────────
|
|
51
|
+
function parseAlternatives(src, from) {
|
|
52
|
+
const alts = [[]];
|
|
53
|
+
let i = from;
|
|
54
|
+
while (i < src.length) {
|
|
55
|
+
const ch = src[i];
|
|
56
|
+
if (ch === ')')
|
|
57
|
+
break;
|
|
58
|
+
if (ch === '|') {
|
|
59
|
+
alts.push([]);
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const item = parseItem(src, i);
|
|
64
|
+
i = item.i;
|
|
65
|
+
let node = item.node;
|
|
66
|
+
const q = parseQuantifier(src, i);
|
|
67
|
+
if (q) {
|
|
68
|
+
i = q.i;
|
|
69
|
+
if (node.t === 'atom' || node.t === 'group')
|
|
70
|
+
node = { ...node, min: q.min, max: q.max, end: i };
|
|
71
|
+
}
|
|
72
|
+
alts[alts.length - 1].push(node);
|
|
73
|
+
}
|
|
74
|
+
return { alts, i };
|
|
75
|
+
}
|
|
76
|
+
function parseQuantifier(src, i) {
|
|
77
|
+
const ch = src[i];
|
|
78
|
+
let min;
|
|
79
|
+
let max;
|
|
80
|
+
let j;
|
|
81
|
+
if (ch === '*')
|
|
82
|
+
[min, max, j] = [0, Infinity, i + 1];
|
|
83
|
+
else if (ch === '+')
|
|
84
|
+
[min, max, j] = [1, Infinity, i + 1];
|
|
85
|
+
else if (ch === '?')
|
|
86
|
+
[min, max, j] = [0, 1, i + 1];
|
|
87
|
+
else if (ch === '{') {
|
|
88
|
+
const m = /^\{(\d+)(?:(,)(\d*))?\}/.exec(src.slice(i));
|
|
89
|
+
if (!m)
|
|
90
|
+
return undefined; // a literal `{`
|
|
91
|
+
min = Number(m[1]);
|
|
92
|
+
max = m[2] === undefined ? min : m[3] === '' || m[3] === undefined ? Infinity : Number(m[3]);
|
|
93
|
+
j = i + m[0].length;
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
return undefined;
|
|
97
|
+
if (src[j] === '?')
|
|
98
|
+
j += 1; // lazy — same set of partitions, different order
|
|
99
|
+
return { min, max, i: j };
|
|
100
|
+
}
|
|
101
|
+
function parseItem(src, i) {
|
|
102
|
+
const ch = src[i];
|
|
103
|
+
if (ch === '(')
|
|
104
|
+
return parseGroup(src, i);
|
|
105
|
+
if (ch === '[') {
|
|
106
|
+
const cls = parseClass(src, i);
|
|
107
|
+
return { node: atom(cls.chars, i, cls.i), i: cls.i };
|
|
108
|
+
}
|
|
109
|
+
if (ch === '\\') {
|
|
110
|
+
const esc = parseEscape(src, i, false);
|
|
111
|
+
if (esc.anchor !== undefined) {
|
|
112
|
+
return { node: { t: 'anchor', restricting: esc.anchor, start: i, end: esc.i }, i: esc.i };
|
|
113
|
+
}
|
|
114
|
+
return { node: atom(esc.chars, i, esc.i), i: esc.i };
|
|
115
|
+
}
|
|
116
|
+
if (ch === '^' || ch === '$')
|
|
117
|
+
return { node: { t: 'anchor', restricting: true, start: i, end: i + 1 }, i: i + 1 };
|
|
118
|
+
if (ch === '.')
|
|
119
|
+
return { node: atom({ space: true, newline: false }, i, i + 1), i: i + 1 };
|
|
120
|
+
return { node: atom(charOf(ch.charCodeAt(0)), i, i + 1), i: i + 1 };
|
|
121
|
+
}
|
|
122
|
+
function atom(chars, start, end) {
|
|
123
|
+
return { t: 'atom', chars, start, end, min: 1, max: 1 };
|
|
124
|
+
}
|
|
125
|
+
function parseGroup(src, i) {
|
|
126
|
+
let j = i + 1;
|
|
127
|
+
let look = false;
|
|
128
|
+
if (src[j] === '?') {
|
|
129
|
+
const next = src[j + 1];
|
|
130
|
+
if (next === ':')
|
|
131
|
+
j += 2;
|
|
132
|
+
else if (next === '=' || next === '!') {
|
|
133
|
+
look = true;
|
|
134
|
+
j += 2;
|
|
135
|
+
}
|
|
136
|
+
else if (next === '<' && (src[j + 2] === '=' || src[j + 2] === '!')) {
|
|
137
|
+
look = true;
|
|
138
|
+
j += 3;
|
|
139
|
+
}
|
|
140
|
+
else if (next === '<') {
|
|
141
|
+
const close = src.indexOf('>', j + 2);
|
|
142
|
+
j = close === -1 ? src.length : close + 1;
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
j += 1;
|
|
146
|
+
}
|
|
147
|
+
const inner = parseAlternatives(src, j);
|
|
148
|
+
const end = inner.i < src.length ? inner.i + 1 : inner.i;
|
|
149
|
+
if (look)
|
|
150
|
+
return { node: { t: 'look', alts: inner.alts, start: i, end }, i: end };
|
|
151
|
+
return { node: { t: 'group', alts: inner.alts, start: i, end, min: 1, max: 1 }, i: end };
|
|
152
|
+
}
|
|
153
|
+
/** A character class: which blank-run characters it matches. */
|
|
154
|
+
function parseClass(src, i) {
|
|
155
|
+
let j = i + 1;
|
|
156
|
+
const negated = src[j] === '^';
|
|
157
|
+
if (negated)
|
|
158
|
+
j += 1;
|
|
159
|
+
let space = false;
|
|
160
|
+
let newline = false;
|
|
161
|
+
let first = true;
|
|
162
|
+
let prevCode;
|
|
163
|
+
while (j < src.length) {
|
|
164
|
+
const ch = src[j];
|
|
165
|
+
if (ch === ']' && !(first && false))
|
|
166
|
+
break;
|
|
167
|
+
first = false;
|
|
168
|
+
if (ch === '-' && prevCode !== undefined && src[j + 1] !== undefined && src[j + 1] !== ']') {
|
|
169
|
+
// A range `a-b`: covers a blank char when it spans it.
|
|
170
|
+
const hi = src[j + 1] === '\\' ? parseEscape(src, j + 1, true) : undefined;
|
|
171
|
+
const hiCode = hi?.code ?? src.charCodeAt(j + 1);
|
|
172
|
+
if (prevCode <= 0x20 && hiCode >= 0x20)
|
|
173
|
+
space = true;
|
|
174
|
+
if (prevCode <= 0x0a && hiCode >= 0x0a)
|
|
175
|
+
newline = true;
|
|
176
|
+
j = hi ? hi.i : j + 2;
|
|
177
|
+
prevCode = undefined;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === '\\') {
|
|
181
|
+
const esc = parseEscape(src, j, true);
|
|
182
|
+
if (esc.chars.space)
|
|
183
|
+
space = true;
|
|
184
|
+
if (esc.chars.newline)
|
|
185
|
+
newline = true;
|
|
186
|
+
prevCode = esc.code;
|
|
187
|
+
j = esc.i;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const code = ch.charCodeAt(0);
|
|
191
|
+
if (code === 0x20)
|
|
192
|
+
space = true;
|
|
193
|
+
if (code === 0x0a)
|
|
194
|
+
newline = true;
|
|
195
|
+
prevCode = code;
|
|
196
|
+
j += 1;
|
|
197
|
+
}
|
|
198
|
+
const end = j < src.length ? j + 1 : j;
|
|
199
|
+
// `[]` matches nothing; `[^]` matches everything — both fall out of the rule.
|
|
200
|
+
return { chars: negated ? { space: !space, newline: !newline } : { space, newline }, i: end };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One escape. `anchor` is set for `\b` (restricting) / `\B` (not) outside a
|
|
204
|
+
* class; `code` for a single-character escape (so a class range can use it).
|
|
205
|
+
*/
|
|
206
|
+
function parseEscape(src, i, inClass) {
|
|
207
|
+
const c = src[i + 1];
|
|
208
|
+
if (c === undefined)
|
|
209
|
+
return { chars: NONE, i: i + 1 };
|
|
210
|
+
const j = i + 2;
|
|
211
|
+
switch (c) {
|
|
212
|
+
case 's':
|
|
213
|
+
case 'D':
|
|
214
|
+
case 'W':
|
|
215
|
+
return { chars: BOTH, i: j };
|
|
216
|
+
case 'S':
|
|
217
|
+
case 'd':
|
|
218
|
+
case 'w':
|
|
219
|
+
return { chars: NONE, i: j };
|
|
220
|
+
case 'b':
|
|
221
|
+
return inClass ? { chars: NONE, i: j, code: 8 } : { chars: NONE, i: j, anchor: true };
|
|
222
|
+
case 'B':
|
|
223
|
+
return { chars: NONE, i: j, anchor: false };
|
|
224
|
+
case 'n':
|
|
225
|
+
return { chars: charOf(0x0a), i: j, code: 0x0a };
|
|
226
|
+
case 'r':
|
|
227
|
+
return { chars: NONE, i: j, code: 0x0d };
|
|
228
|
+
case 't':
|
|
229
|
+
return { chars: NONE, i: j, code: 0x09 };
|
|
230
|
+
case 'f':
|
|
231
|
+
return { chars: NONE, i: j, code: 0x0c };
|
|
232
|
+
case 'v':
|
|
233
|
+
return { chars: NONE, i: j, code: 0x0b };
|
|
234
|
+
case '0':
|
|
235
|
+
return { chars: NONE, i: j, code: 0 };
|
|
236
|
+
case 'x': {
|
|
237
|
+
const m = /^[0-9a-fA-F]{2}/.exec(src.slice(j));
|
|
238
|
+
if (!m)
|
|
239
|
+
return { chars: NONE, i: j };
|
|
240
|
+
const code = parseInt(m[0], 16);
|
|
241
|
+
return { chars: charOf(code), i: j + 2, code };
|
|
242
|
+
}
|
|
243
|
+
case 'u': {
|
|
244
|
+
const braced = /^\{([0-9a-fA-F]+)\}/.exec(src.slice(j));
|
|
245
|
+
const plain = /^[0-9a-fA-F]{4}/.exec(src.slice(j));
|
|
246
|
+
const hex = braced?.[1] ?? plain?.[0];
|
|
247
|
+
if (hex === undefined)
|
|
248
|
+
return { chars: NONE, i: j };
|
|
249
|
+
const code = parseInt(hex, 16);
|
|
250
|
+
return { chars: charOf(code), i: j + (braced ? braced[0].length : 4), code };
|
|
251
|
+
}
|
|
252
|
+
case 'c':
|
|
253
|
+
return { chars: NONE, i: j + 1 };
|
|
254
|
+
case 'k': {
|
|
255
|
+
const close = src.indexOf('>', j);
|
|
256
|
+
return { chars: NONE, i: close === -1 ? src.length : close + 1 };
|
|
257
|
+
}
|
|
258
|
+
case 'p':
|
|
259
|
+
case 'P': {
|
|
260
|
+
const m = /^\{([^}]*)\}/.exec(src.slice(j));
|
|
261
|
+
const name = m?.[1] ?? '';
|
|
262
|
+
const end = m ? j + m[0].length : j;
|
|
263
|
+
if (c === 'P')
|
|
264
|
+
return { chars: BOTH, i: end };
|
|
265
|
+
if (/^(White_Space|space)$/i.test(name))
|
|
266
|
+
return { chars: BOTH, i: end };
|
|
267
|
+
if (/^(Z|Zs|Separator|Space_Separator)$/i.test(name))
|
|
268
|
+
return { chars: { space: true, newline: false }, i: end };
|
|
269
|
+
return { chars: NONE, i: end };
|
|
270
|
+
}
|
|
271
|
+
default:
|
|
272
|
+
if (c >= '1' && c <= '9' && !inClass) {
|
|
273
|
+
// A backreference — what it matches is not known statically.
|
|
274
|
+
let k = j;
|
|
275
|
+
while (k < src.length && src[k] >= '0' && src[k] <= '9')
|
|
276
|
+
k += 1;
|
|
277
|
+
return { chars: NONE, i: k };
|
|
278
|
+
}
|
|
279
|
+
return { chars: charOf(c.charCodeAt(0)), i: j, code: c.charCodeAt(0) };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function charOf(code) {
|
|
283
|
+
return { space: code === 0x20, newline: code === 0x0a };
|
|
284
|
+
}
|
|
285
|
+
// ── analysis ────────────────────────────────────────────────────────────────
|
|
286
|
+
function wsCapable(c) {
|
|
287
|
+
return c.space || c.newline;
|
|
288
|
+
}
|
|
289
|
+
function overlaps(a, b) {
|
|
290
|
+
return (a.space && b.space) || (a.newline && b.newline);
|
|
291
|
+
}
|
|
292
|
+
function unionChars(a, b) {
|
|
293
|
+
return { space: a.space || b.space, newline: a.newline || b.newline };
|
|
294
|
+
}
|
|
295
|
+
function joinPending(a, b) {
|
|
296
|
+
if (a === null)
|
|
297
|
+
return b;
|
|
298
|
+
if (b === null)
|
|
299
|
+
return a;
|
|
300
|
+
return { chars: unionChars(a.chars, b.chars), at: Math.min(a.at, b.at) };
|
|
301
|
+
}
|
|
302
|
+
function join(a, b) {
|
|
303
|
+
return { start: Math.max(a.start, b.start), pending: joinPending(a.pending, b.pending) };
|
|
304
|
+
}
|
|
305
|
+
/** State after consuming ONE mandatory character-matching item that is not an unbounded ws quantifier. */
|
|
306
|
+
function afterConsume(entry, chars) {
|
|
307
|
+
const start = entry.start === START_RESTRICTED
|
|
308
|
+
? START_RESTRICTED
|
|
309
|
+
: chars.space
|
|
310
|
+
? entry.start
|
|
311
|
+
: chars.newline
|
|
312
|
+
? START_LINE
|
|
313
|
+
: START_RESTRICTED;
|
|
314
|
+
// A mandatory character that can itself be blank does NOT separate two
|
|
315
|
+
// whitespace quantifiers (`\s*\n\s*` still re-partitions the run).
|
|
316
|
+
return { start, pending: wsCapable(chars) ? entry.pending : null };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The blank chars a repeated group can consume when its body is made only of
|
|
320
|
+
* whitespace-capable atoms (and optional / non-restricting items) — i.e. when
|
|
321
|
+
* `(?:\s)+` behaves exactly like `\s+`. `undefined` otherwise.
|
|
322
|
+
*/
|
|
323
|
+
function groupWsChars(alts) {
|
|
324
|
+
let acc;
|
|
325
|
+
for (const alt of alts) {
|
|
326
|
+
let sawWs = false;
|
|
327
|
+
let pure = true;
|
|
328
|
+
let chars = NONE;
|
|
329
|
+
for (const n of alt) {
|
|
330
|
+
if (n.t === 'anchor') {
|
|
331
|
+
if (n.restricting)
|
|
332
|
+
pure = false;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (n.t === 'look')
|
|
336
|
+
continue;
|
|
337
|
+
if (n.t === 'atom') {
|
|
338
|
+
if (wsCapable(n.chars)) {
|
|
339
|
+
sawWs = true;
|
|
340
|
+
chars = unionChars(chars, n.chars);
|
|
341
|
+
}
|
|
342
|
+
else if (n.min > 0)
|
|
343
|
+
pure = false;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const inner = groupWsChars(n.alts);
|
|
347
|
+
if (inner) {
|
|
348
|
+
sawWs = true;
|
|
349
|
+
chars = unionChars(chars, inner);
|
|
350
|
+
}
|
|
351
|
+
else if (n.min > 0)
|
|
352
|
+
pure = false;
|
|
353
|
+
}
|
|
354
|
+
if (pure && sawWs)
|
|
355
|
+
acc = acc ? unionChars(acc, chars) : chars;
|
|
356
|
+
}
|
|
357
|
+
return acc;
|
|
358
|
+
}
|
|
359
|
+
function describe(src, start, end) {
|
|
360
|
+
return src.slice(start, end);
|
|
361
|
+
}
|
|
362
|
+
/** A whitespace-consuming unbounded quantifier — the construct every hazard is made of. */
|
|
363
|
+
function onWsQuantifier(src, chars, start, end, min, entry, report) {
|
|
364
|
+
const frag = describe(src, start, end);
|
|
365
|
+
let startMode = entry.start;
|
|
366
|
+
if (entry.start === START_ANYWHERE || (entry.start === START_LINE && chars.newline)) {
|
|
367
|
+
report({
|
|
368
|
+
shape: 'leading',
|
|
369
|
+
index: start,
|
|
370
|
+
fragment: frag,
|
|
371
|
+
message: entry.start !== START_ANYWHERE
|
|
372
|
+
? `\`${frag}\` runs from every line start of a blank run across the lines below it — O(lines × run) per blanked comment`
|
|
373
|
+
: chars.newline
|
|
374
|
+
? `\`${frag}\` can start a match at every offset of a blank run and re-scan the rest of it — O(run²) per blanked comment or string`
|
|
375
|
+
: `\`${frag}\` can start a match at every offset of a blanked line and re-scan the rest of that line — O(line²) per blanked line`,
|
|
376
|
+
// Reach: a quantifier that cannot consume `\n` stops at the end of its
|
|
377
|
+
// line, so its cost on a blanked buffer is per LINE, not per run.
|
|
378
|
+
crossesNewline: chars.newline,
|
|
379
|
+
...(entry.start === START_LINE ? { fromLineStart: true } : {}),
|
|
380
|
+
});
|
|
381
|
+
// One leading finding per alternative is enough — later constructs are
|
|
382
|
+
// reached only through it.
|
|
383
|
+
startMode = START_RESTRICTED;
|
|
384
|
+
}
|
|
385
|
+
if (entry.pending && overlaps(entry.pending.chars, chars)) {
|
|
386
|
+
const pairFrag = describe(src, entry.pending.at, end);
|
|
387
|
+
report({
|
|
388
|
+
shape: 'adjacent',
|
|
389
|
+
index: entry.pending.at,
|
|
390
|
+
fragment: pairFrag,
|
|
391
|
+
message: `\`${pairFrag}\` puts two whitespace-consuming quantifiers side by side with nothing mandatory between them — the engine re-partitions every blank run between them, O(run²)`,
|
|
392
|
+
// The re-partitioning spans lines only when BOTH sides can take `\n`;
|
|
393
|
+
// overlapping on spaces alone, it is bounded by each line.
|
|
394
|
+
crossesNewline: entry.pending.chars.newline && chars.newline,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const mine = { chars, at: start };
|
|
398
|
+
// A skippable quantifier leaves the previous one still "open" as well.
|
|
399
|
+
const pending = min === 0 ? joinPending(entry.pending, mine) : mine;
|
|
400
|
+
const nextStart = startMode === START_RESTRICTED ? START_RESTRICTED : min === 0 ? startMode : startMode;
|
|
401
|
+
return { start: nextStart, pending };
|
|
402
|
+
}
|
|
403
|
+
function analyzeNode(src, node, entry, report) {
|
|
404
|
+
switch (node.t) {
|
|
405
|
+
case 'anchor':
|
|
406
|
+
return node.restricting ? { start: START_RESTRICTED, pending: null } : entry;
|
|
407
|
+
case 'look':
|
|
408
|
+
// Hazards INSIDE a lookaround still cost time; the lookaround itself
|
|
409
|
+
// consumes nothing, so it is transparent to its neighbours.
|
|
410
|
+
for (const alt of node.alts)
|
|
411
|
+
analyzeSeq(src, alt, { start: START_RESTRICTED, pending: null }, report);
|
|
412
|
+
return entry;
|
|
413
|
+
case 'atom': {
|
|
414
|
+
if (node.max > UNBOUNDED_ABOVE && wsCapable(node.chars)) {
|
|
415
|
+
return onWsQuantifier(src, node.chars, node.start, node.end, node.min, entry, report);
|
|
416
|
+
}
|
|
417
|
+
const after = afterConsume(entry, node.chars);
|
|
418
|
+
return node.min === 0 ? join(entry, after) : after;
|
|
419
|
+
}
|
|
420
|
+
case 'group': {
|
|
421
|
+
const repeatedWs = node.max > UNBOUNDED_ABOVE ? groupWsChars(node.alts) : undefined;
|
|
422
|
+
if (repeatedWs) {
|
|
423
|
+
// `(?:\s)+` is `\s+` — judge it as one quantifier.
|
|
424
|
+
for (const alt of node.alts)
|
|
425
|
+
analyzeSeq(src, alt, { start: START_RESTRICTED, pending: null }, report);
|
|
426
|
+
return onWsQuantifier(src, repeatedWs, node.start, node.end, node.min, entry, report);
|
|
427
|
+
}
|
|
428
|
+
let exit = analyzeAlternatives(src, node.alts, entry, report);
|
|
429
|
+
if (node.max > 1) {
|
|
430
|
+
// A repeated group feeds its own entry: catch a body adjacent to itself.
|
|
431
|
+
exit = analyzeAlternatives(src, node.alts, join(entry, exit), report);
|
|
432
|
+
}
|
|
433
|
+
return node.min === 0 ? join(entry, exit) : exit;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function analyzeAlternatives(src, alts, entry, report) {
|
|
438
|
+
let exit;
|
|
439
|
+
for (const alt of alts) {
|
|
440
|
+
const out = analyzeSeq(src, alt, entry, report);
|
|
441
|
+
exit = exit ? join(exit, out) : out;
|
|
442
|
+
}
|
|
443
|
+
return exit ?? entry;
|
|
444
|
+
}
|
|
445
|
+
function analyzeSeq(src, seq, entry, report) {
|
|
446
|
+
let state = entry;
|
|
447
|
+
for (const node of seq)
|
|
448
|
+
state = analyzeNode(src, node, state, report);
|
|
449
|
+
return state;
|
|
450
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { IDeadGlobUnit } from './i-dead-glob-unit.js';
|
|
2
|
+
import type { IGlobListUnits } from './i-glob-list-units.js';
|
|
3
|
+
import type { IUnreadFile } from './unread-file.js';
|
|
4
|
+
/**
|
|
5
|
+
* THE dead-unit decision for a gate-plane glob list — every plane that reports
|
|
6
|
+
* a dead glob (an extraction source, a policy rule's `files`, a command
|
|
7
|
+
* baseline's `watchFiles` probe) reads it here, so "is this glob live?" is not
|
|
8
|
+
* answered three slightly different ways.
|
|
9
|
+
*
|
|
10
|
+
* `positivePaths` is what the walk returned (it may be a union wider than this
|
|
11
|
+
* list — only paths one of THIS list's inclusion globs matches are its positive
|
|
12
|
+
* set); `unread` is the reader's unread entries.
|
|
13
|
+
*
|
|
14
|
+
* - An INCLUSION glob is dead when it selects nothing that survives the list's
|
|
15
|
+
* negations and is in front of no unread entry the list keeps in scope:
|
|
16
|
+
* `matched 0 files`, or `matches only files the list's negations exclude (N)`.
|
|
17
|
+
* - A NEGATION is alive when it removes at least one file from its own list's
|
|
18
|
+
* positive set (a read file, or a matched-but-unread one), or could match
|
|
19
|
+
* beneath an unlistable directory the list's inclusion globs reach. Otherwise
|
|
20
|
+
* it is dead with `excludes nothing — none of the N file(s) the other globs
|
|
21
|
+
* select match it`. A negation is never judged by what it matches on its own:
|
|
22
|
+
* it matches nothing by itself; it subtracts. (Before round 12 every `!` read
|
|
23
|
+
* "matched 0 files", and `--fail-on-dead-units` failed load-bearing exclusions.)
|
|
24
|
+
*
|
|
25
|
+
* The boundary plane words its exemption `!` the same way ("exempts none of the
|
|
26
|
+
* N file(s) the rule's from globs match"): one liveness rule for one syntax.
|
|
27
|
+
*/
|
|
28
|
+
export declare function globListUnits(positivePaths: readonly string[], unread: readonly IUnreadFile[], globs: readonly string[]): IGlobListUnits;
|
|
29
|
+
/**
|
|
30
|
+
* {@link globListUnits} over the one reader's POSITIVE walk of `globs`
|
|
31
|
+
* (`readMatchingFiles`, memoised under `withFileReadCache`, so a plane that
|
|
32
|
+
* already read the same list pays nothing twice), read and unread entries
|
|
33
|
+
* alike. For a plane that selects files by a bare list — a generated rule's
|
|
34
|
+
* `generatedGlob`, a doc-reference rule's `files` (pass its `dotDirsNamedBy`,
|
|
35
|
+
* so the walk is the one the rule reads). An extraction source comes through
|
|
36
|
+
* here too, via `sourceGlobUnits`.
|
|
37
|
+
*
|
|
38
|
+
* The engine entry normalises idempotently (round 13): a loaded list is plain
|
|
39
|
+
* strings and passes through; a hand-built `{ pattern, expectEmpty }` entry
|
|
40
|
+
* is judged as its pattern; a MALFORMED entry is never handed to a glob reader
|
|
41
|
+
* and never silently dropped — it is reported as a dead unit naming the problem.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readGlobListUnits(projectRoot: string, rawGlobs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): IGlobListUnits;
|
|
44
|
+
/** The dead units of one glob list — {@link globListUnits}`.dead`. */
|
|
45
|
+
export declare function deadGlobUnits(positivePaths: readonly string[], unread: readonly IUnreadFile[], globs: readonly string[]): readonly IDeadGlobUnit[];
|
|
46
|
+
//# sourceMappingURL=dead-glob-units.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dead-glob-units.d.ts","sourceRoot":"","sources":["../../src/util/dead-glob-units.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,cAAc,CA0DhB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,cAAc,CAUhB;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAC3B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,SAAS,aAAa,EAAE,CAE1B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { normalizeUnitList, unitProblemsOf } from '@shrkcrft/core';
|
|
2
|
+
import { globListParts, globMayMatchUnder, globToRegex, matchesAny, measureGlobList } from "../scan/glob.js";
|
|
3
|
+
import { isUnreadDirectory, unreadEntryMatches } from "./read-scope-coverage.js";
|
|
4
|
+
import { readMatchingFiles } from "./walk-files.js";
|
|
5
|
+
/**
|
|
6
|
+
* THE dead-unit decision for a gate-plane glob list — every plane that reports
|
|
7
|
+
* a dead glob (an extraction source, a policy rule's `files`, a command
|
|
8
|
+
* baseline's `watchFiles` probe) reads it here, so "is this glob live?" is not
|
|
9
|
+
* answered three slightly different ways.
|
|
10
|
+
*
|
|
11
|
+
* `positivePaths` is what the walk returned (it may be a union wider than this
|
|
12
|
+
* list — only paths one of THIS list's inclusion globs matches are its positive
|
|
13
|
+
* set); `unread` is the reader's unread entries.
|
|
14
|
+
*
|
|
15
|
+
* - An INCLUSION glob is dead when it selects nothing that survives the list's
|
|
16
|
+
* negations and is in front of no unread entry the list keeps in scope:
|
|
17
|
+
* `matched 0 files`, or `matches only files the list's negations exclude (N)`.
|
|
18
|
+
* - A NEGATION is alive when it removes at least one file from its own list's
|
|
19
|
+
* positive set (a read file, or a matched-but-unread one), or could match
|
|
20
|
+
* beneath an unlistable directory the list's inclusion globs reach. Otherwise
|
|
21
|
+
* it is dead with `excludes nothing — none of the N file(s) the other globs
|
|
22
|
+
* select match it`. A negation is never judged by what it matches on its own:
|
|
23
|
+
* it matches nothing by itself; it subtracts. (Before round 12 every `!` read
|
|
24
|
+
* "matched 0 files", and `--fail-on-dead-units` failed load-bearing exclusions.)
|
|
25
|
+
*
|
|
26
|
+
* The boundary plane words its exemption `!` the same way ("exempts none of the
|
|
27
|
+
* N file(s) the rule's from globs match"): one liveness rule for one syntax.
|
|
28
|
+
*/
|
|
29
|
+
export function globListUnits(positivePaths, unread, globs) {
|
|
30
|
+
const { include, exclude } = globListParts(globs);
|
|
31
|
+
const measures = measureGlobList(positivePaths, globs);
|
|
32
|
+
const negationsAsWritten = exclude.map((n) => `!${n}`);
|
|
33
|
+
const unreadFilesInScope = unread.filter((u) => !isUnreadDirectory(u) && matchesAny(u.path, include));
|
|
34
|
+
const dead = [];
|
|
35
|
+
const negations = [];
|
|
36
|
+
let positiveSize;
|
|
37
|
+
for (const m of measures) {
|
|
38
|
+
if (!m.negation) {
|
|
39
|
+
if (m.effective > 0)
|
|
40
|
+
continue;
|
|
41
|
+
// Reaching an unread file, or beneath an unlistable directory, that the
|
|
42
|
+
// list keeps in scope is matching something it could not examine — never dead.
|
|
43
|
+
if (unread.some((u) => unreadEntryMatches(u, [m.glob, ...negationsAsWritten])))
|
|
44
|
+
continue;
|
|
45
|
+
// Every raw hit here was excluded — count the unread ones too, so a glob
|
|
46
|
+
// whose only match is an excluded over-cap file never reads "matched 0 files".
|
|
47
|
+
const includeRe = globToRegex(m.glob);
|
|
48
|
+
const hits = m.matched + unreadFilesInScope.filter((u) => includeRe.test(u.path)).length;
|
|
49
|
+
dead.push({
|
|
50
|
+
glob: m.glob,
|
|
51
|
+
negation: false,
|
|
52
|
+
matched: hits,
|
|
53
|
+
reason: hits === 0 ? 'matched 0 files' : `matches only files the list's negations exclude (${hits})`,
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const pattern = m.glob.slice(1);
|
|
58
|
+
const re = globToRegex(pattern);
|
|
59
|
+
const excludes = m.effective + unreadFilesInScope.filter((u) => re.test(u.path)).length;
|
|
60
|
+
if (excludes > 0) {
|
|
61
|
+
negations.push({ glob: m.glob, excludes });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// A directory whose files were never enumerated may hold what it excludes.
|
|
65
|
+
const mayExcludeUnlisted = unread.some((u) => isUnreadDirectory(u) &&
|
|
66
|
+
include.some((g) => globMayMatchUnder(g, u.path)) &&
|
|
67
|
+
globMayMatchUnder(pattern, u.path));
|
|
68
|
+
if (mayExcludeUnlisted)
|
|
69
|
+
continue;
|
|
70
|
+
positiveSize ??=
|
|
71
|
+
positivePaths.filter((p) => matchesAny(p, include)).length + unreadFilesInScope.length;
|
|
72
|
+
dead.push({
|
|
73
|
+
glob: m.glob,
|
|
74
|
+
negation: true,
|
|
75
|
+
matched: positiveSize,
|
|
76
|
+
reason: `excludes nothing — none of the ${positiveSize} file(s) the other globs select match it`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Emptied by its own negations: nothing an inclusion glob matched survives,
|
|
80
|
+
// a live negation removed something (so the positive set was not empty), and
|
|
81
|
+
// no unread entry the list keeps in scope could hold a survivor.
|
|
82
|
+
const allExcluded = negations.length > 0 &&
|
|
83
|
+
!measures.some((m) => !m.negation && m.effective > 0) &&
|
|
84
|
+
!unread.some((u) => unreadEntryMatches(u, globs));
|
|
85
|
+
return { dead, negations, checked: measures.length, allExcluded, measures };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* {@link globListUnits} over the one reader's POSITIVE walk of `globs`
|
|
89
|
+
* (`readMatchingFiles`, memoised under `withFileReadCache`, so a plane that
|
|
90
|
+
* already read the same list pays nothing twice), read and unread entries
|
|
91
|
+
* alike. For a plane that selects files by a bare list — a generated rule's
|
|
92
|
+
* `generatedGlob`, a doc-reference rule's `files` (pass its `dotDirsNamedBy`,
|
|
93
|
+
* so the walk is the one the rule reads). An extraction source comes through
|
|
94
|
+
* here too, via `sourceGlobUnits`.
|
|
95
|
+
*
|
|
96
|
+
* The engine entry normalises idempotently (round 13): a loaded list is plain
|
|
97
|
+
* strings and passes through; a hand-built `{ pattern, expectEmpty }` entry
|
|
98
|
+
* is judged as its pattern; a MALFORMED entry is never handed to a glob reader
|
|
99
|
+
* and never silently dropped — it is reported as a dead unit naming the problem.
|
|
100
|
+
*/
|
|
101
|
+
export function readGlobListUnits(projectRoot, rawGlobs, excludeDirs = new Set(), allowDotDirs = new Set()) {
|
|
102
|
+
const normalized = normalizeUnitList(rawGlobs, 'files');
|
|
103
|
+
const globs = normalized.ok ? normalized.value.units : rawGlobs.filter((g) => typeof g === 'string');
|
|
104
|
+
const malformed = normalized.ok
|
|
105
|
+
? []
|
|
106
|
+
: unitProblemsOf(normalized.error).map((reason) => ({ glob: '(malformed entry)', negation: false, matched: 0, reason }));
|
|
107
|
+
if (globs.length === 0)
|
|
108
|
+
return { dead: malformed, negations: [], checked: 0, allExcluded: false };
|
|
109
|
+
const matched = readMatchingFiles(projectRoot, globs, excludeDirs, allowDotDirs);
|
|
110
|
+
const units = globListUnits([...matched.files.keys()], matched.unread, globs);
|
|
111
|
+
return malformed.length > 0 ? { ...units, dead: [...units.dead, ...malformed] } : units;
|
|
112
|
+
}
|
|
113
|
+
/** The dead units of one glob list — {@link globListUnits}`.dead`. */
|
|
114
|
+
export function deadGlobUnits(positivePaths, unread, globs) {
|
|
115
|
+
return globListUnits(positivePaths, unread, globs).dead;
|
|
116
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type IUnitLivenessInput } from '@shrkcrft/core';
|
|
2
|
+
import type { IGlobLivenessRequest } from './i-glob-liveness-request.js';
|
|
3
|
+
/**
|
|
4
|
+
* THE observation predicate for a gate-plane glob unit (round 13). Every
|
|
5
|
+
* gate-plane reporter — `gates coverage` / `gates try` / `quality`
|
|
6
|
+
* (`buildGateCoverage`), `policy-lint` (`runPolicyLint`), and each plane
|
|
7
|
+
* engine's rule-emptiness check (`settleGlobLists`) — builds its core
|
|
8
|
+
* `settleUnitLiveness` input here, so a glob and its marker are read one way
|
|
9
|
+
* everywhere:
|
|
10
|
+
*
|
|
11
|
+
* unit | exists | live
|
|
12
|
+
* --------------------------------------+--------+------
|
|
13
|
+
* inclusion glob, not dead | yes | yes (`it now matches N file(s)`)
|
|
14
|
+
* inclusion glob, dead, matched > 0 | yes | no (every match excluded by the list's own negations)
|
|
15
|
+
* inclusion glob, dead, matched 0 | no | no (its target does not exist)
|
|
16
|
+
* negation that excludes a file | yes | yes
|
|
17
|
+
* negation that excludes nothing | no | no
|
|
18
|
+
*
|
|
19
|
+
* `exists` is RAW target existence — it decides a MARKED unit (intended-empty,
|
|
20
|
+
* or went-live), so an acceptance never says "does not exist yet" about a glob
|
|
21
|
+
* whose files exist but are all excluded. `live` is the one dead-unit decision
|
|
22
|
+
* (`globListUnits`) negated — it decides an unmarked unit, and whether a
|
|
23
|
+
* went-live unit is effective. A glob in front of an unread file is not dead
|
|
24
|
+
* (`globListUnits`), so it reads live, never intended-empty.
|
|
25
|
+
*
|
|
26
|
+
* Advisory weight: a dead gate glob is listed and withholds the ✓, it moves
|
|
27
|
+
* the exit only under `--fail-on-dead-units`; record B (the acceptance) is
|
|
28
|
+
* emitted for any intended-empty unit.
|
|
29
|
+
*/
|
|
30
|
+
export declare function globListLivenessInput(request: IGlobLivenessRequest): IUnitLivenessInput;
|
|
31
|
+
//# sourceMappingURL=glob-list-liveness-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob-list-liveness-input.d.ts","sourceRoot":"","sources":["../../src/util/glob-list-liveness-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,kBAAkB,EAAyB,MAAM,gBAAgB,CAAC;AAC/G,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,kBAAkB,CAoCvF"}
|