@blumintinc/eslint-plugin-blumint 1.20.24 → 1.20.26

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requireMemoizeJsxReturners = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const disableDirectives_1 = require("../utils/disableDirectives");
6
7
  const MEMOIZE_PREFERRED_MODULE = '@blumintinc/typescript-memoize';
7
8
  const MEMOIZE_MODULES = new Set([
8
9
  MEMOIZE_PREFERRED_MODULE,
@@ -384,6 +385,13 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
384
385
  let memoizeAlias = 'Memoize';
385
386
  let memoizeNamespace = null;
386
387
  let scheduledImportFix = false;
388
+ /**
389
+ * The `import { Memoize }` statement rides on a single violation's fix, so
390
+ * that violation is the file's import carrier. A suppressed carrier would
391
+ * take the import down with it while the surviving violations still emit
392
+ * `@Memoize()`, leaving a decorator with no import.
393
+ */
394
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
387
395
  const jsxReturnCache = new WeakMap();
388
396
  const reactMemoIdentifiers = new Set();
389
397
  const reactNamespaceIdentifiers = new Set();
@@ -454,11 +462,20 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
454
462
  const decoratorIdent = memoizeNamespace
455
463
  ? `${memoizeNamespace}.Memoize`
456
464
  : memoizeAlias;
465
+ // The report is emitted even when suppressed: ESLint discards it, and
466
+ // reporting keeps the user's disable directive "used" so that
467
+ // `--report-unused-disable-directives` does not flag it.
457
468
  context.report({
458
469
  node,
459
470
  messageId: 'requireMemoizeJsxReturner',
460
471
  data: { name: getMemberName(node) },
461
472
  fix(fixer) {
473
+ // A suppressed report is dropped together with its fix. Producing
474
+ // no fix — and leaving `scheduledImportFix` untouched — passes the
475
+ // import to the first violation that survives.
476
+ if (isReportSuppressed(node)) {
477
+ return null;
478
+ }
462
479
  const sourceCode = context.getSourceCode();
463
480
  const { fixes, scheduledImportFix: newScheduledImportFix } = getImportFixes(fixer, sourceCode, hasMemoizeImport, scheduledImportFix);
464
481
  scheduledImportFix = newScheduledImportFix;
@@ -0,0 +1,51 @@
1
+ import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ type DirectiveKind = 'disable' | 'enable' | 'disable-line' | 'disable-next-line';
3
+ export type Directive = {
4
+ kind: DirectiveKind;
5
+ /** `null` when the directive names no rule, which targets every rule. */
6
+ rules: Set<string> | null;
7
+ /**
8
+ * For block directives, the comment's own position; for line-scoped ones,
9
+ * the single source line the directive covers.
10
+ */
11
+ line: number;
12
+ column: number;
13
+ };
14
+ /**
15
+ * Parses every disable/enable directive in a file, ordered by position so the
16
+ * block-scoped pass can walk them as a timeline.
17
+ */
18
+ export declare function parseDisableDirectives(comments: readonly TSESTree.Comment[]): Directive[];
19
+ /**
20
+ * Whether `ruleId` is disabled at `loc` by the already-parsed `directives`.
21
+ */
22
+ export declare function isSuppressed(directives: readonly Directive[], ruleId: string, loc: TSESTree.Position): boolean;
23
+ /**
24
+ * Anything a rule already has in hand when it reports: a raw position, or any
25
+ * node/token/comment carrying a `loc`. Rules reporting on expressions or JSX
26
+ * elements pass the node as readily as ones reporting on method definitions.
27
+ */
28
+ export type SuppressionTarget = TSESTree.Position | {
29
+ readonly loc: TSESTree.SourceLocation;
30
+ };
31
+ export type SuppressionChecker = (target: SuppressionTarget) => boolean;
32
+ /**
33
+ * Builds a suppression predicate over a fixed set of comments. Exposed for
34
+ * direct testing and for callers that hold comments rather than a rule context;
35
+ * rules should prefer {@link createSuppressionChecker}.
36
+ */
37
+ export declare function createSuppressionCheckerFor(comments: readonly TSESTree.Comment[], ruleId: string): SuppressionChecker;
38
+ /**
39
+ * Builds a per-file suppression predicate for the rule that owns `context`.
40
+ * Intended for a rule's `fix()` callback, which must decline to fix a report
41
+ * ESLint is about to discard.
42
+ *
43
+ * The file's comments are parsed once, on first use, and reused for every
44
+ * subsequent call — a rule asks this per violation.
45
+ *
46
+ * `context.id` is exactly the name a disable directive must spell, whether the
47
+ * rule runs bare (as under `RuleTester`) or under its plugin prefix, so the
48
+ * match never hardcodes a prefix.
49
+ */
50
+ export declare function createSuppressionChecker<TMessageIds extends string, TOptions extends readonly unknown[]>(context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>): SuppressionChecker;
51
+ export {};
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSuppressionChecker = exports.createSuppressionCheckerFor = exports.isSuppressed = exports.parseDisableDirectives = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ /**
6
+ * Resolves whether a report location is suppressed by an inline ESLint disable
7
+ * directive, from inside the rule itself.
8
+ *
9
+ * A rule that adds a shared edit — typically an `import` statement — by
10
+ * attaching it to a single violation's fix makes that violation the file's
11
+ * import carrier. ESLint calls `fix()` eagerly, while inline disable directives
12
+ * are applied afterwards: a suppressed report takes its fix down with it. If the
13
+ * carrier is suppressed, the surviving violations still emit their rewritten
14
+ * code and the file ends up referencing an identifier that was never imported.
15
+ * Resolving suppression up front lets the carrier slot fall to the first
16
+ * violation that actually survives.
17
+ *
18
+ * The parsing below mirrors ESLint's own directive handling
19
+ * (`lib/linter/apply-disable-directives.js`).
20
+ */
21
+ const DIRECTIVE_PATTERN = /^(eslint-disable(?:-next-line|-line)?|eslint-enable)(?:\s|$)/;
22
+ /** ESLint separates a directive from its justification with ` -- `. */
23
+ const JUSTIFICATION_SEPARATOR = /\s-{2,}\s/;
24
+ function parseDirective(comment) {
25
+ const justification = JUSTIFICATION_SEPARATOR.exec(comment.value);
26
+ const directivePart = (justification ? comment.value.slice(0, justification.index) : comment.value).trim();
27
+ const match = DIRECTIVE_PATTERN.exec(directivePart);
28
+ if (!match) {
29
+ return null;
30
+ }
31
+ const token = match[1];
32
+ const kind = token.slice('eslint-'.length);
33
+ const isLineScoped = kind === 'disable-line' || kind === 'disable-next-line';
34
+ // `eslint-disable`/`eslint-enable` are honoured only in block comments, and
35
+ // a multi-line `eslint-disable-line` is rejected outright — both per ESLint.
36
+ if (!isLineScoped && comment.type !== utils_1.AST_TOKEN_TYPES.Block) {
37
+ return null;
38
+ }
39
+ if (kind === 'disable-line' &&
40
+ comment.loc.start.line !== comment.loc.end.line) {
41
+ return null;
42
+ }
43
+ const ruleList = directivePart.slice(token.length).trim();
44
+ const rules = ruleList === ''
45
+ ? null
46
+ : new Set(ruleList
47
+ .split(',')
48
+ .map((rule) => rule.trim())
49
+ .filter((rule) => rule !== ''));
50
+ // A multi-line `eslint-disable-next-line` targets the line after the comment
51
+ // ends, not after it starts.
52
+ const line = kind === 'disable-next-line'
53
+ ? comment.loc.end.line + 1
54
+ : comment.loc.start.line;
55
+ return { kind, rules, line, column: comment.loc.start.column };
56
+ }
57
+ /**
58
+ * Parses every disable/enable directive in a file, ordered by position so the
59
+ * block-scoped pass can walk them as a timeline.
60
+ */
61
+ function parseDisableDirectives(comments) {
62
+ return comments
63
+ .map(parseDirective)
64
+ .filter((directive) => directive !== null)
65
+ .sort((a, b) => a.line - b.line || a.column - b.column);
66
+ }
67
+ exports.parseDisableDirectives = parseDisableDirectives;
68
+ function targetsRule(directive, ruleId) {
69
+ return directive.rules === null || directive.rules.has(ruleId);
70
+ }
71
+ /**
72
+ * ESLint evaluates line-scoped directives in a pass of their own, so the
73
+ * implicit re-enable that follows an `eslint-disable-next-line` cannot punch a
74
+ * hole in a surrounding block disable. This mirrors that separation.
75
+ */
76
+ function isBlockSuppressed(directives, ruleId, loc) {
77
+ let globalDisabled = false;
78
+ let ruleDisabled = false;
79
+ // An `eslint-enable <rule>` carves a single rule out of a bare disable.
80
+ let ruleReEnabled = false;
81
+ for (const directive of directives) {
82
+ if (directive.kind !== 'disable' && directive.kind !== 'enable') {
83
+ continue;
84
+ }
85
+ const isBefore = directive.line < loc.line ||
86
+ (directive.line === loc.line && directive.column <= loc.column);
87
+ if (!isBefore) {
88
+ break;
89
+ }
90
+ const isBare = directive.rules === null;
91
+ if (!isBare && !targetsRule(directive, ruleId)) {
92
+ continue;
93
+ }
94
+ if (directive.kind === 'disable') {
95
+ if (isBare) {
96
+ globalDisabled = true;
97
+ ruleDisabled = false;
98
+ ruleReEnabled = false;
99
+ }
100
+ else if (globalDisabled) {
101
+ ruleReEnabled = false;
102
+ }
103
+ else {
104
+ ruleDisabled = true;
105
+ }
106
+ continue;
107
+ }
108
+ if (isBare) {
109
+ globalDisabled = false;
110
+ ruleDisabled = false;
111
+ ruleReEnabled = false;
112
+ }
113
+ else if (globalDisabled) {
114
+ ruleReEnabled = true;
115
+ }
116
+ else {
117
+ ruleDisabled = false;
118
+ }
119
+ }
120
+ return ruleDisabled || (globalDisabled && !ruleReEnabled);
121
+ }
122
+ /**
123
+ * Whether `ruleId` is disabled at `loc` by the already-parsed `directives`.
124
+ */
125
+ function isSuppressed(directives, ruleId, loc) {
126
+ const lineSuppressed = directives.some((directive) => (directive.kind === 'disable-line' ||
127
+ directive.kind === 'disable-next-line') &&
128
+ directive.line === loc.line &&
129
+ targetsRule(directive, ruleId));
130
+ return lineSuppressed || isBlockSuppressed(directives, ruleId, loc);
131
+ }
132
+ exports.isSuppressed = isSuppressed;
133
+ function positionOf(target) {
134
+ return 'loc' in target ? target.loc.start : target;
135
+ }
136
+ /**
137
+ * Builds a suppression predicate over a fixed set of comments. Exposed for
138
+ * direct testing and for callers that hold comments rather than a rule context;
139
+ * rules should prefer {@link createSuppressionChecker}.
140
+ */
141
+ function createSuppressionCheckerFor(comments, ruleId) {
142
+ const directives = parseDisableDirectives(comments);
143
+ return (target) => isSuppressed(directives, ruleId, positionOf(target));
144
+ }
145
+ exports.createSuppressionCheckerFor = createSuppressionCheckerFor;
146
+ /**
147
+ * Builds a per-file suppression predicate for the rule that owns `context`.
148
+ * Intended for a rule's `fix()` callback, which must decline to fix a report
149
+ * ESLint is about to discard.
150
+ *
151
+ * The file's comments are parsed once, on first use, and reused for every
152
+ * subsequent call — a rule asks this per violation.
153
+ *
154
+ * `context.id` is exactly the name a disable directive must spell, whether the
155
+ * rule runs bare (as under `RuleTester`) or under its plugin prefix, so the
156
+ * match never hardcodes a prefix.
157
+ */
158
+ function createSuppressionChecker(context) {
159
+ let checker = null;
160
+ return (target) => {
161
+ if (checker === null) {
162
+ checker = createSuppressionCheckerFor(context.sourceCode.getAllComments(), context.id);
163
+ }
164
+ return checker(target);
165
+ };
166
+ }
167
+ exports.createSuppressionChecker = createSuppressionChecker;
168
+ //# sourceMappingURL=disableDirectives.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.24",
3
+ "version": "1.20.26",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,129 @@
1
1
  [
2
+ {
3
+ "version": "1.20.26",
4
+ "date": "2026-07-30T06:12:23.979Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-assert-safe-object-key",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1408
11
+ ],
12
+ "summary": "keep the assertSafe import when the carrier violation is disabled inline (closes #1408)"
13
+ },
14
+ {
15
+ "name": "enforce-memoize-async",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1404
19
+ ],
20
+ "summary": "keep the Memoize import when the carrier violation is disabled (closes #1404)"
21
+ },
22
+ {
23
+ "name": "enforce-memoize-getters",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1409
27
+ ],
28
+ "summary": "keep the Memoize import when the carrier violation is disabled inline (closes #1409)"
29
+ },
30
+ {
31
+ "name": "enforce-querykey-ts",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1410
35
+ ],
36
+ "summary": "exclude suppressed reports from the import plan and carrier choice (closes #1410)"
37
+ },
38
+ {
39
+ "name": "enforce-stable-hash-spread-props",
40
+ "changeType": "fix",
41
+ "issues": [
42
+ 1413
43
+ ],
44
+ "summary": "keep the stableHash import when the carrier violation is disabled inline (closes #1413)"
45
+ },
46
+ {
47
+ "name": "fast-deep-equal-over-microdiff",
48
+ "changeType": "fix",
49
+ "issues": [
50
+ 1415
51
+ ],
52
+ "summary": "keep the isEqual import when the carrier violation is disabled inline (closes #1415)"
53
+ },
54
+ {
55
+ "name": "logical-top-to-bottom-grouping",
56
+ "changeType": "fix",
57
+ "issues": [
58
+ 1405
59
+ ],
60
+ "summary": "emit a fully-satisfying reordering as one fix (closes #1405)"
61
+ },
62
+ {
63
+ "name": "no-array-length-in-deps",
64
+ "changeType": "fix",
65
+ "issues": [
66
+ 1412
67
+ ],
68
+ "summary": "keep both planned imports when the carrier violation is disabled inline (closes #1412)"
69
+ },
70
+ {
71
+ "name": "prefer-fragment-component",
72
+ "changeType": "fix",
73
+ "issues": [
74
+ 1407
75
+ ],
76
+ "summary": "keep the Fragment import when the carrier violation is disabled inline (closes #1407)"
77
+ },
78
+ {
79
+ "name": "prefer-type-over-interface",
80
+ "changeType": "fix",
81
+ "issues": [
82
+ 1403,
83
+ 1406
84
+ ],
85
+ "summary": "join heritage clauses with `&` in the autofix (closes #1406); insert `=` after the type parameters (closes #1403)"
86
+ },
87
+ {
88
+ "name": "prefer-usecallback-over-usememo-for-functions",
89
+ "changeType": "fix",
90
+ "issues": [
91
+ 1411
92
+ ],
93
+ "summary": "exclude suppressed violations from the conversion plan (closes #1411)"
94
+ },
95
+ {
96
+ "name": "require-memoize-jsx-returners",
97
+ "changeType": "fix",
98
+ "issues": [
99
+ 1414
100
+ ],
101
+ "summary": "keep the Memoize import when the carrier violation is disabled inline (closes #1414)"
102
+ }
103
+ ]
104
+ },
105
+ {
106
+ "version": "1.20.25",
107
+ "date": "2026-07-30T02:59:17.229Z",
108
+ "rules": [
109
+ {
110
+ "name": "no-entire-object-hook-deps",
111
+ "changeType": "fix",
112
+ "issues": [
113
+ 1401
114
+ ],
115
+ "summary": "render optional links per segment (closes #1401)"
116
+ },
117
+ {
118
+ "name": "prefer-map-over-conditional-dispatch",
119
+ "changeType": "fix",
120
+ "issues": [
121
+ 1402
122
+ ],
123
+ "summary": "validate the synthesized Record annotation (closes #1402)"
124
+ }
125
+ ]
126
+ },
2
127
  {
3
128
  "version": "1.20.24",
4
129
  "date": "2026-07-30T02:00:54.523Z",