@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.
@@ -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 at the start (prefix),
391
- // the end (suffix), or directly before the final noun
392
- // (..._STRING_NAME). Buried deeper (EDITABLE_WRAPPER_NUMBER_PROPS_…)
393
- // it qualifies an intermediate segment a descriptive variant, not a
394
- // type tag.
395
- return (index === 0 || index === lastIndex || index === lastIndex - 1);
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 + 1));
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);
@@ -0,0 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferMap' | 'preferMapManual';
3
+ export declare const preferMapOverConditionalDispatch: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
+ export {};