@brighterly/lib-fe-codestyle 0.4.1 → 0.5.1
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/invariants/ast.d.ts +82 -0
- package/dist/invariants/ast.js +270 -0
- package/dist/invariants/rules/regression.js +3 -0
- package/dist/invariants/run.d.ts +2 -5
- package/dist/invariants/run.js +237 -51
- package/dist/invariants/self-test.d.ts +11 -3
- package/dist/invariants/self-test.js +41 -14
- package/dist/invariants/types.d.ts +28 -4
- package/dist/invariants/util.d.ts +33 -8
- package/dist/invariants/util.js +175 -11
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* No dependencies — this runs in pre-commit, so it must not need an install.
|
|
6
6
|
*/
|
|
7
|
-
import type { DanglingRefInput, Finding, Level, Levels } from './types.js';
|
|
7
|
+
import type { DanglingRefInput, Finding, Level, Levels, MetaRuleId } from './types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Minimal glob → RegExp. Supports `**`, `*`, `?` and `{a,b}`.
|
|
10
10
|
* Deliberately not a full glob implementation: rule scopes are simple by
|
|
@@ -37,14 +37,22 @@ export declare const parseIgnores: (source: string) => {
|
|
|
37
37
|
}[];
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* block comments and HTML comments.
|
|
40
|
+
* Blanks comment bodies in place: every line and every non-comment character stays where it was,
|
|
41
|
+
* so line numbers and offsets survive. Two grammars:
|
|
43
42
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* - script (TS/JS/JSON): a tokenizer that knows string literals, template literals with `${}`
|
|
44
|
+
* nesting and regex literals, so `'…/**'` in a glob or `'http://'` in a URL opens nothing. The
|
|
45
|
+
* regex version this replaces turned the first such glob in a rule file into an open block
|
|
46
|
+
* comment and hid the rest of the file from every rule that reads `code` (land-app, 2026-09-23);
|
|
47
|
+
* - the template part of a .vue file: HTML comments only — a script tokenizer over prose would take
|
|
48
|
+
* "child's" as an open string. Script blocks inside the SFC get the script grammar.
|
|
49
|
+
*
|
|
50
|
+
* Markdown and HTML get HTML comments only; no rule reads code out of them. The regex-vs-division
|
|
51
|
+
* call for `/` is the usual heuristic (what precedes it); a regex right after `)` reads as division,
|
|
52
|
+
* which leaves its body unparsed — never blanked, so the failure mode is a missed comment, not
|
|
53
|
+
* hidden code.
|
|
46
54
|
*/
|
|
47
|
-
export declare const stripCodeComments: (source: string) => string;
|
|
55
|
+
export declare const stripCodeComments: (source: string, file?: string) => string;
|
|
48
56
|
/** Line number (1-based) of a character offset. */
|
|
49
57
|
export declare const lineOf: (source: string, index: number) => number;
|
|
50
58
|
/**
|
|
@@ -67,6 +75,17 @@ export declare const stringKeysOf: (literal: string) => Set<string>;
|
|
|
67
75
|
* self-test can exercise it without touching the repo.
|
|
68
76
|
*/
|
|
69
77
|
export declare const findDanglingRuleRefs: (input: DanglingRefInput) => Finding[];
|
|
78
|
+
/**
|
|
79
|
+
* `invariant-ignore` comments that name a rule declared `noIgnore` (IG2). The
|
|
80
|
+
* runner never honours such a comment; reporting it makes the attempt visible so a
|
|
81
|
+
* reviewer sees the intent and an AI author learns the rule cannot be argued with
|
|
82
|
+
* in the file under check.
|
|
83
|
+
*/
|
|
84
|
+
export declare const findForbiddenSuppressions: (ignores: {
|
|
85
|
+
file: string;
|
|
86
|
+
line: number;
|
|
87
|
+
ids: string[];
|
|
88
|
+
}[], noIgnoreIds: Set<string>) => Finding[];
|
|
70
89
|
/**
|
|
71
90
|
* Severity of one finding, from three inputs that used to be one.
|
|
72
91
|
*
|
|
@@ -101,7 +120,7 @@ export declare const knownRuleIds: (parts: {
|
|
|
101
120
|
fileScan: string[];
|
|
102
121
|
specs: string[];
|
|
103
122
|
linter: string[];
|
|
104
|
-
meta?:
|
|
123
|
+
meta?: MetaRuleId[];
|
|
105
124
|
}) => Set<string>;
|
|
106
125
|
/**
|
|
107
126
|
* The severity override for a rule, or undefined.
|
|
@@ -111,3 +130,9 @@ export declare const knownRuleIds: (parts: {
|
|
|
111
130
|
* accessor keeps every caller from having to care.
|
|
112
131
|
*/
|
|
113
132
|
export declare const levelOverrideOf: (levels: Levels, id: string) => Level | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* The `until YYYY-MM-DD` an allowlist reason may carry. A rule may require one — a coverage gap is
|
|
135
|
+
* a debt with a date, not a permanent exemption — and report the entry once the date has passed.
|
|
136
|
+
* ISO dates compare as strings, so the caller needs no Date arithmetic.
|
|
137
|
+
*/
|
|
138
|
+
export declare const exemptionExpiry: (reason: string) => string | null;
|
package/dist/invariants/util.js
CHANGED
|
@@ -135,19 +135,166 @@ export const parseIgnores = (source) => {
|
|
|
135
135
|
return { suppressions, unreasoned, comments };
|
|
136
136
|
};
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* block comments and HTML comments.
|
|
138
|
+
* Blanks comment bodies in place: every line and every non-comment character stays where it was,
|
|
139
|
+
* so line numbers and offsets survive. Two grammars:
|
|
141
140
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* - script (TS/JS/JSON): a tokenizer that knows string literals, template literals with `${}`
|
|
142
|
+
* nesting and regex literals, so `'…/**'` in a glob or `'http://'` in a URL opens nothing. The
|
|
143
|
+
* regex version this replaces turned the first such glob in a rule file into an open block
|
|
144
|
+
* comment and hid the rest of the file from every rule that reads `code` (land-app, 2026-09-23);
|
|
145
|
+
* - the template part of a .vue file: HTML comments only — a script tokenizer over prose would take
|
|
146
|
+
* "child's" as an open string. Script blocks inside the SFC get the script grammar.
|
|
147
|
+
*
|
|
148
|
+
* Markdown and HTML get HTML comments only; no rule reads code out of them. The regex-vs-division
|
|
149
|
+
* call for `/` is the usual heuristic (what precedes it); a regex right after `)` reads as division,
|
|
150
|
+
* which leaves its body unparsed — never blanked, so the failure mode is a missed comment, not
|
|
151
|
+
* hidden code.
|
|
144
152
|
*/
|
|
145
|
-
export const stripCodeComments = (source) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
export const stripCodeComments = (source, file = '') => {
|
|
154
|
+
if (/\.vue$/.test(file) || (file === '' && /<template[\s>]/.test(source) && /<script[\s>]/.test(source)))
|
|
155
|
+
return stripSfc(source);
|
|
156
|
+
if (/\.(?:md|html)$/.test(file))
|
|
157
|
+
return blankHtmlComments(source);
|
|
158
|
+
return stripScript(source);
|
|
159
|
+
};
|
|
160
|
+
const blankRun = (text) => text.replace(/[^\n]/g, ' ');
|
|
161
|
+
const blankHtmlComments = (source) => source.replace(/<!--[\s\S]*?-->/g, blankRun);
|
|
162
|
+
const stripSfc = (source) => {
|
|
163
|
+
let out = '';
|
|
164
|
+
let last = 0;
|
|
165
|
+
const re = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
|
166
|
+
let m;
|
|
167
|
+
while ((m = re.exec(source)) !== null) {
|
|
168
|
+
const body = m[1] ?? '';
|
|
169
|
+
const bodyStart = m.index + m[0].length - body.length - '</script>'.length;
|
|
170
|
+
out += blankHtmlComments(source.slice(last, bodyStart));
|
|
171
|
+
out += stripScript(body);
|
|
172
|
+
last = bodyStart + body.length;
|
|
173
|
+
}
|
|
174
|
+
return out + blankHtmlComments(source.slice(last));
|
|
175
|
+
};
|
|
176
|
+
/** After one of these a `/` starts a regex literal, not a division. */
|
|
177
|
+
const REGEX_PRECEDER = /[(,=:[!&|?{};+\-*%<>~^]$/;
|
|
178
|
+
const REGEX_KEYWORD = /(?:^|[^\w$])(?:return|typeof|case|do|else|in|instanceof|new|throw|void|yield|await|delete)$/;
|
|
179
|
+
const stripScript = (code) => {
|
|
180
|
+
const out = code.split('');
|
|
181
|
+
const n = code.length;
|
|
182
|
+
const blankAt = (i) => {
|
|
183
|
+
if (out[i] !== '\n')
|
|
184
|
+
out[i] = ' ';
|
|
185
|
+
};
|
|
186
|
+
const regexAllowed = (i) => {
|
|
187
|
+
let j = i - 1;
|
|
188
|
+
while (j >= 0 && /\s/.test(out[j] ?? ''))
|
|
189
|
+
j--;
|
|
190
|
+
if (j < 0)
|
|
191
|
+
return true;
|
|
192
|
+
const before = out.slice(Math.max(0, j - 12), j + 1).join('');
|
|
193
|
+
return REGEX_PRECEDER.test(before) || REGEX_KEYWORD.test(before);
|
|
194
|
+
};
|
|
195
|
+
const skipString = (start, quote) => {
|
|
196
|
+
let i = start + 1;
|
|
197
|
+
while (i < n && code[i] !== quote && code[i] !== '\n') {
|
|
198
|
+
if (code[i] === '\\')
|
|
199
|
+
i++;
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
return i + 1;
|
|
203
|
+
};
|
|
204
|
+
const skipRegex = (start) => {
|
|
205
|
+
let i = start + 1;
|
|
206
|
+
let inClass = false;
|
|
207
|
+
while (i < n && code[i] !== '\n') {
|
|
208
|
+
const ch = code[i];
|
|
209
|
+
if (ch === '\\') {
|
|
210
|
+
i += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (ch === '[')
|
|
214
|
+
inClass = true;
|
|
215
|
+
else if (ch === ']')
|
|
216
|
+
inClass = false;
|
|
217
|
+
else if (ch === '/' && !inClass) {
|
|
218
|
+
i++;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
while (i < n && /[a-z]/i.test(code[i] ?? ''))
|
|
224
|
+
i++;
|
|
225
|
+
return i;
|
|
226
|
+
};
|
|
227
|
+
// Returns the index it stopped at: `n`, or the `}` that closes a `${` when `untilBrace` is set.
|
|
228
|
+
const scan = (start, untilBrace) => {
|
|
229
|
+
let i = start;
|
|
230
|
+
let depth = 0;
|
|
231
|
+
while (i < n) {
|
|
232
|
+
const c = code[i];
|
|
233
|
+
const next = code[i + 1];
|
|
234
|
+
if (c === '"' || c === '\'') {
|
|
235
|
+
i = skipString(i, c);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (c === '`') {
|
|
239
|
+
i = skipTemplate(i);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (c === '/' && next === '/') {
|
|
243
|
+
while (i < n && code[i] !== '\n') {
|
|
244
|
+
blankAt(i);
|
|
245
|
+
i++;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (c === '/' && next === '*') {
|
|
250
|
+
blankAt(i);
|
|
251
|
+
blankAt(i + 1);
|
|
252
|
+
i += 2;
|
|
253
|
+
while (i < n && !(code[i] === '*' && code[i + 1] === '/')) {
|
|
254
|
+
blankAt(i);
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
if (i < n) {
|
|
258
|
+
blankAt(i);
|
|
259
|
+
blankAt(i + 1);
|
|
260
|
+
i += 2;
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (c === '/' && regexAllowed(i)) {
|
|
265
|
+
i = skipRegex(i);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (untilBrace) {
|
|
269
|
+
if (c === '{')
|
|
270
|
+
depth++;
|
|
271
|
+
else if (c === '}') {
|
|
272
|
+
if (depth === 0)
|
|
273
|
+
return i;
|
|
274
|
+
depth--;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
return i;
|
|
280
|
+
};
|
|
281
|
+
const skipTemplate = (start) => {
|
|
282
|
+
let i = start + 1;
|
|
283
|
+
while (i < n && code[i] !== '`') {
|
|
284
|
+
if (code[i] === '\\') {
|
|
285
|
+
i += 2;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
289
|
+
i = scan(i + 2, true) + 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
return i + 1;
|
|
295
|
+
};
|
|
296
|
+
scan(0, false);
|
|
297
|
+
return out.join('');
|
|
151
298
|
};
|
|
152
299
|
/** Line number (1-based) of a character offset. */
|
|
153
300
|
export const lineOf = (source, index) => source.slice(0, index).split('\n').length;
|
|
@@ -238,6 +385,17 @@ export const findDanglingRuleRefs = (input) => {
|
|
|
238
385
|
}
|
|
239
386
|
return out;
|
|
240
387
|
};
|
|
388
|
+
/**
|
|
389
|
+
* `invariant-ignore` comments that name a rule declared `noIgnore` (IG2). The
|
|
390
|
+
* runner never honours such a comment; reporting it makes the attempt visible so a
|
|
391
|
+
* reviewer sees the intent and an AI author learns the rule cannot be argued with
|
|
392
|
+
* in the file under check.
|
|
393
|
+
*/
|
|
394
|
+
export const findForbiddenSuppressions = (ignores, noIgnoreIds) => ignores.flatMap(({ file, line, ids }) => ids.filter(id => noIgnoreIds.has(id)).map(id => ({
|
|
395
|
+
file,
|
|
396
|
+
line,
|
|
397
|
+
message: `invariant-ignore names "${id}", a noIgnore rule — this suppression is inert and the attempt is reported`,
|
|
398
|
+
})));
|
|
241
399
|
const LEVEL_RANK = { off: 0, warn: 1, error: 2 };
|
|
242
400
|
/**
|
|
243
401
|
* Severity of one finding, from three inputs that used to be one.
|
|
@@ -284,3 +442,9 @@ export const knownRuleIds = (parts) => new Set([...parts.fileScan, ...parts.spec
|
|
|
284
442
|
* accessor keeps every caller from having to care.
|
|
285
443
|
*/
|
|
286
444
|
export const levelOverrideOf = (levels, id) => levels[id]?.level;
|
|
445
|
+
/**
|
|
446
|
+
* The `until YYYY-MM-DD` an allowlist reason may carry. A rule may require one — a coverage gap is
|
|
447
|
+
* a debt with a date, not a permanent exemption — and report the entry once the date has passed.
|
|
448
|
+
* ISO dates compare as strings, so the caller needs no Date arithmetic.
|
|
449
|
+
*/
|
|
450
|
+
export const exemptionExpiry = (reason) => reason.match(/\buntil\s+(\d{4}-\d{2}-\d{2})\b/)?.[1] ?? null;
|
package/package.json
CHANGED