@blumintinc/eslint-plugin-blumint 1.18.15 → 1.19.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/README.md +4 -0
- package/lib/index.js +13 -1
- package/lib/rules/enforce-single-exported-unit-per-file.d.ts +3 -0
- package/lib/rules/enforce-single-exported-unit-per-file.js +571 -0
- package/lib/rules/memo-nested-react-components.js +30 -2
- package/lib/rules/no-harness-coupled-disables.d.ts +1 -0
- package/lib/rules/no-harness-coupled-disables.js +134 -0
- package/lib/rules/no-hungarian.js +34 -6
- package/lib/rules/prefer-clone-deep.js +8 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.d.ts +4 -0
- package/lib/rules/prefer-map-over-conditional-dispatch.js +907 -0
- package/lib/rules/prefer-union-from-const-array.d.ts +3 -0
- package/lib/rules/prefer-union-from-const-array.js +137 -0
- package/package.json +1 -1
- package/release-manifest.json +68 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noHarnessCoupledDisables = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* Recognizes ESLint disable directives (block or line) by their leading
|
|
7
|
+
* keyword. `eslint-enable` deliberately does not match: it carries no
|
|
8
|
+
* justification to classify.
|
|
9
|
+
*/
|
|
10
|
+
const DIRECTIVE_PREFIX = /^eslint-disable(?:-next-line|-line)?\b/;
|
|
11
|
+
/**
|
|
12
|
+
* Unconditional harness terms — always flag. Each describes the Claude Code
|
|
13
|
+
* CLI development harness and has no code-level meaning in a suppression
|
|
14
|
+
* reason. When several co-occur, the one appearing FIRST in the justification
|
|
15
|
+
* text is reported (see `findHarnessTerm`), so "claude session ... cwd" reports
|
|
16
|
+
* `claude`, not `cwd`.
|
|
17
|
+
*/
|
|
18
|
+
const UNCONDITIONAL_MATCHERS = [
|
|
19
|
+
// Word-boundary start, suffix/compound tolerant: worktree, worktrees,
|
|
20
|
+
// worktree-cwd, cross-worktree, worktree-hook.
|
|
21
|
+
{ term: 'worktree', pattern: /\bworktree/ },
|
|
22
|
+
// Word-boundary token, plural/possessive tolerant: cwd, cwds, cwd's.
|
|
23
|
+
{ term: 'cwd', pattern: /\bcwd(?:'?s)?\b/ },
|
|
24
|
+
// Word-boundary start: claude, claude's, claude-code, claude code.
|
|
25
|
+
{ term: 'claude', pattern: /\bclaude/ },
|
|
26
|
+
// The Claude Code Stop hook compound: stop-hook / stop hook / stop_hook,
|
|
27
|
+
// plus near-adjacent forms with at most one intervening word ("stop lint
|
|
28
|
+
// hook"). Distant incidental co-occurrence of "stop" and "hook" does not
|
|
29
|
+
// match, keeping bare React-hook justifications safe.
|
|
30
|
+
{ term: 'stop-hook', pattern: /\bstop[-_ ]+(?:\w+[-_ ]+)?hook/ },
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Conditional session compound. `session` (auth session) is legitimate
|
|
34
|
+
* code-level vocabulary, so it only flags in the unambiguous harness compounds
|
|
35
|
+
* `agent session` / `claude session`. When it co-occurs with an unconditional
|
|
36
|
+
* term, that term reports instead — so in practice only `agent session`
|
|
37
|
+
* surfaces `session` as the matched term.
|
|
38
|
+
*/
|
|
39
|
+
const SESSION_MATCHER = {
|
|
40
|
+
term: 'session',
|
|
41
|
+
pattern: /\b(?:agent|claude)[-_ ]+sessions?\b/,
|
|
42
|
+
};
|
|
43
|
+
function isDirectiveComment(comment) {
|
|
44
|
+
return DIRECTIVE_PREFIX.test(comment.value.trim());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Isolates the justification text of a directive comment: everything after the
|
|
48
|
+
* first `--` separator, spanning every line of a multi-line block body. The
|
|
49
|
+
* directive keyword and the comma-separated rule-name list both precede the
|
|
50
|
+
* `--` (and never contain `--`), so slicing past it strips them in one step.
|
|
51
|
+
* Returns `null` when there is no `--` — a bare disable with no justification
|
|
52
|
+
* is out of scope.
|
|
53
|
+
*/
|
|
54
|
+
function extractJustification(comment) {
|
|
55
|
+
const separatorIndex = comment.value.indexOf('--');
|
|
56
|
+
if (separatorIndex === -1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return comment.value.slice(separatorIndex + 2);
|
|
60
|
+
}
|
|
61
|
+
function findHarnessTerm(text) {
|
|
62
|
+
const lowered = text.toLowerCase();
|
|
63
|
+
let earliest = null;
|
|
64
|
+
for (const { term, pattern } of UNCONDITIONAL_MATCHERS) {
|
|
65
|
+
const match = pattern.exec(lowered);
|
|
66
|
+
if (match && (earliest === null || match.index < earliest.index)) {
|
|
67
|
+
earliest = { term, index: match.index };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (earliest !== null) {
|
|
71
|
+
return earliest.term;
|
|
72
|
+
}
|
|
73
|
+
// Only reached when no unconditional term is present: an isolated harness
|
|
74
|
+
// session compound.
|
|
75
|
+
if (SESSION_MATCHER.pattern.test(lowered)) {
|
|
76
|
+
return SESSION_MATCHER.term;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
exports.noHarnessCoupledDisables = (0, createRule_1.createRule)({
|
|
81
|
+
name: 'no-harness-coupled-disables',
|
|
82
|
+
meta: {
|
|
83
|
+
type: 'problem',
|
|
84
|
+
docs: {
|
|
85
|
+
description: 'Disallow eslint-disable justifications that reference the agent development harness (worktree, cwd, stop-hook, claude) instead of the code’s own semantics.',
|
|
86
|
+
recommended: 'error',
|
|
87
|
+
},
|
|
88
|
+
fixable: undefined,
|
|
89
|
+
schema: [],
|
|
90
|
+
messages: {
|
|
91
|
+
harnessCoupled: "This eslint-disable justification references the development harness ('{{matchedTerm}}'), not the code's own semantics. Harness quirks (how the stop hook invokes lint, cwd resolution, worktree path mapping) belong in harness config — fix the hook, not the source. See the harness docs referenced in the disable-directive hygiene section of the repo's linting skill doc. If this disable is NOT harness-related and the word is a false match (e.g. 'hook' meaning a React hook), rephrase the justification to describe the code-level reason instead.",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
defaultOptions: [],
|
|
95
|
+
create(context) {
|
|
96
|
+
const sourceCode = context.getSourceCode();
|
|
97
|
+
const comments = sourceCode.getAllComments();
|
|
98
|
+
return {
|
|
99
|
+
Program() {
|
|
100
|
+
comments.forEach((comment, index) => {
|
|
101
|
+
if (!isDirectiveComment(comment)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const justification = extractJustification(comment);
|
|
105
|
+
// No `--` separator, or nothing but whitespace after it: a bare
|
|
106
|
+
// disable carries no justification to classify (out of scope).
|
|
107
|
+
if (justification === null || justification.trim() === '') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Split-justification style: an immediately-adjacent preceding
|
|
111
|
+
// non-directive comment (no intervening blank line or code) is part
|
|
112
|
+
// of the directive's rationale when the directive defers to it.
|
|
113
|
+
let scanned = justification;
|
|
114
|
+
const previous = comments[index - 1];
|
|
115
|
+
if (previous &&
|
|
116
|
+
!isDirectiveComment(previous) &&
|
|
117
|
+
comment.loc.start.line - previous.loc.end.line <= 1) {
|
|
118
|
+
scanned = `${previous.value}\n${scanned}`;
|
|
119
|
+
}
|
|
120
|
+
const matchedTerm = findHarnessTerm(scanned);
|
|
121
|
+
if (matchedTerm === null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
context.report({
|
|
125
|
+
loc: comment.loc,
|
|
126
|
+
messageId: 'harnessCoupled',
|
|
127
|
+
data: { matchedTerm },
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=no-harness-coupled-disables.js.map
|
|
@@ -298,6 +298,15 @@ function isDomainNumberCompound(name) {
|
|
|
298
298
|
const lastSegment = segments[segments.length - 1];
|
|
299
299
|
return (!!lastSegment && DOMAIN_NUMBER_HEAD_NOUNS.has(lastSegment.toLowerCase()));
|
|
300
300
|
}
|
|
301
|
+
// Rebuild a SCREAMING_SNAKE_CASE identifier's segments into a PascalCase compound
|
|
302
|
+
// (["MATCH","NUMBER"] -> "MatchNumber") so the snake-case branch can reuse the
|
|
303
|
+
// camelCase isDomainNumberCompound / DOMAIN_NUMBER_HEAD_NOUNS exemption verbatim,
|
|
304
|
+
// keeping MATCH_NUMBER and matchNumber on a single code path (#1294).
|
|
305
|
+
function screamingSnakePartsToPascalCase(parts) {
|
|
306
|
+
return parts
|
|
307
|
+
.map((part) => part.charAt(0) + part.slice(1).toLowerCase())
|
|
308
|
+
.join('');
|
|
309
|
+
}
|
|
301
310
|
exports.noHungarian = (0, createRule_1.createRule)({
|
|
302
311
|
name: 'no-hungarian',
|
|
303
312
|
meta: {
|
|
@@ -387,12 +396,31 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
387
396
|
if (isAbbreviation) {
|
|
388
397
|
return true;
|
|
389
398
|
}
|
|
390
|
-
// A FULL type word tags the entity's type only
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
399
|
+
// A FULL type word tags the entity's runtime type only as a genuine
|
|
400
|
+
// leading prefix (index 0) or trailing head-noun (last segment).
|
|
401
|
+
// Mirror the camelCase/PascalCase branch, which never flags a
|
|
402
|
+
// full-type-word in a MIDDLE segment: an interior NUMBER/STRING is a
|
|
403
|
+
// domain modifier describing a variant (CADENCE_NUMBER_EDITORS —
|
|
404
|
+
// "editors of a numeric cadence"), not a redundant type tag. The
|
|
405
|
+
// previous `index === lastIndex - 1` allowance produced a casing
|
|
406
|
+
// asymmetry — CadenceNumberEditor was exempt (#1250) but
|
|
407
|
+
// CADENCE_NUMBER_EDITORS fired (#1294).
|
|
408
|
+
if (index !== 0 && index !== lastIndex) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
// A trailing "..._NUMBER" whose preceding head noun is a domain
|
|
412
|
+
// entity (MATCH_NUMBER, ISSUE_NUMBER, CURRENT_LINE_NUMBER) is a
|
|
413
|
+
// domain compound, not a Hungarian type tag — route through the same
|
|
414
|
+
// isDomainNumberCompound carve-out used for camelCase matchNumber
|
|
415
|
+
// (#1277), so numeric-quantity heads (COUNT_NUMBER, MAX_RETRY_NUMBER)
|
|
416
|
+
// still fire because those heads are absent from
|
|
417
|
+
// DOMAIN_NUMBER_HEAD_NOUNS.
|
|
418
|
+
if (normalizedMarker === 'number' &&
|
|
419
|
+
index === lastIndex &&
|
|
420
|
+
isDomainNumberCompound(screamingSnakePartsToPascalCase(parts))) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
396
424
|
});
|
|
397
425
|
});
|
|
398
426
|
}
|
|
@@ -52,15 +52,20 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
52
52
|
node.key.callee.name === 'Symbol')) {
|
|
53
53
|
hasSymbol = true;
|
|
54
54
|
}
|
|
55
|
-
// Visit child nodes without traversing parent references
|
|
55
|
+
// Visit child nodes without traversing parent references. Depth tracks
|
|
56
|
+
// object-nesting level: an object's OWN direct properties stay at the
|
|
57
|
+
// object's depth, and only descending into a property's value (a
|
|
58
|
+
// genuinely nested child) increments it. This keeps the top-level
|
|
59
|
+
// object's own `...spread` at depth 0 so it is never miscounted as a
|
|
60
|
+
// nested spread.
|
|
56
61
|
if (node.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
57
62
|
if (depth > 0) {
|
|
58
63
|
hasNestedObject = true;
|
|
59
64
|
}
|
|
60
|
-
node.properties.forEach((prop) => visit(prop, depth
|
|
65
|
+
node.properties.forEach((prop) => visit(prop, depth));
|
|
61
66
|
}
|
|
62
67
|
else if (node.type === utils_1.AST_NODE_TYPES.Property) {
|
|
63
|
-
visit(node.value, depth);
|
|
68
|
+
visit(node.value, depth + 1);
|
|
64
69
|
}
|
|
65
70
|
else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
66
71
|
visit(node.argument, depth);
|