@sarj/eslint-plugin 2.0.2 → 2.2.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/dist/index.cjs +1070 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +1075 -114
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -1,45 +1,30 @@
|
|
|
1
1
|
// src/rules/enforce-file-structure.ts
|
|
2
2
|
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
3
3
|
var SECTION = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
functions: 3,
|
|
8
|
-
exports: 4
|
|
4
|
+
declarations: 0,
|
|
5
|
+
functions: 1,
|
|
6
|
+
exports: 2
|
|
9
7
|
};
|
|
10
|
-
var SECTION_NAMES = [
|
|
11
|
-
"imports",
|
|
12
|
-
"types",
|
|
13
|
-
"constants",
|
|
14
|
-
"functions",
|
|
15
|
-
"exports"
|
|
16
|
-
];
|
|
8
|
+
var SECTION_NAMES = ["declarations", "functions", "exports"];
|
|
17
9
|
var sectionName = (ordinal) => {
|
|
18
10
|
const name = SECTION_NAMES[ordinal];
|
|
19
11
|
return name ?? "unknown";
|
|
20
12
|
};
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
var SERVER_ACTION_FILE_RE = /(?:^|\/)actions\/|\.action\.[jt]sx?$|(?:^|\/)actions\.[jt]sx?$/;
|
|
14
|
+
var isFunctionExpression = (node) => node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression;
|
|
15
|
+
var isFunctionLikeVariable = (statement) => statement.declarations.length > 0 && statement.declarations.every(
|
|
16
|
+
(decl) => decl.init !== null && isFunctionExpression(decl.init)
|
|
17
|
+
);
|
|
26
18
|
var getStatementSection = (statement) => {
|
|
27
19
|
switch (statement.type) {
|
|
28
20
|
case AST_NODE_TYPES.ImportDeclaration:
|
|
29
|
-
return SECTION.imports;
|
|
30
21
|
case AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
31
22
|
case AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
32
23
|
case AST_NODE_TYPES.TSEnumDeclaration:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (firstDeclarator !== void 0 && isConstantNamed(firstDeclarator)) {
|
|
38
|
-
return SECTION.constants;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return SECTION.functions;
|
|
42
|
-
}
|
|
24
|
+
case AST_NODE_TYPES.ClassDeclaration:
|
|
25
|
+
return SECTION.declarations;
|
|
26
|
+
case AST_NODE_TYPES.VariableDeclaration:
|
|
27
|
+
return isFunctionLikeVariable(statement) ? SECTION.functions : SECTION.declarations;
|
|
43
28
|
case AST_NODE_TYPES.FunctionDeclaration:
|
|
44
29
|
return SECTION.functions;
|
|
45
30
|
case AST_NODE_TYPES.ExportNamedDeclaration:
|
|
@@ -64,7 +49,7 @@ var enforce_file_structure_default = ESLintUtils.RuleCreator(
|
|
|
64
49
|
meta: {
|
|
65
50
|
type: "suggestion",
|
|
66
51
|
docs: {
|
|
67
|
-
description: "Enforce
|
|
52
|
+
description: "Enforce that function definitions follow the file's top-of-file declarations (imports, types, constants, classes) \u2014 the stepdown rule. Ordering among non-function declarations is not enforced. Server-action files (under `/actions/`, named `*.action.ts`, or `actions.ts`) must also begin with a `use server` directive."
|
|
68
53
|
},
|
|
69
54
|
schema: [],
|
|
70
55
|
messages: {
|
|
@@ -75,7 +60,7 @@ var enforce_file_structure_default = ESLintUtils.RuleCreator(
|
|
|
75
60
|
defaultOptions: [],
|
|
76
61
|
create(context) {
|
|
77
62
|
const filename = context.filename;
|
|
78
|
-
const isServerAction =
|
|
63
|
+
const isServerAction = SERVER_ACTION_FILE_RE.test(filename);
|
|
79
64
|
return {
|
|
80
65
|
Program(node) {
|
|
81
66
|
const body = node.body;
|
|
@@ -88,9 +73,8 @@ var enforce_file_structure_default = ESLintUtils.RuleCreator(
|
|
|
88
73
|
});
|
|
89
74
|
}
|
|
90
75
|
}
|
|
91
|
-
let currentSection = SECTION.
|
|
76
|
+
let currentSection = SECTION.declarations;
|
|
92
77
|
for (const statement of body) {
|
|
93
|
-
if (isUseServerDirective(statement)) continue;
|
|
94
78
|
if (statement.type === AST_NODE_TYPES.ExpressionStatement && statement.expression.type === AST_NODE_TYPES.Literal && typeof statement.expression.value === "string" && statement.expression.value.startsWith("use ")) {
|
|
95
79
|
continue;
|
|
96
80
|
}
|
|
@@ -129,7 +113,7 @@ var HTTP_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
|
129
113
|
"head",
|
|
130
114
|
"options"
|
|
131
115
|
]);
|
|
132
|
-
var
|
|
116
|
+
var ANALYTICS_SEGMENTS = /* @__PURE__ */ new Set([
|
|
133
117
|
"analytics",
|
|
134
118
|
"telemetry",
|
|
135
119
|
"track",
|
|
@@ -138,7 +122,7 @@ var ANALYTICS_KEYWORDS = [
|
|
|
138
122
|
"beacon",
|
|
139
123
|
"metrics",
|
|
140
124
|
"event"
|
|
141
|
-
];
|
|
125
|
+
]);
|
|
142
126
|
function isEffectHookCall(node) {
|
|
143
127
|
const callee = node.callee;
|
|
144
128
|
if (callee.type === AST_NODE_TYPES2.Identifier) {
|
|
@@ -212,7 +196,7 @@ function extractUrlString(node) {
|
|
|
212
196
|
function isAnalyticsCall(node) {
|
|
213
197
|
const url = extractUrlString(node).toLowerCase();
|
|
214
198
|
if (url === "") return false;
|
|
215
|
-
return
|
|
199
|
+
return url.split(/[/.]/).some((segment) => ANALYTICS_SEGMENTS.has(segment));
|
|
216
200
|
}
|
|
217
201
|
var no_client_side_data_fetching_default = ESLintUtils2.RuleCreator(
|
|
218
202
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
@@ -251,8 +235,105 @@ var no_client_side_data_fetching_default = ESLintUtils2.RuleCreator(
|
|
|
251
235
|
}
|
|
252
236
|
});
|
|
253
237
|
|
|
254
|
-
// src/rules/no-
|
|
238
|
+
// src/rules/no-comment-cruft.ts
|
|
255
239
|
import { ESLintUtils as ESLintUtils3 } from "@typescript-eslint/utils";
|
|
240
|
+
var LEADING_PREAMBLE_MIN = 4;
|
|
241
|
+
var DIRECTIVE_RE = /^(eslint\b|eslint-|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
|
|
242
|
+
var LICENSE_RE = /copyright|licen[cs]ed?|spdx|permission is hereby granted|all rights reserved/i;
|
|
243
|
+
var BANNER_FULL_RE = /^[\s\-=*#~_+.]{4,}$/;
|
|
244
|
+
var BANNER_RUN_RE = /={4,}|-{4,}|#{4,}|\*{4,}|~{4,}/;
|
|
245
|
+
var REGION_RE = /^#?(?:end)?region\b/i;
|
|
246
|
+
var CODE_KEYWORD_RE = /^(import |export |const |let |var |function\b|class |interface |type \w|enum |return\b|throw |await |async |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|new |console\.)/;
|
|
247
|
+
var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
|
|
248
|
+
var CALL_OR_ASSIGN_RE = /^[A-Za-z_$][\w.$[\]]*\s*(?:=(?![=>])|\+=|-=|\*=)\s*\S.*[;)}\]]\s*$|^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
|
|
249
|
+
function stripCommentMarker(line) {
|
|
250
|
+
return line.replace(/^\s*\/\//, "").replace(/^\s*\*+/, "").trim();
|
|
251
|
+
}
|
|
252
|
+
function isDirective(text) {
|
|
253
|
+
return DIRECTIVE_RE.test(text.trim());
|
|
254
|
+
}
|
|
255
|
+
function isBanner(text) {
|
|
256
|
+
const t = text.trim();
|
|
257
|
+
if (!t) return false;
|
|
258
|
+
return BANNER_FULL_RE.test(t) || BANNER_RUN_RE.test(t) || REGION_RE.test(t);
|
|
259
|
+
}
|
|
260
|
+
function looksLikeCode(text) {
|
|
261
|
+
const t = text.trim();
|
|
262
|
+
if (!t) return false;
|
|
263
|
+
if (CODE_KEYWORD_RE.test(t) && CODE_TAIL_RE.test(t)) return true;
|
|
264
|
+
return CALL_OR_ASSIGN_RE.test(t);
|
|
265
|
+
}
|
|
266
|
+
var no_comment_cruft_default = ESLintUtils3.RuleCreator(
|
|
267
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
268
|
+
)({
|
|
269
|
+
name: "no-comment-cruft",
|
|
270
|
+
meta: {
|
|
271
|
+
type: "suggestion",
|
|
272
|
+
docs: {
|
|
273
|
+
description: "Flag commented-out code, section-banner comments, and leading file-header comment preambles."
|
|
274
|
+
},
|
|
275
|
+
schema: [],
|
|
276
|
+
messages: {
|
|
277
|
+
commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
|
|
278
|
+
sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
|
|
279
|
+
fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines."
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
defaultOptions: [],
|
|
283
|
+
create(context) {
|
|
284
|
+
const sourceCode = context.sourceCode;
|
|
285
|
+
function isStandalone(comment) {
|
|
286
|
+
const before = sourceCode.getTokenBefore(comment, {
|
|
287
|
+
includeComments: false
|
|
288
|
+
});
|
|
289
|
+
return !before || before.loc.end.line < comment.loc.start.line;
|
|
290
|
+
}
|
|
291
|
+
function isJsDoc(comment) {
|
|
292
|
+
return comment.type === "Block" && /^\*/.test(comment.value);
|
|
293
|
+
}
|
|
294
|
+
function reportLeadingPreamble(comments, firstCodeLine) {
|
|
295
|
+
const leading = [];
|
|
296
|
+
let prevLine = null;
|
|
297
|
+
for (const comment of comments) {
|
|
298
|
+
if (comment.type !== "Line") break;
|
|
299
|
+
if (comment.loc.start.line >= firstCodeLine) break;
|
|
300
|
+
if (!isStandalone(comment)) break;
|
|
301
|
+
const body = stripCommentMarker(comment.value);
|
|
302
|
+
if (isDirective(body) || body.startsWith("!")) continue;
|
|
303
|
+
if (prevLine !== null && comment.loc.start.line !== prevLine + 1) break;
|
|
304
|
+
leading.push(comment);
|
|
305
|
+
prevLine = comment.loc.start.line;
|
|
306
|
+
}
|
|
307
|
+
const first = leading[0];
|
|
308
|
+
if (first === void 0 || leading.length < LEADING_PREAMBLE_MIN) return;
|
|
309
|
+
const isLicense = leading.some(
|
|
310
|
+
(c) => LICENSE_RE.test(stripCommentMarker(c.value))
|
|
311
|
+
);
|
|
312
|
+
if (!isLicense) {
|
|
313
|
+
context.report({ node: first, messageId: "fileHeaderPreamble" });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
Program() {
|
|
318
|
+
const comments = sourceCode.getAllComments();
|
|
319
|
+
const firstCodeLine = sourceCode.ast.tokens[0]?.loc.start.line ?? Number.MAX_SAFE_INTEGER;
|
|
320
|
+
for (const comment of comments) {
|
|
321
|
+
if (isJsDoc(comment) || !isStandalone(comment)) continue;
|
|
322
|
+
const texts = comment.value.split("\n").map(stripCommentMarker).filter((l) => l.length > 0 && !isDirective(l));
|
|
323
|
+
if (texts.some(isBanner)) {
|
|
324
|
+
context.report({ node: comment, messageId: "sectionBanner" });
|
|
325
|
+
} else if (texts.some(looksLikeCode)) {
|
|
326
|
+
context.report({ node: comment, messageId: "commentedOutCode" });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
reportLeadingPreamble(comments, firstCodeLine);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/rules/no-enum.ts
|
|
336
|
+
import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
|
|
256
337
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
257
338
|
/[\\/]generated[\\/]/,
|
|
258
339
|
/\.gen\.tsx?$/,
|
|
@@ -271,7 +352,7 @@ function hasGeneratedMarker(sourceText) {
|
|
|
271
352
|
const head = sourceText.slice(0, 1024);
|
|
272
353
|
return /@generated\b/.test(head);
|
|
273
354
|
}
|
|
274
|
-
var no_enum_default =
|
|
355
|
+
var no_enum_default = ESLintUtils4.RuleCreator(
|
|
275
356
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
276
357
|
)({
|
|
277
358
|
name: "no-enum",
|
|
@@ -321,9 +402,260 @@ var no_enum_default = ESLintUtils3.RuleCreator(
|
|
|
321
402
|
}
|
|
322
403
|
});
|
|
323
404
|
|
|
405
|
+
// src/rules/no-insecure-random-id.ts
|
|
406
|
+
import { ESLintUtils as ESLintUtils5 } from "@typescript-eslint/utils";
|
|
407
|
+
var NAME_PATTERN = /id|token|key|secret|uuid|nonce|session|password|salt/i;
|
|
408
|
+
function isMathRandomCall(node) {
|
|
409
|
+
if (node.type !== "CallExpression") {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
const callee = node.callee;
|
|
413
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const { object, property } = callee;
|
|
417
|
+
return object.type === "Identifier" && object.name === "Math" && property.type === "Identifier" && property.name === "random";
|
|
418
|
+
}
|
|
419
|
+
function isPartOfToString36Chain(node) {
|
|
420
|
+
let current = node;
|
|
421
|
+
let parent = current.parent;
|
|
422
|
+
while (parent) {
|
|
423
|
+
if (parent.type === "MemberExpression" && parent.object === current && !parent.computed && parent.property.type === "Identifier" && parent.property.name === "toString") {
|
|
424
|
+
const grandparent = parent.parent;
|
|
425
|
+
if (grandparent && grandparent.type === "CallExpression" && grandparent.callee === parent) {
|
|
426
|
+
const firstArg = grandparent.arguments[0];
|
|
427
|
+
if (firstArg && firstArg.type === "Literal" && firstArg.value === 36) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (parent.type === "MemberExpression" && parent.object === current) {
|
|
433
|
+
current = parent;
|
|
434
|
+
parent = current.parent;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (parent.type === "CallExpression" && parent.callee === current) {
|
|
438
|
+
current = parent;
|
|
439
|
+
parent = current.parent;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
function findEnclosingName(node) {
|
|
447
|
+
let current = node;
|
|
448
|
+
let parent = current.parent;
|
|
449
|
+
while (parent) {
|
|
450
|
+
if (parent.type === "VariableDeclarator" && parent.init === current) {
|
|
451
|
+
if (parent.id.type === "Identifier") {
|
|
452
|
+
return parent.id.name;
|
|
453
|
+
}
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
if (parent.type === "Property" && parent.value === current) {
|
|
457
|
+
const key = parent.key;
|
|
458
|
+
if (!parent.computed && key.type === "Identifier") {
|
|
459
|
+
return key.name;
|
|
460
|
+
}
|
|
461
|
+
if (key.type === "Literal" && typeof key.value === "string") {
|
|
462
|
+
return key.value;
|
|
463
|
+
}
|
|
464
|
+
return void 0;
|
|
465
|
+
}
|
|
466
|
+
if (parent.type === "PropertyDefinition" && parent.value === current) {
|
|
467
|
+
const key = parent.key;
|
|
468
|
+
if (!parent.computed && key.type === "Identifier") {
|
|
469
|
+
return key.name;
|
|
470
|
+
}
|
|
471
|
+
if (key.type === "Literal" && typeof key.value === "string") {
|
|
472
|
+
return key.value;
|
|
473
|
+
}
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression" || parent.type === "BlockStatement" || parent.type === "ReturnStatement" || parent.type === "ExpressionStatement") {
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
current = parent;
|
|
480
|
+
parent = current.parent;
|
|
481
|
+
}
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
var no_insecure_random_id_default = ESLintUtils5.RuleCreator(
|
|
485
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
486
|
+
)({
|
|
487
|
+
name: "no-insecure-random-id",
|
|
488
|
+
meta: {
|
|
489
|
+
type: "problem",
|
|
490
|
+
docs: {
|
|
491
|
+
description: "Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead."
|
|
492
|
+
},
|
|
493
|
+
schema: [],
|
|
494
|
+
messages: {
|
|
495
|
+
insecureRandomId: "`Math.random()` is not cryptographically secure and is predictable; do not use it to generate IDs, tokens, or secrets. Use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead."
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
defaultOptions: [],
|
|
499
|
+
create(context) {
|
|
500
|
+
return {
|
|
501
|
+
CallExpression(node) {
|
|
502
|
+
if (!isMathRandomCall(node)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (isPartOfToString36Chain(node)) {
|
|
506
|
+
context.report({ node, messageId: "insecureRandomId" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const name = findEnclosingName(node);
|
|
510
|
+
if (name !== void 0 && NAME_PATTERN.test(name)) {
|
|
511
|
+
context.report({ node, messageId: "insecureRandomId" });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// src/rules/no-json-stringify-error.ts
|
|
519
|
+
import { ESLintUtils as ESLintUtils6 } from "@typescript-eslint/utils";
|
|
520
|
+
var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
|
|
521
|
+
function isCatchBinding(scope, name) {
|
|
522
|
+
let current = scope;
|
|
523
|
+
while (current) {
|
|
524
|
+
const variable = current.set.get(name);
|
|
525
|
+
if (variable) {
|
|
526
|
+
for (const def of variable.defs) {
|
|
527
|
+
if (def.type === "CatchClause") {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
current = current.upper;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
function isJsonStringify(callee) {
|
|
537
|
+
return callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "JSON" && callee.property.type === "Identifier" && callee.property.name === "stringify";
|
|
538
|
+
}
|
|
539
|
+
var no_json_stringify_error_default = ESLintUtils6.RuleCreator(
|
|
540
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
541
|
+
)({
|
|
542
|
+
name: "no-json-stringify-error",
|
|
543
|
+
meta: {
|
|
544
|
+
type: "problem",
|
|
545
|
+
docs: {
|
|
546
|
+
description: "Disallow `JSON.stringify` on an Error value; it yields `{}` because `message`/`stack` are non-enumerable."
|
|
547
|
+
},
|
|
548
|
+
schema: [],
|
|
549
|
+
messages: {
|
|
550
|
+
noJsonStringifyError: "`JSON.stringify` on an Error yields `{}` because `message`/`stack` are non-enumerable. Log `err.message` / `err.stack`, or use a proper error serializer."
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
defaultOptions: [],
|
|
554
|
+
create(context) {
|
|
555
|
+
return {
|
|
556
|
+
CallExpression(node) {
|
|
557
|
+
if (!isJsonStringify(node.callee)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const firstArg = node.arguments[0];
|
|
561
|
+
if (!firstArg || firstArg.type !== "Identifier") {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const name = firstArg.name;
|
|
565
|
+
const scope = context.sourceCode.getScope(firstArg);
|
|
566
|
+
if (ERROR_NAME_PATTERN.test(name) || isCatchBinding(scope, name)) {
|
|
567
|
+
context.report({
|
|
568
|
+
node,
|
|
569
|
+
messageId: "noJsonStringifyError"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// src/rules/no-log-only-catch.ts
|
|
578
|
+
import { ESLintUtils as ESLintUtils7 } from "@typescript-eslint/utils";
|
|
579
|
+
var DEFAULT_IGNORE_PATTERNS2 = [
|
|
580
|
+
/\.test\./,
|
|
581
|
+
/\.spec\./,
|
|
582
|
+
/[\\/]__tests__[\\/]/
|
|
583
|
+
];
|
|
584
|
+
var CONSOLE_METHODS = /* @__PURE__ */ new Set([
|
|
585
|
+
"log",
|
|
586
|
+
"error",
|
|
587
|
+
"warn",
|
|
588
|
+
"info",
|
|
589
|
+
"debug"
|
|
590
|
+
]);
|
|
591
|
+
function isConsoleCallStatement(statement) {
|
|
592
|
+
if (statement.type !== "ExpressionStatement") {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
const expr = statement.expression;
|
|
596
|
+
if (expr.type !== "CallExpression") {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const callee = expr.callee;
|
|
600
|
+
if (callee.type !== "MemberExpression") {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
const { object, property } = callee;
|
|
604
|
+
if (object.type !== "Identifier" || object.name !== "console") {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
if (callee.computed) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
if (property.type !== "Identifier") {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
return CONSOLE_METHODS.has(property.name);
|
|
614
|
+
}
|
|
615
|
+
var no_log_only_catch_default = ESLintUtils7.RuleCreator(
|
|
616
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
617
|
+
)({
|
|
618
|
+
name: "no-log-only-catch",
|
|
619
|
+
meta: {
|
|
620
|
+
type: "problem",
|
|
621
|
+
docs: {
|
|
622
|
+
description: "Disallow `catch` clauses that only log (or do nothing) and then swallow the error; rethrow or handle it instead."
|
|
623
|
+
},
|
|
624
|
+
schema: [],
|
|
625
|
+
messages: {
|
|
626
|
+
noLogOnlyCatch: "Logging then swallowing the error hides failures. Rethrow the error or handle it for real."
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
defaultOptions: [],
|
|
630
|
+
create(context) {
|
|
631
|
+
const filename = context.filename;
|
|
632
|
+
const isIgnoredByDefault = DEFAULT_IGNORE_PATTERNS2.some(
|
|
633
|
+
(re) => re.test(filename)
|
|
634
|
+
);
|
|
635
|
+
if (isIgnoredByDefault) {
|
|
636
|
+
return {};
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
CatchClause(node) {
|
|
640
|
+
const statements = node.body.body;
|
|
641
|
+
if (statements.length === 0) {
|
|
642
|
+
context.report({ node, messageId: "noLogOnlyCatch" });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const everyStatementIsConsoleLog = statements.every(
|
|
646
|
+
(statement) => isConsoleCallStatement(statement)
|
|
647
|
+
);
|
|
648
|
+
if (everyStatementIsConsoleLog) {
|
|
649
|
+
context.report({ node, messageId: "noLogOnlyCatch" });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
324
656
|
// src/rules/no-raw-env.ts
|
|
325
|
-
import { ESLintUtils as
|
|
326
|
-
var no_raw_env_default =
|
|
657
|
+
import { ESLintUtils as ESLintUtils8 } from "@typescript-eslint/utils";
|
|
658
|
+
var no_raw_env_default = ESLintUtils8.RuleCreator(
|
|
327
659
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
328
660
|
)({
|
|
329
661
|
name: "no-raw-env",
|
|
@@ -355,10 +687,305 @@ var no_raw_env_default = ESLintUtils4.RuleCreator(
|
|
|
355
687
|
}
|
|
356
688
|
});
|
|
357
689
|
|
|
690
|
+
// src/rules/no-sentinel-return-on-catch.ts
|
|
691
|
+
import {
|
|
692
|
+
ESLintUtils as ESLintUtils9,
|
|
693
|
+
AST_NODE_TYPES as AST_NODE_TYPES3
|
|
694
|
+
} from "@typescript-eslint/utils";
|
|
695
|
+
function isSentinelArgument(arg) {
|
|
696
|
+
if (arg === null) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if (arg.type === AST_NODE_TYPES3.Literal && arg.value === null) {
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
if (arg.type === AST_NODE_TYPES3.Literal && arg.value === false) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
if (arg.type === AST_NODE_TYPES3.Identifier && arg.name === "undefined") {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
if (arg.type === AST_NODE_TYPES3.ArrayExpression && arg.elements.length === 0) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
if (arg.type === AST_NODE_TYPES3.ObjectExpression && arg.properties.length === 0) {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
function containsThrow(node) {
|
|
717
|
+
let found = false;
|
|
718
|
+
const visit = (current) => {
|
|
719
|
+
if (found) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (current.type === AST_NODE_TYPES3.ThrowStatement) {
|
|
723
|
+
found = true;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (current.type === AST_NODE_TYPES3.FunctionDeclaration || current.type === AST_NODE_TYPES3.FunctionExpression || current.type === AST_NODE_TYPES3.ArrowFunctionExpression) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
for (const key of Object.keys(current)) {
|
|
730
|
+
if (key === "parent") {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const value = current[key];
|
|
734
|
+
if (Array.isArray(value)) {
|
|
735
|
+
for (const child of value) {
|
|
736
|
+
if (isNode(child)) {
|
|
737
|
+
visit(child);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} else if (isNode(value)) {
|
|
741
|
+
visit(value);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
visit(node);
|
|
746
|
+
return found;
|
|
747
|
+
}
|
|
748
|
+
function isNode(value) {
|
|
749
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
750
|
+
}
|
|
751
|
+
var no_sentinel_return_on_catch_default = ESLintUtils9.RuleCreator(
|
|
752
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
753
|
+
)({
|
|
754
|
+
name: "no-sentinel-return-on-catch",
|
|
755
|
+
meta: {
|
|
756
|
+
type: "problem",
|
|
757
|
+
docs: {
|
|
758
|
+
description: "Disallow swallowing a caught error by returning an empty sentinel (`null`, `undefined`, `false`, `[]`, `{}`) as the final statement of a `catch` block."
|
|
759
|
+
},
|
|
760
|
+
schema: [],
|
|
761
|
+
messages: {
|
|
762
|
+
noSentinelReturn: "This `catch` block swallows the error by returning an empty sentinel. Rethrow it, return a typed Result, or handle the error explicitly."
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
defaultOptions: [],
|
|
766
|
+
create(context) {
|
|
767
|
+
return {
|
|
768
|
+
CatchClause(node) {
|
|
769
|
+
const body = node.body.body;
|
|
770
|
+
if (body.length === 0) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const last = body[body.length - 1];
|
|
774
|
+
if (last === void 0 || last.type !== AST_NODE_TYPES3.ReturnStatement) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!isSentinelArgument(last.argument)) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (containsThrow(node.body)) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
context.report({
|
|
784
|
+
node: last,
|
|
785
|
+
messageId: "noSentinelReturn"
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// src/rules/no-sequential-await.ts
|
|
793
|
+
import { ESLintUtils as ESLintUtils10 } from "@typescript-eslint/utils";
|
|
794
|
+
function isFunctionLike(node) {
|
|
795
|
+
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
796
|
+
}
|
|
797
|
+
function isLoop(node) {
|
|
798
|
+
return node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement" || node.type === "WhileStatement" || node.type === "DoWhileStatement";
|
|
799
|
+
}
|
|
800
|
+
var no_sequential_await_default = ESLintUtils10.RuleCreator(
|
|
801
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
802
|
+
)({
|
|
803
|
+
name: "no-sequential-await",
|
|
804
|
+
meta: {
|
|
805
|
+
type: "problem",
|
|
806
|
+
docs: {
|
|
807
|
+
description: "Disallow serial `await` inside a loop; use `await Promise.all(...)` to run the operations concurrently."
|
|
808
|
+
},
|
|
809
|
+
schema: [],
|
|
810
|
+
messages: {
|
|
811
|
+
noSequentialAwait: "Avoid `await` inside a loop \u2014 it serializes I/O. Collect the promises and `await Promise.all(xs.map(async (x) => ...))` instead."
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
defaultOptions: [],
|
|
815
|
+
create(context) {
|
|
816
|
+
function findAwaitInScope(node) {
|
|
817
|
+
if (node.type === "AwaitExpression") {
|
|
818
|
+
return node;
|
|
819
|
+
}
|
|
820
|
+
if (isFunctionLike(node)) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
for (const key of Object.keys(node)) {
|
|
824
|
+
if (key === "parent") {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const value = node[key];
|
|
828
|
+
if (Array.isArray(value)) {
|
|
829
|
+
for (const child of value) {
|
|
830
|
+
if (isNode2(child) && !isLoop(child)) {
|
|
831
|
+
const found = findAwaitInScope(child);
|
|
832
|
+
if (found) {
|
|
833
|
+
return found;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} else if (isNode2(value) && !isLoop(value)) {
|
|
838
|
+
const found = findAwaitInScope(value);
|
|
839
|
+
if (found) {
|
|
840
|
+
return found;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
function isNode2(value) {
|
|
847
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
848
|
+
}
|
|
849
|
+
function checkLoop(node) {
|
|
850
|
+
const parts = [node.body];
|
|
851
|
+
if (node.type === "ForStatement") {
|
|
852
|
+
parts.push(node.init, node.test, node.update);
|
|
853
|
+
} else if (node.type === "ForOfStatement" || node.type === "ForInStatement") {
|
|
854
|
+
parts.push(node.right);
|
|
855
|
+
} else {
|
|
856
|
+
parts.push(node.test);
|
|
857
|
+
}
|
|
858
|
+
for (const part of parts) {
|
|
859
|
+
if (part && !isLoop(part) && findAwaitInScope(part)) {
|
|
860
|
+
context.report({ node, messageId: "noSequentialAwait" });
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
ForStatement: checkLoop,
|
|
867
|
+
ForInStatement: checkLoop,
|
|
868
|
+
WhileStatement: checkLoop,
|
|
869
|
+
DoWhileStatement: checkLoop,
|
|
870
|
+
ForOfStatement(node) {
|
|
871
|
+
if (node.await) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
checkLoop(node);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// src/rules/no-string-concat-in-loop.ts
|
|
881
|
+
import { ESLintUtils as ESLintUtils11 } from "@typescript-eslint/utils";
|
|
882
|
+
var LOOP_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
883
|
+
"ForStatement",
|
|
884
|
+
"ForOfStatement",
|
|
885
|
+
"ForInStatement",
|
|
886
|
+
"WhileStatement",
|
|
887
|
+
"DoWhileStatement"
|
|
888
|
+
]);
|
|
889
|
+
function isStringLiteralInit(node) {
|
|
890
|
+
if (node === null) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
if (node.type === "TemplateLiteral") {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
if (node.type === "Literal") {
|
|
897
|
+
return typeof node.value === "string";
|
|
898
|
+
}
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
function findVariable(scope, name) {
|
|
902
|
+
let current = scope;
|
|
903
|
+
while (current !== null) {
|
|
904
|
+
const variable = current.variables.find((v) => v.name === name);
|
|
905
|
+
if (variable !== void 0) {
|
|
906
|
+
return variable;
|
|
907
|
+
}
|
|
908
|
+
current = current.upper;
|
|
909
|
+
}
|
|
910
|
+
return void 0;
|
|
911
|
+
}
|
|
912
|
+
function isStringInitializedVariable(variable) {
|
|
913
|
+
if (variable.defs.length !== 1) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
const def = variable.defs[0];
|
|
917
|
+
if (def === void 0 || def.type !== "Variable") {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
const declarator = def.node;
|
|
921
|
+
if (declarator.type !== "VariableDeclarator") {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
return isStringLiteralInit(declarator.init);
|
|
925
|
+
}
|
|
926
|
+
function isInsideLoopBody(node) {
|
|
927
|
+
let child = node;
|
|
928
|
+
let parent = node.parent;
|
|
929
|
+
while (parent !== void 0 && parent !== null) {
|
|
930
|
+
if (LOOP_NODE_TYPES.has(parent.type)) {
|
|
931
|
+
const loop = parent;
|
|
932
|
+
if (loop.body === child) {
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
child = parent;
|
|
937
|
+
parent = parent.parent;
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
var no_string_concat_in_loop_default = ESLintUtils11.RuleCreator(
|
|
942
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
943
|
+
)({
|
|
944
|
+
name: "no-string-concat-in-loop",
|
|
945
|
+
meta: {
|
|
946
|
+
type: "suggestion",
|
|
947
|
+
docs: {
|
|
948
|
+
description: "Disallow O(n^2) string building via `+=` on a string variable inside a loop; push parts to an array and `join` instead."
|
|
949
|
+
},
|
|
950
|
+
schema: [],
|
|
951
|
+
messages: {
|
|
952
|
+
noStringConcatInLoop: 'Avoid building a string with `+=` inside a loop \u2014 this is O(n^2). Push the parts onto an array and use `arr.join("")` after the loop.'
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
defaultOptions: [],
|
|
956
|
+
create(context) {
|
|
957
|
+
return {
|
|
958
|
+
AssignmentExpression(node) {
|
|
959
|
+
if (node.operator !== "+=") {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (node.left.type !== "Identifier") {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (!isInsideLoopBody(node)) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const scope = context.sourceCode.getScope(node);
|
|
969
|
+
const variable = findVariable(scope, node.left.name);
|
|
970
|
+
if (variable === void 0) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (!isStringInitializedVariable(variable)) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
context.report({
|
|
977
|
+
node,
|
|
978
|
+
messageId: "noStringConcatInLoop"
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
|
|
358
985
|
// src/rules/no-unnecessary-use-client.ts
|
|
359
986
|
import {
|
|
360
|
-
AST_NODE_TYPES as
|
|
361
|
-
ESLintUtils as
|
|
987
|
+
AST_NODE_TYPES as AST_NODE_TYPES4,
|
|
988
|
+
ESLintUtils as ESLintUtils12
|
|
362
989
|
} from "@typescript-eslint/utils";
|
|
363
990
|
var HOOK_REGEX = /^use([A-Z]|$)/;
|
|
364
991
|
var EVENT_PROP_REGEX = /^on[A-Z]/;
|
|
@@ -382,16 +1009,16 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
382
1009
|
]);
|
|
383
1010
|
var CLIENT_ONLY_PACKAGES_REGEX = /^(?:@radix-ui\/|framer-motion|react-dom|react-day-picker|@floating-ui\/|react-select|react-toastify|react-hook-form|recharts|react-dropzone|react-slick|react-swipeable|react-resizable|react-draggable|react-beautiful-dnd|@hello-pangea\/dnd|react-virtualized|react-window|@tanstack\/react-table|@tanstack\/react-query|react-redux|recoil|jotai|zustand|@tippyjs\/react|react-color|react-datepicker|next-themes|react-helmet|react-helmet-async|styled-components|@emotion\/)/;
|
|
384
1011
|
var isUseClientDirective = (node) => {
|
|
385
|
-
return node.type ===
|
|
1012
|
+
return node.type === AST_NODE_TYPES4.ExpressionStatement && node.expression.type === AST_NODE_TYPES4.Literal && node.expression.value === "use client";
|
|
386
1013
|
};
|
|
387
1014
|
var isGlobalReference = (node, context) => {
|
|
388
1015
|
if (!BROWSER_GLOBALS.has(node.name)) return false;
|
|
389
1016
|
const parent = node.parent;
|
|
390
1017
|
if (parent !== void 0) {
|
|
391
|
-
if (parent.type ===
|
|
1018
|
+
if (parent.type === AST_NODE_TYPES4.MemberExpression && parent.property === node && !parent.computed) {
|
|
392
1019
|
return false;
|
|
393
1020
|
}
|
|
394
|
-
if (parent.type ===
|
|
1021
|
+
if (parent.type === AST_NODE_TYPES4.Property && parent.key === node && !parent.computed) {
|
|
395
1022
|
return false;
|
|
396
1023
|
}
|
|
397
1024
|
if (parent.type.startsWith("TS")) {
|
|
@@ -408,7 +1035,7 @@ var isGlobalReference = (node, context) => {
|
|
|
408
1035
|
}
|
|
409
1036
|
return true;
|
|
410
1037
|
};
|
|
411
|
-
var no_unnecessary_use_client_default =
|
|
1038
|
+
var no_unnecessary_use_client_default = ESLintUtils12.RuleCreator(
|
|
412
1039
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
413
1040
|
)({
|
|
414
1041
|
name: "no-unnecessary-use-client",
|
|
@@ -431,13 +1058,13 @@ var no_unnecessary_use_client_default = ESLintUtils5.RuleCreator(
|
|
|
431
1058
|
let directiveNode = null;
|
|
432
1059
|
let hasClientIndicator = false;
|
|
433
1060
|
const markIfHookOrContext = (callee) => {
|
|
434
|
-
if (callee.type ===
|
|
1061
|
+
if (callee.type === AST_NODE_TYPES4.Identifier) {
|
|
435
1062
|
if (HOOK_REGEX.test(callee.name) || callee.name === "createContext") {
|
|
436
1063
|
hasClientIndicator = true;
|
|
437
1064
|
}
|
|
438
1065
|
return;
|
|
439
1066
|
}
|
|
440
|
-
if (callee.type ===
|
|
1067
|
+
if (callee.type === AST_NODE_TYPES4.MemberExpression && callee.property.type === AST_NODE_TYPES4.Identifier) {
|
|
441
1068
|
const name = callee.property.name;
|
|
442
1069
|
if (HOOK_REGEX.test(name) || name === "createContext") {
|
|
443
1070
|
hasClientIndicator = true;
|
|
@@ -447,7 +1074,7 @@ var no_unnecessary_use_client_default = ESLintUtils5.RuleCreator(
|
|
|
447
1074
|
return {
|
|
448
1075
|
Program(node) {
|
|
449
1076
|
for (const stmt of node.body) {
|
|
450
|
-
if (stmt.type !==
|
|
1077
|
+
if (stmt.type !== AST_NODE_TYPES4.ExpressionStatement) break;
|
|
451
1078
|
if (isUseClientDirective(stmt)) {
|
|
452
1079
|
directiveNode = stmt;
|
|
453
1080
|
break;
|
|
@@ -455,35 +1082,43 @@ var no_unnecessary_use_client_default = ESLintUtils5.RuleCreator(
|
|
|
455
1082
|
}
|
|
456
1083
|
},
|
|
457
1084
|
CallExpression(node) {
|
|
1085
|
+
if (directiveNode === null) return;
|
|
458
1086
|
markIfHookOrContext(node.callee);
|
|
459
1087
|
},
|
|
460
1088
|
JSXAttribute(node) {
|
|
461
|
-
if (
|
|
1089
|
+
if (directiveNode === null) return;
|
|
1090
|
+
if (node.name.type === AST_NODE_TYPES4.JSXIdentifier && EVENT_PROP_REGEX.test(node.name.name)) {
|
|
462
1091
|
hasClientIndicator = true;
|
|
463
1092
|
}
|
|
464
1093
|
},
|
|
465
1094
|
ImportDeclaration(node) {
|
|
1095
|
+
if (directiveNode === null) return;
|
|
466
1096
|
if (typeof node.source.value === "string" && CLIENT_ONLY_PACKAGES_REGEX.test(node.source.value)) {
|
|
467
1097
|
hasClientIndicator = true;
|
|
468
1098
|
}
|
|
469
1099
|
},
|
|
470
1100
|
ExportNamedDeclaration(node) {
|
|
1101
|
+
if (directiveNode === null) return;
|
|
471
1102
|
if (node.source !== null) {
|
|
472
1103
|
hasClientIndicator = true;
|
|
473
1104
|
}
|
|
474
1105
|
},
|
|
475
1106
|
ExportAllDeclaration(node) {
|
|
1107
|
+
if (directiveNode === null) return;
|
|
476
1108
|
if (node.source !== null) {
|
|
477
1109
|
hasClientIndicator = true;
|
|
478
1110
|
}
|
|
479
1111
|
},
|
|
480
1112
|
ClassDeclaration() {
|
|
1113
|
+
if (directiveNode === null) return;
|
|
481
1114
|
hasClientIndicator = true;
|
|
482
1115
|
},
|
|
483
1116
|
ClassExpression() {
|
|
1117
|
+
if (directiveNode === null) return;
|
|
484
1118
|
hasClientIndicator = true;
|
|
485
1119
|
},
|
|
486
1120
|
Identifier(node) {
|
|
1121
|
+
if (directiveNode === null) return;
|
|
487
1122
|
if (isGlobalReference(node, context)) {
|
|
488
1123
|
hasClientIndicator = true;
|
|
489
1124
|
}
|
|
@@ -500,17 +1135,101 @@ var no_unnecessary_use_client_default = ESLintUtils5.RuleCreator(
|
|
|
500
1135
|
}
|
|
501
1136
|
});
|
|
502
1137
|
|
|
1138
|
+
// src/rules/prefer-discriminated-union.ts
|
|
1139
|
+
import { ESLintUtils as ESLintUtils13 } from "@typescript-eslint/utils";
|
|
1140
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
|
|
1141
|
+
var STATUS_MEMBER_NAMES = /* @__PURE__ */ new Set([
|
|
1142
|
+
"success",
|
|
1143
|
+
"ok",
|
|
1144
|
+
"error",
|
|
1145
|
+
"failed",
|
|
1146
|
+
"isError"
|
|
1147
|
+
]);
|
|
1148
|
+
var MIN_OPTIONAL_MEMBERS = 2;
|
|
1149
|
+
function getMemberName(member) {
|
|
1150
|
+
if (member.type !== AST_NODE_TYPES5.TSPropertySignature) {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const { key } = member;
|
|
1154
|
+
if (key.type === AST_NODE_TYPES5.Identifier) {
|
|
1155
|
+
return key.name;
|
|
1156
|
+
}
|
|
1157
|
+
if (key.type === AST_NODE_TYPES5.Literal && typeof key.value === "string") {
|
|
1158
|
+
return key.value;
|
|
1159
|
+
}
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
function isBooleanTyped(member) {
|
|
1163
|
+
return member.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES5.TSBooleanKeyword;
|
|
1164
|
+
}
|
|
1165
|
+
function looksLikeMutuallyExclusiveState(typeLiteral) {
|
|
1166
|
+
let hasStatusBoolean = false;
|
|
1167
|
+
let optionalCount = 0;
|
|
1168
|
+
for (const member of typeLiteral.members) {
|
|
1169
|
+
if (member.type !== AST_NODE_TYPES5.TSPropertySignature) {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (member.optional) {
|
|
1173
|
+
optionalCount += 1;
|
|
1174
|
+
}
|
|
1175
|
+
const name = getMemberName(member);
|
|
1176
|
+
if (name !== null && STATUS_MEMBER_NAMES.has(name) && isBooleanTyped(member)) {
|
|
1177
|
+
hasStatusBoolean = true;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return hasStatusBoolean && optionalCount >= MIN_OPTIONAL_MEMBERS;
|
|
1181
|
+
}
|
|
1182
|
+
var prefer_discriminated_union_default = ESLintUtils13.RuleCreator(
|
|
1183
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
1184
|
+
)({
|
|
1185
|
+
name: "prefer-discriminated-union",
|
|
1186
|
+
meta: {
|
|
1187
|
+
type: "suggestion",
|
|
1188
|
+
docs: {
|
|
1189
|
+
description: "Flag object types with a boolean status flag and many optionals; model them as a discriminated union instead."
|
|
1190
|
+
},
|
|
1191
|
+
schema: [],
|
|
1192
|
+
messages: {
|
|
1193
|
+
preferDiscriminatedUnion: "This object type uses a boolean status flag alongside several optional fields, which lets illegal states be representable. Model it as a `z.discriminatedUnion` / discriminated union (e.g. `{ ok: true; data: T } | { ok: false; error: E }`) to make illegal states unrepresentable."
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
defaultOptions: [],
|
|
1197
|
+
create(context) {
|
|
1198
|
+
function checkTypeLiteral(typeLiteral, reportNode) {
|
|
1199
|
+
if (looksLikeMutuallyExclusiveState(typeLiteral)) {
|
|
1200
|
+
context.report({
|
|
1201
|
+
node: reportNode,
|
|
1202
|
+
messageId: "preferDiscriminatedUnion"
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
TSInterfaceDeclaration(node) {
|
|
1208
|
+
const synthetic = {
|
|
1209
|
+
...node.body,
|
|
1210
|
+
type: AST_NODE_TYPES5.TSTypeLiteral,
|
|
1211
|
+
members: node.body.body
|
|
1212
|
+
};
|
|
1213
|
+
checkTypeLiteral(synthetic, node);
|
|
1214
|
+
},
|
|
1215
|
+
"TSTypeAliasDeclaration > TSTypeLiteral"(node) {
|
|
1216
|
+
checkTypeLiteral(node, node.parent);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
503
1222
|
// src/rules/prefer-schema-for-api-payload.ts
|
|
504
1223
|
import {
|
|
505
|
-
AST_NODE_TYPES as
|
|
506
|
-
ESLintUtils as
|
|
1224
|
+
AST_NODE_TYPES as AST_NODE_TYPES6,
|
|
1225
|
+
ESLintUtils as ESLintUtils14
|
|
507
1226
|
} from "@typescript-eslint/utils";
|
|
508
1227
|
var unwrap = (node) => {
|
|
509
1228
|
let current = node;
|
|
510
1229
|
while (current !== null && current !== void 0) {
|
|
511
|
-
if (current.type ===
|
|
1230
|
+
if (current.type === AST_NODE_TYPES6.TSAsExpression || current.type === AST_NODE_TYPES6.TSTypeAssertion || current.type === AST_NODE_TYPES6.TSNonNullExpression || current.type === AST_NODE_TYPES6.TSSatisfiesExpression) {
|
|
512
1231
|
current = current.expression;
|
|
513
|
-
} else if (current.type ===
|
|
1232
|
+
} else if (current.type === AST_NODE_TYPES6.ChainExpression) {
|
|
514
1233
|
current = current.expression;
|
|
515
1234
|
} else {
|
|
516
1235
|
break;
|
|
@@ -521,20 +1240,20 @@ var unwrap = (node) => {
|
|
|
521
1240
|
var isJsonCall = (node) => {
|
|
522
1241
|
let current = unwrap(node);
|
|
523
1242
|
if (current === null) return false;
|
|
524
|
-
if (current.type ===
|
|
1243
|
+
if (current.type === AST_NODE_TYPES6.AwaitExpression) {
|
|
525
1244
|
current = unwrap(current.argument);
|
|
526
1245
|
}
|
|
527
|
-
if (current === null || current.type !==
|
|
1246
|
+
if (current === null || current.type !== AST_NODE_TYPES6.CallExpression) {
|
|
528
1247
|
return false;
|
|
529
1248
|
}
|
|
530
1249
|
const callee = unwrap(current.callee);
|
|
531
|
-
if (callee === null || callee.type !==
|
|
1250
|
+
if (callee === null || callee.type !== AST_NODE_TYPES6.MemberExpression) {
|
|
532
1251
|
return false;
|
|
533
1252
|
}
|
|
534
1253
|
const property = unwrap(callee.property);
|
|
535
|
-
return property !== null && property.type ===
|
|
1254
|
+
return property !== null && property.type === AST_NODE_TYPES6.Identifier && property.name === "json";
|
|
536
1255
|
};
|
|
537
|
-
var
|
|
1256
|
+
var findVariable2 = (scope, name) => {
|
|
538
1257
|
let current = scope;
|
|
539
1258
|
while (current !== null) {
|
|
540
1259
|
const variable = current.set.get(name);
|
|
@@ -545,13 +1264,13 @@ var findVariable = (scope, name) => {
|
|
|
545
1264
|
};
|
|
546
1265
|
var isUnvalidatedVariableRef = (node, scope, tracked) => {
|
|
547
1266
|
const unwrapped = unwrap(node);
|
|
548
|
-
if (unwrapped === null || unwrapped.type !==
|
|
1267
|
+
if (unwrapped === null || unwrapped.type !== AST_NODE_TYPES6.Identifier) {
|
|
549
1268
|
return false;
|
|
550
1269
|
}
|
|
551
|
-
const variable =
|
|
1270
|
+
const variable = findVariable2(scope, unwrapped.name);
|
|
552
1271
|
return variable !== null && tracked.has(variable);
|
|
553
1272
|
};
|
|
554
|
-
var prefer_schema_for_api_payload_default =
|
|
1273
|
+
var prefer_schema_for_api_payload_default = ESLintUtils14.RuleCreator(
|
|
555
1274
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
556
1275
|
)({
|
|
557
1276
|
name: "prefer-schema-for-api-payload",
|
|
@@ -579,11 +1298,11 @@ var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
|
579
1298
|
return {
|
|
580
1299
|
VariableDeclarator(node) {
|
|
581
1300
|
const scope = context.sourceCode.getScope(node);
|
|
582
|
-
if (node.id.type ===
|
|
1301
|
+
if (node.id.type === AST_NODE_TYPES6.Identifier) {
|
|
583
1302
|
trackInitializer(node);
|
|
584
1303
|
return;
|
|
585
1304
|
}
|
|
586
|
-
if (node.id.type ===
|
|
1305
|
+
if (node.id.type === AST_NODE_TYPES6.ObjectPattern || node.id.type === AST_NODE_TYPES6.ArrayPattern) {
|
|
587
1306
|
if (isJsonCall(node.init)) {
|
|
588
1307
|
context.report({ node: node.id, messageId: "unparsedJsonAccess" });
|
|
589
1308
|
return;
|
|
@@ -595,8 +1314,8 @@ var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
|
595
1314
|
},
|
|
596
1315
|
AssignmentExpression(node) {
|
|
597
1316
|
const scope = context.sourceCode.getScope(node);
|
|
598
|
-
if (node.left.type ===
|
|
599
|
-
const variable =
|
|
1317
|
+
if (node.left.type === AST_NODE_TYPES6.Identifier) {
|
|
1318
|
+
const variable = findVariable2(scope, node.left.name);
|
|
600
1319
|
if (variable === null) return;
|
|
601
1320
|
if (isJsonCall(node.right)) {
|
|
602
1321
|
unvalidatedVariables.add(variable);
|
|
@@ -605,7 +1324,7 @@ var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
|
605
1324
|
}
|
|
606
1325
|
return;
|
|
607
1326
|
}
|
|
608
|
-
if (node.left.type ===
|
|
1327
|
+
if (node.left.type === AST_NODE_TYPES6.ObjectPattern || node.left.type === AST_NODE_TYPES6.ArrayPattern) {
|
|
609
1328
|
if (isJsonCall(node.right)) {
|
|
610
1329
|
context.report({
|
|
611
1330
|
node: node.left,
|
|
@@ -626,15 +1345,15 @@ var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
|
626
1345
|
const obj = unwrap(node.object);
|
|
627
1346
|
if (isJsonCall(obj)) {
|
|
628
1347
|
const parent = node.parent;
|
|
629
|
-
if (parent.type ===
|
|
1348
|
+
if (parent.type === AST_NODE_TYPES6.CallExpression && parent.callee === node && node.property.type === AST_NODE_TYPES6.Identifier && (node.property.name === "parse" || node.property.name === "safeParse")) {
|
|
630
1349
|
return;
|
|
631
1350
|
}
|
|
632
1351
|
context.report({ node, messageId: "unparsedJsonAccess" });
|
|
633
1352
|
return;
|
|
634
1353
|
}
|
|
635
|
-
if (obj !== null && obj.type ===
|
|
1354
|
+
if (obj !== null && obj.type === AST_NODE_TYPES6.Identifier && isUnvalidatedVariableRef(obj, scope, unvalidatedVariables)) {
|
|
636
1355
|
context.report({ node, messageId: "unparsedJsonAccess" });
|
|
637
|
-
const variable =
|
|
1356
|
+
const variable = findVariable2(scope, obj.name);
|
|
638
1357
|
if (variable !== null) {
|
|
639
1358
|
unvalidatedVariables.delete(variable);
|
|
640
1359
|
}
|
|
@@ -644,8 +1363,153 @@ var prefer_schema_for_api_payload_default = ESLintUtils6.RuleCreator(
|
|
|
644
1363
|
}
|
|
645
1364
|
});
|
|
646
1365
|
|
|
1366
|
+
// src/rules/prefer-semantic-colors.ts
|
|
1367
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES7, ESLintUtils as ESLintUtils15 } from "@typescript-eslint/utils";
|
|
1368
|
+
|
|
1369
|
+
// src/rules/_tailwind.ts
|
|
1370
|
+
var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
|
|
1371
|
+
var classTokens = (value) => value.split(/\s+/).filter(Boolean);
|
|
1372
|
+
|
|
1373
|
+
// src/rules/prefer-semantic-colors.ts
|
|
1374
|
+
var COLOR_PREFIXES = "text|bg|border(?:-[trblxyse])?|ring(?:-offset)?|fill|stroke|from|via|to|divide|decoration|placeholder|accent|caret|shadow|outline";
|
|
1375
|
+
var PALETTE = "red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone";
|
|
1376
|
+
var COLOR_FN = "rgba?|hsla?|hwb|oklch|oklab|lab|lch|color";
|
|
1377
|
+
var RAW_PALETTE_RE = new RegExp(`^(?:${COLOR_PREFIXES})-(?:${PALETTE})-\\d{2,3}(?:/\\d{1,3})?$`);
|
|
1378
|
+
var ARBITRARY_COLOR_RE = new RegExp(
|
|
1379
|
+
`^(?:${COLOR_PREFIXES})-\\[(?:#[0-9a-fA-F]{3,8}|(?:${COLOR_FN})\\([^\\]]*\\))\\]$`,
|
|
1380
|
+
"i"
|
|
1381
|
+
);
|
|
1382
|
+
var CLASS_FNS = /* @__PURE__ */ new Set(["cn", "clsx", "cva", "tv", "cx", "twMerge", "classnames", "classNames"]);
|
|
1383
|
+
var CLASS_NAME_RE = /class/i;
|
|
1384
|
+
var STYLE_COLOR_PROPS = /* @__PURE__ */ new Set([
|
|
1385
|
+
"color",
|
|
1386
|
+
"background",
|
|
1387
|
+
"backgroundColor",
|
|
1388
|
+
"borderColor",
|
|
1389
|
+
"borderTopColor",
|
|
1390
|
+
"borderRightColor",
|
|
1391
|
+
"borderBottomColor",
|
|
1392
|
+
"borderLeftColor",
|
|
1393
|
+
"outlineColor",
|
|
1394
|
+
"caretColor",
|
|
1395
|
+
"textDecorationColor",
|
|
1396
|
+
"columnRuleColor",
|
|
1397
|
+
"fill",
|
|
1398
|
+
"stroke",
|
|
1399
|
+
"stopColor",
|
|
1400
|
+
"floodColor",
|
|
1401
|
+
"lightingColor"
|
|
1402
|
+
]);
|
|
1403
|
+
var RAW_COLOR_VALUE_RE = new RegExp(`#[0-9a-fA-F]{3,8}\\b|\\b(?:${COLOR_FN})\\s*\\(`, "i");
|
|
1404
|
+
var propName = (key) => {
|
|
1405
|
+
if (key.type === AST_NODE_TYPES7.Identifier) return key.name;
|
|
1406
|
+
if (key.type === AST_NODE_TYPES7.Literal && typeof key.value === "string") return key.value;
|
|
1407
|
+
return null;
|
|
1408
|
+
};
|
|
1409
|
+
var prefer_semantic_colors_default = ESLintUtils15.RuleCreator(
|
|
1410
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
1411
|
+
)({
|
|
1412
|
+
name: "prefer-semantic-colors",
|
|
1413
|
+
meta: {
|
|
1414
|
+
type: "suggestion",
|
|
1415
|
+
docs: {
|
|
1416
|
+
description: "Enforce design-system semantic color tokens (bg-primary, text-destructive, \u2026) over raw Tailwind palette classes (text-red-500), arbitrary color values (bg-[#fff]), and inline color literals."
|
|
1417
|
+
},
|
|
1418
|
+
schema: [],
|
|
1419
|
+
messages: {
|
|
1420
|
+
rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
|
|
1421
|
+
arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
|
|
1422
|
+
inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
defaultOptions: [],
|
|
1426
|
+
create(context) {
|
|
1427
|
+
const reportClasses = (value, node) => {
|
|
1428
|
+
for (const token of classTokens(value)) {
|
|
1429
|
+
const base = tailwindBase(token);
|
|
1430
|
+
if (RAW_PALETTE_RE.test(base)) {
|
|
1431
|
+
context.report({ node, messageId: "rawPalette", data: { class: token } });
|
|
1432
|
+
} else if (ARBITRARY_COLOR_RE.test(base)) {
|
|
1433
|
+
context.report({ node, messageId: "arbitraryColor", data: { class: token } });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
const checkClassNode = (node) => {
|
|
1438
|
+
if (node === null) return;
|
|
1439
|
+
switch (node.type) {
|
|
1440
|
+
case AST_NODE_TYPES7.Literal:
|
|
1441
|
+
if (typeof node.value === "string") reportClasses(node.value, node);
|
|
1442
|
+
break;
|
|
1443
|
+
case AST_NODE_TYPES7.TemplateLiteral:
|
|
1444
|
+
for (const quasi of node.quasis) reportClasses(quasi.value.cooked ?? "", quasi);
|
|
1445
|
+
break;
|
|
1446
|
+
case AST_NODE_TYPES7.ArrayExpression:
|
|
1447
|
+
for (const element of node.elements) {
|
|
1448
|
+
if (element !== null && element.type !== AST_NODE_TYPES7.SpreadElement) checkClassNode(element);
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
case AST_NODE_TYPES7.ObjectExpression:
|
|
1452
|
+
for (const property of node.properties) {
|
|
1453
|
+
if (property.type === AST_NODE_TYPES7.Property) checkClassNode(property.value);
|
|
1454
|
+
}
|
|
1455
|
+
break;
|
|
1456
|
+
case AST_NODE_TYPES7.ConditionalExpression:
|
|
1457
|
+
checkClassNode(node.consequent);
|
|
1458
|
+
checkClassNode(node.alternate);
|
|
1459
|
+
break;
|
|
1460
|
+
case AST_NODE_TYPES7.LogicalExpression:
|
|
1461
|
+
checkClassNode(node.right);
|
|
1462
|
+
break;
|
|
1463
|
+
default:
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
const checkColorValueNode = (node) => {
|
|
1468
|
+
if (node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string" && RAW_COLOR_VALUE_RE.test(node.value)) {
|
|
1469
|
+
context.report({ node, messageId: "inlineColor", data: { value: node.value } });
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
return {
|
|
1473
|
+
"JSXAttribute[name.name='className']"(node) {
|
|
1474
|
+
if (node.value === null) return;
|
|
1475
|
+
if (node.value.type === AST_NODE_TYPES7.Literal) checkClassNode(node.value);
|
|
1476
|
+
else if (node.value.type === AST_NODE_TYPES7.JSXExpressionContainer) {
|
|
1477
|
+
if (node.value.expression.type !== AST_NODE_TYPES7.JSXEmptyExpression) {
|
|
1478
|
+
checkClassNode(node.value.expression);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
CallExpression(node) {
|
|
1483
|
+
if (node.callee.type === AST_NODE_TYPES7.Identifier && CLASS_FNS.has(node.callee.name)) {
|
|
1484
|
+
for (const arg of node.arguments) {
|
|
1485
|
+
if (arg.type !== AST_NODE_TYPES7.SpreadElement) checkClassNode(arg);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
VariableDeclarator(node) {
|
|
1490
|
+
if (node.id.type === AST_NODE_TYPES7.Identifier && CLASS_NAME_RE.test(node.id.name)) {
|
|
1491
|
+
checkClassNode(node.init);
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
Property(node) {
|
|
1495
|
+
const name = propName(node.key);
|
|
1496
|
+
if (name !== null && CLASS_NAME_RE.test(name)) checkClassNode(node.value);
|
|
1497
|
+
},
|
|
1498
|
+
// SVG presentation attributes: <path fill="#000" stroke="#fff" />
|
|
1499
|
+
"JSXAttribute[name.name=/^(fill|stroke|color)$/]"(node) {
|
|
1500
|
+
if (node.value?.type === AST_NODE_TYPES7.Literal) checkColorValueNode(node.value);
|
|
1501
|
+
},
|
|
1502
|
+
// Inline style objects: style={{ color: "#111827", backgroundColor: "#fff" }}
|
|
1503
|
+
"JSXAttribute[name.name='style'] ObjectExpression > Property"(node) {
|
|
1504
|
+
const name = propName(node.key);
|
|
1505
|
+
if (name !== null && STYLE_COLOR_PROPS.has(name)) checkColorValueNode(node.value);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
647
1511
|
// src/rules/prefer-server-actions.ts
|
|
648
|
-
import { ESLintUtils as
|
|
1512
|
+
import { ESLintUtils as ESLintUtils16 } from "@typescript-eslint/utils";
|
|
649
1513
|
var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
650
1514
|
var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
651
1515
|
var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
@@ -705,7 +1569,7 @@ function isMutationMethod(node, context) {
|
|
|
705
1569
|
}
|
|
706
1570
|
return false;
|
|
707
1571
|
}
|
|
708
|
-
function getPropertyNode(objNode,
|
|
1572
|
+
function getPropertyNode(objNode, propName2) {
|
|
709
1573
|
if (!objNode || objNode.type !== "ObjectExpression") return null;
|
|
710
1574
|
for (const prop of objNode.properties) {
|
|
711
1575
|
if (prop.type !== "Property") continue;
|
|
@@ -715,7 +1579,7 @@ function getPropertyNode(objNode, propName) {
|
|
|
715
1579
|
} else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
|
|
716
1580
|
keyName = prop.key.value;
|
|
717
1581
|
}
|
|
718
|
-
if (keyName ===
|
|
1582
|
+
if (keyName === propName2) {
|
|
719
1583
|
if (prop.value.type === "AssignmentPattern" || prop.value.type === "ArrayPattern" || prop.value.type === "ObjectPattern") {
|
|
720
1584
|
return null;
|
|
721
1585
|
}
|
|
@@ -724,7 +1588,7 @@ function getPropertyNode(objNode, propName) {
|
|
|
724
1588
|
}
|
|
725
1589
|
return null;
|
|
726
1590
|
}
|
|
727
|
-
var prefer_server_actions_default =
|
|
1591
|
+
var prefer_server_actions_default = ESLintUtils16.RuleCreator(
|
|
728
1592
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
729
1593
|
)({
|
|
730
1594
|
name: "prefer-server-actions",
|
|
@@ -763,7 +1627,10 @@ var prefer_server_actions_default = ESLintUtils7.RuleCreator(
|
|
|
763
1627
|
const methodName = node.callee.property.name.toLowerCase();
|
|
764
1628
|
if (AXIOS_MUTATION_METHODS.has(methodName)) {
|
|
765
1629
|
const urlArg = node.arguments[0];
|
|
766
|
-
|
|
1630
|
+
const hasHandlerArg = node.arguments.some(
|
|
1631
|
+
(arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression"
|
|
1632
|
+
);
|
|
1633
|
+
if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context)) {
|
|
767
1634
|
isMutation = true;
|
|
768
1635
|
}
|
|
769
1636
|
}
|
|
@@ -789,14 +1656,14 @@ var prefer_server_actions_default = ESLintUtils7.RuleCreator(
|
|
|
789
1656
|
});
|
|
790
1657
|
|
|
791
1658
|
// src/rules/prefer-shadcn.ts
|
|
792
|
-
import { ESLintUtils as
|
|
1659
|
+
import { ESLintUtils as ESLintUtils17 } from "@typescript-eslint/utils";
|
|
793
1660
|
var REPLACEMENTS = {
|
|
794
1661
|
input: "Input",
|
|
795
1662
|
select: "Select",
|
|
796
1663
|
textarea: "Textarea",
|
|
797
1664
|
dialog: "Dialog"
|
|
798
1665
|
};
|
|
799
|
-
var prefer_shadcn_default =
|
|
1666
|
+
var prefer_shadcn_default = ESLintUtils17.RuleCreator(
|
|
800
1667
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
801
1668
|
)({
|
|
802
1669
|
name: "prefer-shadcn",
|
|
@@ -837,36 +1704,52 @@ var prefer_shadcn_default = ESLintUtils8.RuleCreator(
|
|
|
837
1704
|
});
|
|
838
1705
|
|
|
839
1706
|
// src/rules/require-assert-never.ts
|
|
840
|
-
import { ESLintUtils as
|
|
1707
|
+
import { ESLintUtils as ESLintUtils18, AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
|
|
841
1708
|
var isAssertNeverCall = (expression) => {
|
|
842
|
-
if (expression.type !==
|
|
1709
|
+
if (expression.type !== AST_NODE_TYPES8.CallExpression) return false;
|
|
843
1710
|
const callee = expression.callee;
|
|
844
|
-
|
|
1711
|
+
if (callee.type === AST_NODE_TYPES8.Identifier) {
|
|
1712
|
+
return callee.name === "assertNever";
|
|
1713
|
+
}
|
|
1714
|
+
if (callee.type === AST_NODE_TYPES8.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES8.Identifier) {
|
|
1715
|
+
return callee.property.name === "assertNever";
|
|
1716
|
+
}
|
|
1717
|
+
return false;
|
|
845
1718
|
};
|
|
846
1719
|
var statementContainsAssertNever = (statement) => {
|
|
847
|
-
if (statement.type ===
|
|
1720
|
+
if (statement.type === AST_NODE_TYPES8.ExpressionStatement) {
|
|
848
1721
|
return isAssertNeverCall(statement.expression);
|
|
849
1722
|
}
|
|
850
|
-
if (statement.type ===
|
|
1723
|
+
if (statement.type === AST_NODE_TYPES8.ThrowStatement) {
|
|
851
1724
|
return isAssertNeverCall(statement.argument);
|
|
852
1725
|
}
|
|
853
|
-
if (statement.type ===
|
|
1726
|
+
if (statement.type === AST_NODE_TYPES8.ReturnStatement) {
|
|
1727
|
+
return statement.argument !== null && isAssertNeverCall(statement.argument);
|
|
1728
|
+
}
|
|
1729
|
+
if (statement.type === AST_NODE_TYPES8.BlockStatement) {
|
|
854
1730
|
return statement.body.some(statementContainsAssertNever);
|
|
855
1731
|
}
|
|
856
1732
|
return false;
|
|
857
1733
|
};
|
|
858
|
-
var
|
|
1734
|
+
var isRuntimeHandlingStatement = (statement) => {
|
|
1735
|
+
if (statement.type === AST_NODE_TYPES8.EmptyStatement) return false;
|
|
1736
|
+
if (statement.type === AST_NODE_TYPES8.BlockStatement) {
|
|
1737
|
+
return statement.body.some(isRuntimeHandlingStatement);
|
|
1738
|
+
}
|
|
1739
|
+
return true;
|
|
1740
|
+
};
|
|
1741
|
+
var require_assert_never_default = ESLintUtils18.RuleCreator(
|
|
859
1742
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
860
1743
|
)({
|
|
861
1744
|
name: "require-assert-never",
|
|
862
1745
|
meta: {
|
|
863
1746
|
type: "problem",
|
|
864
1747
|
docs: {
|
|
865
|
-
description: "Require switch
|
|
1748
|
+
description: "Require an exhaustive-style switch whose `default` case does no runtime work to call `assertNever(_)` so that discriminated unions are exhaustively checked at compile time. Switches with a legitimate runtime default (a reducer's `return state`, an HTTP-status `return fallback()`, a `break`, a `throw`, etc.) are left alone."
|
|
866
1749
|
},
|
|
867
1750
|
schema: [],
|
|
868
1751
|
messages: {
|
|
869
|
-
missingAssertNever: "
|
|
1752
|
+
missingAssertNever: "Empty switch `default` case \u2014 add runtime handling or call `assertNever()` so the discriminated union is exhaustively checked at compile time."
|
|
870
1753
|
}
|
|
871
1754
|
},
|
|
872
1755
|
defaultOptions: [],
|
|
@@ -877,10 +1760,8 @@ var require_assert_never_default = ESLintUtils9.RuleCreator(
|
|
|
877
1760
|
(caseNode) => caseNode.test === null
|
|
878
1761
|
);
|
|
879
1762
|
if (!defaultCase) return;
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
);
|
|
883
|
-
if (hasAssertNever) return;
|
|
1763
|
+
if (defaultCase.consequent.some(statementContainsAssertNever)) return;
|
|
1764
|
+
if (defaultCase.consequent.some(isRuntimeHandlingStatement)) return;
|
|
884
1765
|
context.report({
|
|
885
1766
|
node: defaultCase,
|
|
886
1767
|
messageId: "missingAssertNever"
|
|
@@ -891,43 +1772,91 @@ var require_assert_never_default = ESLintUtils9.RuleCreator(
|
|
|
891
1772
|
});
|
|
892
1773
|
|
|
893
1774
|
// src/rules/require-zod-form-validation.ts
|
|
894
|
-
import { ESLintUtils as
|
|
895
|
-
var
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1775
|
+
import { ESLintUtils as ESLintUtils19, AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
|
|
1776
|
+
var ZOD_SCHEMA_NAME_RE = /Schema$|^Z[A-Z]/;
|
|
1777
|
+
var looksLikeZodSchema = (node) => {
|
|
1778
|
+
let current = node;
|
|
1779
|
+
while (true) {
|
|
1780
|
+
if (current.type === AST_NODE_TYPES9.Identifier) {
|
|
1781
|
+
return current.name === "z" || ZOD_SCHEMA_NAME_RE.test(current.name);
|
|
1782
|
+
}
|
|
1783
|
+
if (current.type === AST_NODE_TYPES9.CallExpression) {
|
|
1784
|
+
current = current.callee;
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (current.type === AST_NODE_TYPES9.MemberExpression) {
|
|
1788
|
+
current = current.object;
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
899
1791
|
return false;
|
|
900
1792
|
}
|
|
901
|
-
return callee.object.type === AST_NODE_TYPES6.Identifier && callee.object.name === "formData";
|
|
902
1793
|
};
|
|
903
|
-
var
|
|
904
|
-
if (node.type !==
|
|
1794
|
+
var isZodParseCall = (node) => {
|
|
1795
|
+
if (node.type !== AST_NODE_TYPES9.CallExpression) return false;
|
|
905
1796
|
const callee = node.callee;
|
|
906
|
-
if (callee.type !==
|
|
907
|
-
|
|
1797
|
+
if (callee.type !== AST_NODE_TYPES9.MemberExpression) return false;
|
|
1798
|
+
if (callee.computed) return false;
|
|
1799
|
+
if (callee.property.type !== AST_NODE_TYPES9.Identifier) return false;
|
|
1800
|
+
const method = callee.property.name;
|
|
1801
|
+
if (method !== "parse" && method !== "safeParse") return false;
|
|
1802
|
+
return looksLikeZodSchema(callee.object);
|
|
908
1803
|
};
|
|
909
|
-
var
|
|
1804
|
+
var isFormDataMethodCall = (node) => {
|
|
1805
|
+
let current = node;
|
|
1806
|
+
if (current.type === AST_NODE_TYPES9.AwaitExpression) {
|
|
1807
|
+
current = current.argument;
|
|
1808
|
+
}
|
|
1809
|
+
if (current.type !== AST_NODE_TYPES9.CallExpression) return false;
|
|
1810
|
+
const callee = current.callee;
|
|
1811
|
+
return callee.type === AST_NODE_TYPES9.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES9.Identifier && callee.property.name === "formData";
|
|
1812
|
+
};
|
|
1813
|
+
var require_zod_form_validation_default = ESLintUtils19.RuleCreator(
|
|
910
1814
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
911
1815
|
)({
|
|
912
1816
|
name: "require-zod-form-validation",
|
|
913
1817
|
meta: {
|
|
914
1818
|
type: "problem",
|
|
915
1819
|
docs: {
|
|
916
|
-
description: "Require Zod validation (`Schema.parse(...)`) when reading values out of a `FormData` object."
|
|
1820
|
+
description: "Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object."
|
|
917
1821
|
},
|
|
918
1822
|
schema: [],
|
|
919
1823
|
messages: {
|
|
920
|
-
missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse())"
|
|
1824
|
+
missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse() / Schema.safeParse())"
|
|
921
1825
|
}
|
|
922
1826
|
},
|
|
923
1827
|
defaultOptions: [],
|
|
924
1828
|
create(context) {
|
|
1829
|
+
const isFormSourceIdentifier = (node) => {
|
|
1830
|
+
if (node.type !== AST_NODE_TYPES9.Identifier) return false;
|
|
1831
|
+
if (/formdata/i.test(node.name)) return true;
|
|
1832
|
+
let scope = context.sourceCode.getScope(node);
|
|
1833
|
+
while (scope !== null) {
|
|
1834
|
+
const variable = scope.set.get(node.name);
|
|
1835
|
+
if (variable !== void 0 && variable.defs.length === 1) {
|
|
1836
|
+
const def = variable.defs[0];
|
|
1837
|
+
if (def !== void 0 && def.type === "Variable" && def.node.type === AST_NODE_TYPES9.VariableDeclarator && def.node.init !== null) {
|
|
1838
|
+
return isFormDataMethodCall(def.node.init);
|
|
1839
|
+
}
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
scope = scope.upper;
|
|
1843
|
+
}
|
|
1844
|
+
return false;
|
|
1845
|
+
};
|
|
1846
|
+
const isFormDataGetCall = (node) => {
|
|
1847
|
+
const callee = node.callee;
|
|
1848
|
+
if (callee.type !== AST_NODE_TYPES9.MemberExpression) return false;
|
|
1849
|
+
if (callee.property.type !== AST_NODE_TYPES9.Identifier || callee.property.name !== "get") {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
return isFormSourceIdentifier(callee.object);
|
|
1853
|
+
};
|
|
925
1854
|
return {
|
|
926
1855
|
CallExpression(node) {
|
|
927
1856
|
if (!isFormDataGetCall(node)) return;
|
|
928
1857
|
let parent = node.parent;
|
|
929
1858
|
while (parent !== null && parent !== void 0) {
|
|
930
|
-
if (
|
|
1859
|
+
if (isZodParseCall(parent)) return;
|
|
931
1860
|
parent = parent.parent;
|
|
932
1861
|
}
|
|
933
1862
|
context.report({
|
|
@@ -940,15 +1869,15 @@ var require_zod_form_validation_default = ESLintUtils10.RuleCreator(
|
|
|
940
1869
|
});
|
|
941
1870
|
|
|
942
1871
|
// src/rules/zod-naming-convention.ts
|
|
943
|
-
import { ESLintUtils as
|
|
1872
|
+
import { ESLintUtils as ESLintUtils20, AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
|
|
944
1873
|
var calleeChainStartsWithZ = (node) => {
|
|
945
1874
|
let current = node;
|
|
946
|
-
while (current.type ===
|
|
1875
|
+
while (current.type === AST_NODE_TYPES10.MemberExpression) {
|
|
947
1876
|
const receiver = current.object;
|
|
948
|
-
if (receiver.type ===
|
|
1877
|
+
if (receiver.type === AST_NODE_TYPES10.Identifier && receiver.name === "z") {
|
|
949
1878
|
return true;
|
|
950
1879
|
}
|
|
951
|
-
if (receiver.type ===
|
|
1880
|
+
if (receiver.type === AST_NODE_TYPES10.CallExpression) {
|
|
952
1881
|
current = receiver.callee;
|
|
953
1882
|
continue;
|
|
954
1883
|
}
|
|
@@ -956,7 +1885,7 @@ var calleeChainStartsWithZ = (node) => {
|
|
|
956
1885
|
}
|
|
957
1886
|
return false;
|
|
958
1887
|
};
|
|
959
|
-
var zod_naming_convention_default =
|
|
1888
|
+
var zod_naming_convention_default = ESLintUtils20.RuleCreator(
|
|
960
1889
|
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
961
1890
|
)({
|
|
962
1891
|
name: "zod-naming-convention",
|
|
@@ -976,11 +1905,11 @@ var zod_naming_convention_default = ESLintUtils11.RuleCreator(
|
|
|
976
1905
|
VariableDeclarator(node) {
|
|
977
1906
|
const init = node.init;
|
|
978
1907
|
if (init === null || init === void 0) return;
|
|
979
|
-
if (init.type !==
|
|
1908
|
+
if (init.type !== AST_NODE_TYPES10.CallExpression) return;
|
|
980
1909
|
const callee = init.callee;
|
|
981
|
-
if (callee.type !==
|
|
1910
|
+
if (callee.type !== AST_NODE_TYPES10.MemberExpression) return;
|
|
982
1911
|
if (!calleeChainStartsWithZ(callee)) return;
|
|
983
|
-
if (node.id.type !==
|
|
1912
|
+
if (node.id.type !== AST_NODE_TYPES10.Identifier) return;
|
|
984
1913
|
const variableName = node.id.name;
|
|
985
1914
|
if (variableName.startsWith("Z")) return;
|
|
986
1915
|
context.report({
|
|
@@ -996,10 +1925,19 @@ var zod_naming_convention_default = ESLintUtils11.RuleCreator(
|
|
|
996
1925
|
var rules = {
|
|
997
1926
|
"enforce-file-structure": enforce_file_structure_default,
|
|
998
1927
|
"no-client-side-data-fetching": no_client_side_data_fetching_default,
|
|
1928
|
+
"no-comment-cruft": no_comment_cruft_default,
|
|
999
1929
|
"no-enum": no_enum_default,
|
|
1930
|
+
"no-insecure-random-id": no_insecure_random_id_default,
|
|
1931
|
+
"no-json-stringify-error": no_json_stringify_error_default,
|
|
1932
|
+
"no-log-only-catch": no_log_only_catch_default,
|
|
1000
1933
|
"no-raw-env": no_raw_env_default,
|
|
1934
|
+
"no-sentinel-return-on-catch": no_sentinel_return_on_catch_default,
|
|
1935
|
+
"no-sequential-await": no_sequential_await_default,
|
|
1936
|
+
"no-string-concat-in-loop": no_string_concat_in_loop_default,
|
|
1001
1937
|
"no-unnecessary-use-client": no_unnecessary_use_client_default,
|
|
1938
|
+
"prefer-discriminated-union": prefer_discriminated_union_default,
|
|
1002
1939
|
"prefer-schema-for-api-payload": prefer_schema_for_api_payload_default,
|
|
1940
|
+
"prefer-semantic-colors": prefer_semantic_colors_default,
|
|
1003
1941
|
"prefer-server-actions": prefer_server_actions_default,
|
|
1004
1942
|
"prefer-shadcn": prefer_shadcn_default,
|
|
1005
1943
|
"require-assert-never": require_assert_never_default,
|
|
@@ -1009,7 +1947,7 @@ var rules = {
|
|
|
1009
1947
|
var plugin = {
|
|
1010
1948
|
meta: {
|
|
1011
1949
|
name: "@sarj/eslint-plugin",
|
|
1012
|
-
version: "2.
|
|
1950
|
+
version: "2.2.0"
|
|
1013
1951
|
},
|
|
1014
1952
|
rules,
|
|
1015
1953
|
configs: {
|
|
@@ -1023,7 +1961,18 @@ var plugin = {
|
|
|
1023
1961
|
"@sarj/no-client-side-data-fetching": "warn",
|
|
1024
1962
|
"@sarj/prefer-server-actions": "warn",
|
|
1025
1963
|
"@sarj/no-unnecessary-use-client": "warn",
|
|
1026
|
-
"@sarj/prefer-schema-for-api-payload": "warn"
|
|
1964
|
+
"@sarj/prefer-schema-for-api-payload": "warn",
|
|
1965
|
+
// Distilled from sarj-audit skills — warn in recommended, error in strict.
|
|
1966
|
+
"@sarj/no-sequential-await": "warn",
|
|
1967
|
+
"@sarj/no-sentinel-return-on-catch": "warn",
|
|
1968
|
+
"@sarj/no-log-only-catch": "warn",
|
|
1969
|
+
"@sarj/no-insecure-random-id": "warn",
|
|
1970
|
+
"@sarj/no-json-stringify-error": "warn",
|
|
1971
|
+
"@sarj/no-string-concat-in-loop": "warn",
|
|
1972
|
+
"@sarj/prefer-discriminated-union": "warn",
|
|
1973
|
+
"@sarj/no-comment-cruft": "warn",
|
|
1974
|
+
// Frontend / styling — distilled from frontend PR-review mining.
|
|
1975
|
+
"@sarj/prefer-semantic-colors": "warn"
|
|
1027
1976
|
}
|
|
1028
1977
|
},
|
|
1029
1978
|
strict: {
|
|
@@ -1039,7 +1988,19 @@ var plugin = {
|
|
|
1039
1988
|
"@sarj/no-client-side-data-fetching": "error",
|
|
1040
1989
|
"@sarj/prefer-server-actions": "error",
|
|
1041
1990
|
"@sarj/no-unnecessary-use-client": "error",
|
|
1042
|
-
"@sarj/prefer-schema-for-api-payload": "error"
|
|
1991
|
+
"@sarj/prefer-schema-for-api-payload": "error",
|
|
1992
|
+
// Distilled from sarj-audit skills.
|
|
1993
|
+
"@sarj/no-sequential-await": "error",
|
|
1994
|
+
"@sarj/no-sentinel-return-on-catch": "error",
|
|
1995
|
+
"@sarj/no-log-only-catch": "error",
|
|
1996
|
+
"@sarj/no-insecure-random-id": "error",
|
|
1997
|
+
"@sarj/no-json-stringify-error": "error",
|
|
1998
|
+
"@sarj/no-string-concat-in-loop": "error",
|
|
1999
|
+
"@sarj/prefer-discriminated-union": "error",
|
|
2000
|
+
"@sarj/no-comment-cruft": "error",
|
|
2001
|
+
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
2002
|
+
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
2003
|
+
"@sarj/prefer-semantic-colors": "warn"
|
|
1043
2004
|
}
|
|
1044
2005
|
}
|
|
1045
2006
|
}
|