@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.
- package/lib/index.js +1 -1
- package/lib/rules/enforce-assert-safe-object-key.js +17 -0
- package/lib/rules/enforce-memoize-async.js +19 -2
- package/lib/rules/enforce-memoize-getters.js +17 -0
- package/lib/rules/enforce-querykey-ts.js +25 -3
- package/lib/rules/enforce-stable-hash-spread-props.js +16 -0
- package/lib/rules/fast-deep-equal-over-microdiff.js +14 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +226 -48
- package/lib/rules/no-array-length-in-deps.js +23 -1
- package/lib/rules/no-entire-object-hook-deps.js +89 -89
- package/lib/rules/prefer-fragment-component.js +52 -1
- package/lib/rules/prefer-map-over-conditional-dispatch.js +120 -1
- package/lib/rules/prefer-type-over-interface.js +38 -11
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +23 -2
- package/lib/rules/require-memoize-jsx-returners.js +17 -0
- package/lib/utils/disableDirectives.d.ts +51 -0
- package/lib/utils/disableDirectives.js +168 -0
- package/package.json +1 -1
- package/release-manifest.json +125 -0
|
@@ -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
package/release-manifest.json
CHANGED
|
@@ -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",
|