ucn 5.4.1 → 5.5.0
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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ignore-rules.js - .gitignore rules compiled once, matched with git's
|
|
3
|
+
* own semantics (gitignore(5); wildmatch with WM_PATHNAME).
|
|
4
|
+
*
|
|
5
|
+
* Every .gitignore file is one SCOPE: the directory it sits in (root-relative,
|
|
6
|
+
* '/'-separated, '' for the project root) and its rules in file order. A path
|
|
7
|
+
* is decided by the scopes of its ancestor directories, deepest first; within
|
|
8
|
+
* a scope the LAST matching rule wins, so a `!negation` re-includes. A rule
|
|
9
|
+
* with no slash (other than a trailing one) matches the basename at any depth
|
|
10
|
+
* below its scope; any other rule matches the path relative to the scope
|
|
11
|
+
* directory (a leading slash only anchors). A trailing slash restricts the
|
|
12
|
+
* rule to directories. Discovery walks top-down and never admits an untracked
|
|
13
|
+
* path below an excluded directory: git cannot re-include a file whose parent
|
|
14
|
+
* directory is excluded.
|
|
15
|
+
*
|
|
16
|
+
* A run of asterisks is a globstar only as a whole path segment: at the start
|
|
17
|
+
* before '/', at the end after '/', or between two '/'. Any other run is an
|
|
18
|
+
* ordinary `*`, which never matches '/' (gitignore(5): "Other consecutive
|
|
19
|
+
* asterisks are considered regular asterisks"). Path rules match as git 2.52+
|
|
20
|
+
* does: a rule `a**` then `/b` matches `a/b` and `ax/b`, never `ab` or
|
|
21
|
+
* `ax/y/b`.
|
|
22
|
+
*
|
|
23
|
+
* Matching is compiled per scope: literal basenames sit in a map, `*suffix`
|
|
24
|
+
* rules in a list, path rules in buckets by slash depth (without a whole-segment
|
|
25
|
+
* `**` a rule with k slashes can only match a path with k slashes). A query
|
|
26
|
+
* does no path arithmetic per rule; each scope derives the scope-relative path
|
|
27
|
+
* once.
|
|
28
|
+
*
|
|
29
|
+
* Not modeled (same as before this module): .git/info/exclude, the user's
|
|
30
|
+
* core.excludesFile, core.ignorecase, and .gitignore files above the project
|
|
31
|
+
* root (an enclosing repository's rules; a home-directory repository that
|
|
32
|
+
* ignores `*` must not empty every project below it).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const GLOB_SPECIAL = /[*?[\\]/;
|
|
38
|
+
|
|
39
|
+
const POSIX_CLASSES = {
|
|
40
|
+
alnum: 'A-Za-z0-9',
|
|
41
|
+
alpha: 'A-Za-z',
|
|
42
|
+
blank: ' \\t',
|
|
43
|
+
cntrl: '\\x00-\\x1f\\x7f',
|
|
44
|
+
digit: '0-9',
|
|
45
|
+
graph: '\\x21-\\x7e',
|
|
46
|
+
lower: 'a-z',
|
|
47
|
+
print: '\\x20-\\x7e',
|
|
48
|
+
punct: '!-\\/:-@\\[-`{-~',
|
|
49
|
+
space: ' \\t\\n\\v\\f\\r',
|
|
50
|
+
upper: 'A-Z',
|
|
51
|
+
xdigit: '0-9A-Fa-f',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function escapeClassChar(ch) {
|
|
55
|
+
const code = ch.charCodeAt(0);
|
|
56
|
+
return '\\u' + code.toString(16).padStart(4, '0');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeRegexChar(ch) {
|
|
60
|
+
return /[\\^$.|?*+()[\]{}/]/.test(ch) ? '\\' + ch : ch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A wildmatch bracket expression starting at pattern[start] === '['.
|
|
65
|
+
* Returns { source, end } (end = index after the closing bracket) or null
|
|
66
|
+
* when the expression is malformed: wildmatch aborts the whole match then,
|
|
67
|
+
* so the rule can never match.
|
|
68
|
+
*/
|
|
69
|
+
function compileBracket(pattern, start) {
|
|
70
|
+
const n = pattern.length;
|
|
71
|
+
let p = start + 1;
|
|
72
|
+
let negated = false;
|
|
73
|
+
if (pattern[p] === '!' || pattern[p] === '^') {
|
|
74
|
+
negated = true;
|
|
75
|
+
p++;
|
|
76
|
+
}
|
|
77
|
+
const members = [];
|
|
78
|
+
let prev = null; // previous literal member (range low end), or null
|
|
79
|
+
// do { ... } while (next char is not ']'): the first member may be ']'.
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (p >= n) return null;
|
|
82
|
+
const ch = pattern[p];
|
|
83
|
+
let literal;
|
|
84
|
+
if (ch === '\\') {
|
|
85
|
+
p++;
|
|
86
|
+
if (p >= n) return null;
|
|
87
|
+
literal = pattern[p];
|
|
88
|
+
} else if (ch === '-' && prev !== null && p + 1 < n && pattern[p + 1] !== ']') {
|
|
89
|
+
p++;
|
|
90
|
+
let hi = pattern[p];
|
|
91
|
+
if (hi === '\\') {
|
|
92
|
+
p++;
|
|
93
|
+
if (p >= n) return null;
|
|
94
|
+
hi = pattern[p];
|
|
95
|
+
}
|
|
96
|
+
// A reversed range matches nothing (wildmatch compares codes).
|
|
97
|
+
if (prev <= hi) members.push(escapeClassChar(prev) + '-' + escapeClassChar(hi));
|
|
98
|
+
prev = null;
|
|
99
|
+
p++;
|
|
100
|
+
if (p >= n) return null;
|
|
101
|
+
if (pattern[p] === ']') break;
|
|
102
|
+
continue;
|
|
103
|
+
} else if (ch === '[' && pattern[p + 1] === ':') {
|
|
104
|
+
const s = p + 2;
|
|
105
|
+
let q = s;
|
|
106
|
+
while (q < n && pattern[q] !== ']') q++;
|
|
107
|
+
if (q >= n) return null;
|
|
108
|
+
if (q - s - 1 < 0 || pattern[q - 1] !== ':') {
|
|
109
|
+
// No ":]": the '[' is an ordinary member and scanning
|
|
110
|
+
// resumes at the ':'.
|
|
111
|
+
literal = '[';
|
|
112
|
+
} else {
|
|
113
|
+
const cls = POSIX_CLASSES[pattern.slice(s, q - 1)];
|
|
114
|
+
if (!cls) return null; // malformed [:class:]
|
|
115
|
+
members.push(cls);
|
|
116
|
+
prev = null;
|
|
117
|
+
p = q + 1;
|
|
118
|
+
if (p >= n) return null;
|
|
119
|
+
if (pattern[p] === ']') break;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
literal = ch;
|
|
124
|
+
}
|
|
125
|
+
members.push(escapeClassChar(literal));
|
|
126
|
+
prev = literal;
|
|
127
|
+
p++;
|
|
128
|
+
if (p >= n) return null;
|
|
129
|
+
if (pattern[p] === ']') break;
|
|
130
|
+
}
|
|
131
|
+
const end = p + 1;
|
|
132
|
+
const body = members.join('');
|
|
133
|
+
// Under WM_PATHNAME a bracket never matches '/'.
|
|
134
|
+
if (negated) return { source: `[^/${body}]`, end };
|
|
135
|
+
return { source: body ? `(?!/)[${body}]` : '(?!)', end };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the run of asterisks pattern[start, end) is a globstar: wildmatch
|
|
140
|
+
* gives `**` its special meaning only when the run starts the pattern or
|
|
141
|
+
* follows '/', and ends it or precedes '/' (or an escaped '/').
|
|
142
|
+
*/
|
|
143
|
+
function isGlobstar(pattern, start, end) {
|
|
144
|
+
if (end - start < 2) return false;
|
|
145
|
+
if (start > 0 && pattern[start - 1] !== '/') return false;
|
|
146
|
+
return end === pattern.length || pattern[end] === '/' ||
|
|
147
|
+
(pattern[end] === '\\' && pattern[end + 1] === '/');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Regex source for a wildmatch pattern with WM_PATHNAME semantics, or null
|
|
152
|
+
* when the pattern can never match (malformed bracket, dangling escape).
|
|
153
|
+
*/
|
|
154
|
+
function wildmatchSource(pattern) {
|
|
155
|
+
let out = '';
|
|
156
|
+
const n = pattern.length;
|
|
157
|
+
let i = 0;
|
|
158
|
+
while (i < n) {
|
|
159
|
+
const ch = pattern[i];
|
|
160
|
+
if (ch === '\\') {
|
|
161
|
+
if (i + 1 >= n) return null;
|
|
162
|
+
out += escapeRegexChar(pattern[i + 1]);
|
|
163
|
+
i += 2;
|
|
164
|
+
} else if (ch === '?') {
|
|
165
|
+
out += '[^/]';
|
|
166
|
+
i++;
|
|
167
|
+
} else if (ch === '*') {
|
|
168
|
+
let j = i;
|
|
169
|
+
while (j < n && pattern[j] === '*') j++;
|
|
170
|
+
if (isGlobstar(pattern, i, j)) {
|
|
171
|
+
if (j === n) { out += '.*'; i = j; continue; }
|
|
172
|
+
if (pattern[j] === '/') { out += '(?:.*/)?'; i = j + 1; continue; }
|
|
173
|
+
out += '.*'; // before an escaped '/'
|
|
174
|
+
i = j;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
out += '[^/]*'; // any other run is one `*`
|
|
178
|
+
i = j;
|
|
179
|
+
} else if (ch === '[') {
|
|
180
|
+
const bracket = compileBracket(pattern, i);
|
|
181
|
+
if (!bracket) return null;
|
|
182
|
+
out += bracket.source;
|
|
183
|
+
i = bracket.end;
|
|
184
|
+
} else {
|
|
185
|
+
out += escapeRegexChar(ch);
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Number of '/' a path rule requires (brackets, `?` and `*` never match
|
|
194
|
+
* '/'), or -1 when a globstar may span directories.
|
|
195
|
+
*/
|
|
196
|
+
function slashDepth(pattern) {
|
|
197
|
+
let depth = 0;
|
|
198
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
199
|
+
const ch = pattern[i];
|
|
200
|
+
if (ch === '\\') {
|
|
201
|
+
if (pattern[i + 1] === '/') depth++;
|
|
202
|
+
i++;
|
|
203
|
+
} else if (ch === '[') {
|
|
204
|
+
const bracket = compileBracket(pattern, i);
|
|
205
|
+
if (!bracket) return -1;
|
|
206
|
+
i = bracket.end - 1;
|
|
207
|
+
} else if (ch === '*') {
|
|
208
|
+
let j = i + 1;
|
|
209
|
+
while (j < pattern.length && pattern[j] === '*') j++;
|
|
210
|
+
if (isGlobstar(pattern, i, j)) return -1;
|
|
211
|
+
i = j - 1;
|
|
212
|
+
} else if (ch === '/') {
|
|
213
|
+
depth++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return depth;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Git's trim_trailing_spaces: unescaped trailing spaces only. */
|
|
220
|
+
function trimTrailingSpaces(line) {
|
|
221
|
+
let lastSpace = -1;
|
|
222
|
+
for (let i = 0; i < line.length; i++) {
|
|
223
|
+
const ch = line[i];
|
|
224
|
+
if (ch === ' ') {
|
|
225
|
+
if (lastSpace < 0) lastSpace = i;
|
|
226
|
+
} else if (ch === '\\') {
|
|
227
|
+
i++;
|
|
228
|
+
if (i >= line.length) return line;
|
|
229
|
+
lastSpace = -1;
|
|
230
|
+
} else {
|
|
231
|
+
lastSpace = -1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return lastSpace >= 0 ? line.slice(0, lastSpace) : line;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse one .gitignore body into rules (git's add_patterns_from_buffer and
|
|
239
|
+
* parse_path_pattern). Each rule: { index, text, negative, mustBeDir,
|
|
240
|
+
* basename, literal, suffix, regex, depth, never }.
|
|
241
|
+
*/
|
|
242
|
+
function parseRules(content) {
|
|
243
|
+
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
|
|
244
|
+
const rules = [];
|
|
245
|
+
for (let line of content.split('\n')) {
|
|
246
|
+
if (!line || line[0] === '#') continue;
|
|
247
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
248
|
+
line = trimTrailingSpaces(line);
|
|
249
|
+
if (!line) continue;
|
|
250
|
+
const text = line;
|
|
251
|
+
let body = line;
|
|
252
|
+
const negative = body[0] === '!';
|
|
253
|
+
if (negative) body = body.slice(1);
|
|
254
|
+
const mustBeDir = body.endsWith('/');
|
|
255
|
+
if (mustBeDir) body = body.slice(0, -1);
|
|
256
|
+
if (!body) continue;
|
|
257
|
+
const basename = !body.includes('/');
|
|
258
|
+
let pattern = body;
|
|
259
|
+
if (!basename && pattern[0] === '/') pattern = pattern.slice(1);
|
|
260
|
+
const rule = {
|
|
261
|
+
index: rules.length, text, negative, mustBeDir, basename,
|
|
262
|
+
literal: null, suffix: null, regex: null, depth: 0, never: false,
|
|
263
|
+
};
|
|
264
|
+
if (!GLOB_SPECIAL.test(pattern)) {
|
|
265
|
+
rule.literal = pattern;
|
|
266
|
+
if (!basename) rule.depth = slashDepth(pattern);
|
|
267
|
+
} else if (basename && pattern[0] === '*' && !GLOB_SPECIAL.test(pattern.slice(1))) {
|
|
268
|
+
rule.suffix = pattern.slice(1);
|
|
269
|
+
} else if (basename) {
|
|
270
|
+
// match_basename: the whole pattern against the basename.
|
|
271
|
+
const source = wildmatchSource(pattern);
|
|
272
|
+
if (source === null) rule.never = true;
|
|
273
|
+
else rule.regex = new RegExp('^' + source + '$');
|
|
274
|
+
} else {
|
|
275
|
+
// match_pathname compares the literal prefix, then wildmatches
|
|
276
|
+
// from the prefix's last character (git 2.52+), which equals
|
|
277
|
+
// wildmatching the whole pattern: a `**` right after the prefix
|
|
278
|
+
// is no globstar. Before 2.52 git wildmatched the rest from its
|
|
279
|
+
// own start, so `a**/b` also matched `ab` and `ax/y/b`.
|
|
280
|
+
const source = wildmatchSource(pattern);
|
|
281
|
+
if (source === null) rule.never = true;
|
|
282
|
+
else rule.regex = new RegExp('^' + source + '$');
|
|
283
|
+
rule.depth = slashDepth(pattern);
|
|
284
|
+
}
|
|
285
|
+
rules.push(rule); // never-matching rules keep file-order indices
|
|
286
|
+
}
|
|
287
|
+
return rules;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
class IgnoreScope {
|
|
291
|
+
/**
|
|
292
|
+
* @param {string} base - directory of the .gitignore, root-relative ('' = root)
|
|
293
|
+
* @param {object[]} rules - parseRules() output
|
|
294
|
+
*/
|
|
295
|
+
constructor(base, rules) {
|
|
296
|
+
this.base = base;
|
|
297
|
+
this.rules = rules;
|
|
298
|
+
this.names = new Map(); // literal basename -> rules (ascending index)
|
|
299
|
+
this.suffixes = []; // `*literal` basename rules
|
|
300
|
+
this.basenameGlobs = []; // other basename rules
|
|
301
|
+
this.pathByDepth = new Map(); // slash depth -> path rules
|
|
302
|
+
this.pathAnyDepth = []; // path rules with a globstar
|
|
303
|
+
for (const rule of rules) {
|
|
304
|
+
if (rule.never) continue;
|
|
305
|
+
if (rule.basename) {
|
|
306
|
+
if (rule.literal !== null) {
|
|
307
|
+
let list = this.names.get(rule.literal);
|
|
308
|
+
if (!list) { list = []; this.names.set(rule.literal, list); }
|
|
309
|
+
list.push(rule);
|
|
310
|
+
} else if (rule.suffix !== null) {
|
|
311
|
+
this.suffixes.push(rule);
|
|
312
|
+
} else {
|
|
313
|
+
this.basenameGlobs.push(rule);
|
|
314
|
+
}
|
|
315
|
+
} else if (rule.depth < 0) {
|
|
316
|
+
this.pathAnyDepth.push(rule);
|
|
317
|
+
} else {
|
|
318
|
+
let list = this.pathByDepth.get(rule.depth);
|
|
319
|
+
if (!list) { list = []; this.pathByDepth.set(rule.depth, list); }
|
|
320
|
+
list.push(rule);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The last rule of this scope matching `sub` (the path relative to the
|
|
327
|
+
* scope directory, `depth` slashes) with basename `name`, or null.
|
|
328
|
+
*/
|
|
329
|
+
lastMatch(sub, depth, name, isDir) {
|
|
330
|
+
let best = null;
|
|
331
|
+
const named = this.names.get(name);
|
|
332
|
+
if (named) best = lastIn(named, null, isDir, NAME_TEST, name);
|
|
333
|
+
if (this.suffixes.length) best = lastIn(this.suffixes, best, isDir, SUFFIX_TEST, name);
|
|
334
|
+
if (this.basenameGlobs.length) best = lastIn(this.basenameGlobs, best, isDir, GLOB_TEST, name);
|
|
335
|
+
const atDepth = this.pathByDepth.get(depth);
|
|
336
|
+
if (atDepth) best = lastIn(atDepth, best, isDir, PATH_TEST, sub);
|
|
337
|
+
if (this.pathAnyDepth.length) best = lastIn(this.pathAnyDepth, best, isDir, PATH_TEST, sub);
|
|
338
|
+
return best;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const NAME_TEST = 0;
|
|
343
|
+
const SUFFIX_TEST = 1;
|
|
344
|
+
const GLOB_TEST = 2;
|
|
345
|
+
const PATH_TEST = 3;
|
|
346
|
+
|
|
347
|
+
/** The last rule of `list` (ascending index) matching `text`, if it beats `best`. */
|
|
348
|
+
function lastIn(list, best, isDir, kind, text) {
|
|
349
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
350
|
+
const rule = list[i];
|
|
351
|
+
if (best && rule.index < best.index) return best;
|
|
352
|
+
if (rule.mustBeDir && !isDir) continue;
|
|
353
|
+
let hit;
|
|
354
|
+
if (kind === NAME_TEST) hit = true;
|
|
355
|
+
else if (kind === SUFFIX_TEST) hit = text.endsWith(rule.suffix);
|
|
356
|
+
else if (kind === GLOB_TEST) hit = rule.regex.test(text);
|
|
357
|
+
else hit = rule.literal !== null ? rule.literal === text : rule.regex.test(text);
|
|
358
|
+
if (hit) return rule;
|
|
359
|
+
}
|
|
360
|
+
return best;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
class IgnoreRules {
|
|
364
|
+
/**
|
|
365
|
+
* @param {{base: string, content: string}[]} files - parent before child
|
|
366
|
+
*/
|
|
367
|
+
constructor(files) {
|
|
368
|
+
this.scopes = new Map();
|
|
369
|
+
this.order = [];
|
|
370
|
+
for (const { base, content } of files) {
|
|
371
|
+
const rules = parseRules(content);
|
|
372
|
+
if (rules.length === 0) continue;
|
|
373
|
+
let scope = this.scopes.get(base);
|
|
374
|
+
if (scope) {
|
|
375
|
+
// Same directory listed twice: keep one scope in file order.
|
|
376
|
+
scope = new IgnoreScope(base, [...scope.rules, ...rules.map((rule, i) => ({
|
|
377
|
+
...rule, index: scope.rules.length + i,
|
|
378
|
+
}))]);
|
|
379
|
+
} else {
|
|
380
|
+
scope = new IgnoreScope(base, rules);
|
|
381
|
+
this.order.push(base);
|
|
382
|
+
}
|
|
383
|
+
this.scopes.set(base, scope);
|
|
384
|
+
}
|
|
385
|
+
this.size = [...this.scopes.values()].reduce((sum, scope) => sum + scope.rules.length, 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Stable text for the discovery fingerprint (cache staleness). */
|
|
389
|
+
fingerprint() {
|
|
390
|
+
const parts = [];
|
|
391
|
+
for (const base of this.order) {
|
|
392
|
+
parts.push(`@${base}`);
|
|
393
|
+
for (const rule of this.scopes.get(base).rules) parts.push(rule.text);
|
|
394
|
+
}
|
|
395
|
+
return parts.join('\0');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Scope chain for a directory, given its parent's chain. */
|
|
399
|
+
scopesFor(parentScopes, dirRel) {
|
|
400
|
+
const own = this.scopes.get(dirRel);
|
|
401
|
+
return own ? [...parentScopes, own] : parentScopes;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Scope chain for a directory from scratch. */
|
|
405
|
+
chainFor(dirRel) {
|
|
406
|
+
let chain = [];
|
|
407
|
+
const root = this.scopes.get('');
|
|
408
|
+
if (root) chain = [root];
|
|
409
|
+
if (!dirRel) return chain;
|
|
410
|
+
const parts = dirRel.split('/');
|
|
411
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
412
|
+
chain = this.scopesFor(chain, parts.slice(0, i).join('/'));
|
|
413
|
+
}
|
|
414
|
+
return chain;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Decide one path from the scopes of its parent directory (deepest
|
|
419
|
+
* wins). True = excluded, false = not excluded or re-included.
|
|
420
|
+
* @param {IgnoreScope[]} scopes - chain for the path's parent directory
|
|
421
|
+
* @param {string} rel - root-relative path, '/'-separated
|
|
422
|
+
* @param {number} relDepth - number of '/' in rel
|
|
423
|
+
* @param {string} name - basename of rel
|
|
424
|
+
*/
|
|
425
|
+
decide(scopes, rel, relDepth, name, isDir) {
|
|
426
|
+
for (let s = scopes.length - 1; s >= 0; s--) {
|
|
427
|
+
const scope = scopes[s];
|
|
428
|
+
let sub = rel;
|
|
429
|
+
let depth = relDepth;
|
|
430
|
+
if (scope.base) {
|
|
431
|
+
sub = rel.slice(scope.base.length + 1);
|
|
432
|
+
depth = relDepth - scope.baseDepth();
|
|
433
|
+
}
|
|
434
|
+
const rule = scope.lastMatch(sub, depth, name, isDir);
|
|
435
|
+
if (rule) return !rule.negative;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Full git answer for one root-relative path: excluded when the path or
|
|
442
|
+
* any of its parent directories is excluded.
|
|
443
|
+
*/
|
|
444
|
+
isIgnored(rel, isDir = false) {
|
|
445
|
+
const parts = rel.split('/').filter(Boolean);
|
|
446
|
+
let chain = this.chainFor('');
|
|
447
|
+
let prefix = '';
|
|
448
|
+
for (let i = 0; i < parts.length; i++) {
|
|
449
|
+
const last = i === parts.length - 1;
|
|
450
|
+
const current = prefix ? `${prefix}/${parts[i]}` : parts[i];
|
|
451
|
+
if (this.decide(chain, current, i, parts[i], last ? isDir : true)) return true;
|
|
452
|
+
if (!last) chain = this.scopesFor(chain, current);
|
|
453
|
+
prefix = current;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
IgnoreScope.prototype.baseDepth = function baseDepth() {
|
|
460
|
+
if (this._baseDepth === undefined) {
|
|
461
|
+
let depth = 0;
|
|
462
|
+
for (let i = 0; i < this.base.length; i++) if (this.base.charCodeAt(i) === 47) depth++;
|
|
463
|
+
this._baseDepth = depth + 1;
|
|
464
|
+
}
|
|
465
|
+
return this._baseDepth;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
module.exports = {
|
|
469
|
+
IgnoreRules,
|
|
470
|
+
parseRules,
|
|
471
|
+
wildmatchSource,
|
|
472
|
+
};
|