@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
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
10
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
11
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
11
12
|
const DEFAULT_IMPORT_PATH = 'functions/src/util/assertSafe';
|
|
12
13
|
exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
13
14
|
name: 'enforce-assert-safe-object-key',
|
|
@@ -35,6 +36,13 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
35
36
|
create(context, [options]) {
|
|
36
37
|
const importPath = options?.assertSafeImportPath || DEFAULT_IMPORT_PATH;
|
|
37
38
|
let hasAssertSafeImport = false;
|
|
39
|
+
/**
|
|
40
|
+
* The `import { assertSafe }` statement rides on a single violation's fix,
|
|
41
|
+
* making that violation the file's import carrier. A suppressed carrier
|
|
42
|
+
* would take the import down with it while the surviving violations still
|
|
43
|
+
* emit `assertSafe(...)`, leaving the call unbound.
|
|
44
|
+
*/
|
|
45
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
38
46
|
/**
|
|
39
47
|
* Computes the module specifier for the injected assertSafe import.
|
|
40
48
|
*
|
|
@@ -97,11 +105,20 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
97
105
|
fixes.push(fixer.replaceText(node, `assertSafe(${argText})`));
|
|
98
106
|
return fixes;
|
|
99
107
|
};
|
|
108
|
+
// The report is emitted even when suppressed: ESLint discards it, and
|
|
109
|
+
// reporting keeps the user's disable directive "used" so that
|
|
110
|
+
// `--report-unused-disable-directives` does not flag it.
|
|
100
111
|
const reportUseAssertSafe = (node, expressionText) => context.report({
|
|
101
112
|
node,
|
|
102
113
|
messageId: 'useAssertSafe',
|
|
103
114
|
data: { key: expressionText },
|
|
104
115
|
fix(fixer) {
|
|
116
|
+
// A suppressed report is dropped together with its fix. Producing no
|
|
117
|
+
// fix — and leaving the import unclaimed — passes the carrier slot to
|
|
118
|
+
// the first violation that survives.
|
|
119
|
+
if (isReportSuppressed(node)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
105
122
|
return createFixes(fixer, node, expressionText);
|
|
106
123
|
},
|
|
107
124
|
});
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMemoizeAsync = 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_MODULE = '@blumintinc/typescript-memoize';
|
|
7
8
|
const ALLOWED_MEMOIZE_MODULES = new Set([MEMOIZE_MODULE, 'typescript-memoize']);
|
|
8
9
|
/**
|
|
@@ -54,6 +55,13 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
54
55
|
const memoizeAliases = new Map(); // alias -> source module
|
|
55
56
|
const memoizeNamespaces = new Map(); // namespace -> source module
|
|
56
57
|
let scheduledImportFix = false;
|
|
58
|
+
/**
|
|
59
|
+
* The `import { Memoize }` statement rides on a single violation's fix, so
|
|
60
|
+
* that violation is the file's import carrier. A suppressed carrier would
|
|
61
|
+
* take the import down with it while the surviving violations still emit
|
|
62
|
+
* `@Memoize()`, leaving a decorator with no import.
|
|
63
|
+
*/
|
|
64
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
57
65
|
return {
|
|
58
66
|
ImportDeclaration(node) {
|
|
59
67
|
if (ALLOWED_MEMOIZE_MODULES.has(String(node.source.value))) {
|
|
@@ -119,10 +127,19 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
119
127
|
if (hasDecorator) {
|
|
120
128
|
return;
|
|
121
129
|
}
|
|
130
|
+
// The report is emitted even when suppressed: ESLint discards it, and
|
|
131
|
+
// reporting keeps the user's disable directive "used" so that
|
|
132
|
+
// `--report-unused-disable-directives` does not flag it.
|
|
122
133
|
context.report({
|
|
123
134
|
node,
|
|
124
135
|
messageId: 'requireMemoize',
|
|
125
136
|
fix(fixer) {
|
|
137
|
+
// A suppressed report is dropped together with its fix. Producing
|
|
138
|
+
// no fix — and leaving the import unscheduled — passes the import
|
|
139
|
+
// to the first violation that survives.
|
|
140
|
+
if (isReportSuppressed(node)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
126
143
|
const fixes = [];
|
|
127
144
|
const sourceCode = context.sourceCode;
|
|
128
145
|
// Determine which identifier to use for the decorator
|
|
@@ -135,13 +152,13 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
135
152
|
}
|
|
136
153
|
else if (memoizeAliases.size > 0) {
|
|
137
154
|
// Find first alias from new package, fallback to any alias
|
|
138
|
-
const newPackageAlias = Array.from(memoizeAliases.entries()).find(([
|
|
155
|
+
const newPackageAlias = Array.from(memoizeAliases.entries()).find(([, pkg]) => pkg === MEMOIZE_MODULE)?.[0];
|
|
139
156
|
decoratorIdent =
|
|
140
157
|
newPackageAlias || Array.from(memoizeAliases.keys())[0];
|
|
141
158
|
}
|
|
142
159
|
else if (memoizeNamespaces.size > 0) {
|
|
143
160
|
// Prefer namespace from the new package if available
|
|
144
|
-
const newPackageNs = Array.from(memoizeNamespaces.entries()).find(([
|
|
161
|
+
const newPackageNs = Array.from(memoizeNamespaces.entries()).find(([, pkg]) => pkg === MEMOIZE_MODULE)?.[0];
|
|
145
162
|
const selectedNs = newPackageNs || Array.from(memoizeNamespaces.keys())[0];
|
|
146
163
|
decoratorIdent = `${selectedNs}.Memoize`;
|
|
147
164
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMemoizeGetters = 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,
|
|
@@ -58,6 +59,13 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
58
59
|
let memoizeNamespace = null;
|
|
59
60
|
let hasNamedImport = false;
|
|
60
61
|
let scheduledImportFix = false;
|
|
62
|
+
/**
|
|
63
|
+
* The `import { Memoize }` statement rides on a single violation's fix, so
|
|
64
|
+
* that violation is the file's import carrier. A suppressed carrier would
|
|
65
|
+
* take the import down with it while the surviving violations still emit
|
|
66
|
+
* `@Memoize()`, leaving a decorator with no import.
|
|
67
|
+
*/
|
|
68
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
61
69
|
return {
|
|
62
70
|
ImportDeclaration(node) {
|
|
63
71
|
if (MEMOIZE_MODULES.has(String(node.source.value))) {
|
|
@@ -94,11 +102,20 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
94
102
|
const propertyName = node.computed
|
|
95
103
|
? '[computed]'
|
|
96
104
|
: sourceCode.getText(node.key);
|
|
105
|
+
// The report is emitted even when suppressed: ESLint discards it, and
|
|
106
|
+
// reporting keeps the user's disable directive "used" so that
|
|
107
|
+
// `--report-unused-disable-directives` does not flag it.
|
|
97
108
|
context.report({
|
|
98
109
|
node,
|
|
99
110
|
messageId: 'requireMemoizeGetter',
|
|
100
111
|
data: { name: propertyName },
|
|
101
112
|
fix(fixer) {
|
|
113
|
+
// A suppressed report is dropped together with its fix. Producing
|
|
114
|
+
// no fix — and leaving the import unscheduled — passes the import
|
|
115
|
+
// to the first violation that survives.
|
|
116
|
+
if (isReportSuppressed(node)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
102
119
|
const fixes = [];
|
|
103
120
|
const getDecoratorIdent = () => {
|
|
104
121
|
if (hasNamedImport) {
|
|
@@ -7,6 +7,7 @@ exports.enforceQueryKeyTs = void 0;
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
10
11
|
// The module's path below the project root doubles as the bare specifier,
|
|
11
12
|
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
13
|
// it.
|
|
@@ -97,6 +98,17 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
97
98
|
* per-violation insertions that overlap and get dropped.
|
|
98
99
|
*/
|
|
99
100
|
const pendingReports = [];
|
|
101
|
+
/**
|
|
102
|
+
* The queryKeys import rides on one violation's fix, which makes that
|
|
103
|
+
* violation the file's import carrier. ESLint collects fixes before it
|
|
104
|
+
* applies inline disable directives, so a suppressed carrier takes the
|
|
105
|
+
* import down with it while the surviving substitutions still land —
|
|
106
|
+
* leaving the file referencing constants nothing imports (#1410).
|
|
107
|
+
* Resolving suppression here keeps a suppressed violation out of the plan
|
|
108
|
+
* entirely: it neither claims the carrier slot nor contributes a specifier
|
|
109
|
+
* to the import, which would otherwise be imported and never used.
|
|
110
|
+
*/
|
|
111
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
100
112
|
/**
|
|
101
113
|
* `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
|
|
102
114
|
* fallback keeps the rule working on ESLint versions that predate it.
|
|
@@ -246,8 +258,11 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
246
258
|
const resolutions = new Map();
|
|
247
259
|
const missingConstants = [];
|
|
248
260
|
const canImport = importSourceOf() !== null;
|
|
261
|
+
// The location handed to `context.report` below is what ESLint matches a
|
|
262
|
+
// directive against, so suppression is resolved from exactly that node.
|
|
263
|
+
const suppressed = new Set(pendingReports.filter((report) => isReportSuppressed(report.node)));
|
|
249
264
|
for (const report of pendingReports) {
|
|
250
|
-
if (!report.substitution) {
|
|
265
|
+
if (!report.substitution || suppressed.has(report)) {
|
|
251
266
|
continue;
|
|
252
267
|
}
|
|
253
268
|
const { constant, scope } = report.substitution;
|
|
@@ -267,10 +282,16 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
267
282
|
// The first applied substitution carries the import for every other one:
|
|
268
283
|
// its fix range then starts at the top of the file and ends before the
|
|
269
284
|
// remaining literals, so no two fixes of this rule overlap in a pass.
|
|
285
|
+
// Suppressed reports are skipped so the slot falls to a survivor.
|
|
270
286
|
const importCarrier = pendingReports.find((report) => {
|
|
271
287
|
const resolution = resolutions.get(report);
|
|
272
|
-
return
|
|
288
|
+
return (!suppressed.has(report) &&
|
|
289
|
+
resolution !== undefined &&
|
|
290
|
+
resolution.state !== 'conflict');
|
|
273
291
|
});
|
|
292
|
+
// Suppressed violations are still reported: ESLint discards them, and
|
|
293
|
+
// reporting keeps the user's directive "used" so that
|
|
294
|
+
// `--report-unused-disable-directives` does not flag it.
|
|
274
295
|
for (const report of pendingReports) {
|
|
275
296
|
context.report({
|
|
276
297
|
node: report.node,
|
|
@@ -279,7 +300,8 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
279
300
|
fix(fixer) {
|
|
280
301
|
const { substitution } = report;
|
|
281
302
|
const resolution = resolutions.get(report);
|
|
282
|
-
if (
|
|
303
|
+
if (suppressed.has(report) ||
|
|
304
|
+
!substitution ||
|
|
283
305
|
!resolution ||
|
|
284
306
|
resolution.state === 'conflict') {
|
|
285
307
|
return null;
|
|
@@ -4,6 +4,7 @@ exports.enforceStableHashSpreadProps = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
6
|
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
7
8
|
const DEFAULT_HASH_IMPORT = {
|
|
8
9
|
source: 'functions/src/util/hash/stableHash',
|
|
9
10
|
importName: 'stableHash',
|
|
@@ -215,7 +216,12 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
215
216
|
...(options.allowedHashFunctions ?? []),
|
|
216
217
|
]);
|
|
217
218
|
const hookNames = new Set([...DEFAULT_HOOKS, ...userHookNames]);
|
|
219
|
+
// The `import { stableHash }` statement rides on a single violation's fix,
|
|
220
|
+
// so that violation is the file's import carrier. A suppressed carrier
|
|
221
|
+
// would take the import down with it while the surviving violations still
|
|
222
|
+
// emit `stableHash(...)`, leaving calls to an unbound identifier.
|
|
218
223
|
let importPlanned = false;
|
|
224
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
219
225
|
const hashIdentifier = existingHashLocalNames[0] ?? hashImport.importName;
|
|
220
226
|
const functionStack = [];
|
|
221
227
|
function getCurrentComponentContext() {
|
|
@@ -294,11 +300,21 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
294
300
|
if (offendingElements.length === 0)
|
|
295
301
|
return;
|
|
296
302
|
const offendingNames = Array.from(new Set(offendingElements.map(({ name }) => name)));
|
|
303
|
+
// The report is emitted even when suppressed: ESLint discards it, and
|
|
304
|
+
// reporting keeps the user's disable directive "used" so that
|
|
305
|
+
// `--report-unused-disable-directives` does not flag it.
|
|
297
306
|
context.report({
|
|
298
307
|
node: depsArg,
|
|
299
308
|
messageId: 'wrapSpreadPropsWithStableHash',
|
|
300
309
|
data: { names: offendingNames.join(', ') },
|
|
301
310
|
fix(fixer) {
|
|
311
|
+
// A suppressed report is dropped together with its fix. Producing
|
|
312
|
+
// no fix — and leaving the import unscheduled — passes the import
|
|
313
|
+
// to the first violation that survives. The check runs on the
|
|
314
|
+
// reported node so it resolves the same location ESLint does.
|
|
315
|
+
if (isReportSuppressed(depsArg)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
302
318
|
const fixes = [];
|
|
303
319
|
const seen = new Set();
|
|
304
320
|
for (const { node: targetNode } of offendingElements) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.fastDeepEqualOverMicrodiff = 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
|
exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'fast-deep-equal-over-microdiff',
|
|
8
9
|
meta: {
|
|
@@ -31,6 +32,13 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
31
32
|
let fastDeepEqualImportName = 'isEqual';
|
|
32
33
|
const reportedNodes = new Set();
|
|
33
34
|
let plannedFastDeepEqualImport = false;
|
|
35
|
+
/**
|
|
36
|
+
* The `import ... from 'fast-deep-equal'` statement rides on a single
|
|
37
|
+
* violation's fix, so that violation is the file's import carrier. A
|
|
38
|
+
* suppressed carrier would take the import down with it while the surviving
|
|
39
|
+
* violations still emit `isEqual(...)` calls, leaving them unbound.
|
|
40
|
+
*/
|
|
41
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
34
42
|
const isChainExpression = (node) => node.type === utils_1.AST_NODE_TYPES.ChainExpression;
|
|
35
43
|
function isMicrodiffCallee(callee) {
|
|
36
44
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -292,6 +300,12 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
292
300
|
* Create a fix for replacing microdiff equality check with fast-deep-equal
|
|
293
301
|
*/
|
|
294
302
|
function createFix(fixer, node, diffCall, isEquality) {
|
|
303
|
+
// A suppressed report is dropped together with its fix. Producing no fix
|
|
304
|
+
// — and leaving the import unscheduled — passes the carrier slot to the
|
|
305
|
+
// first violation that survives.
|
|
306
|
+
if (isReportSuppressed(node)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
295
309
|
const args = diffCall.arguments;
|
|
296
310
|
if (args.length !== 2) {
|
|
297
311
|
return null; // Can't fix if not exactly 2 arguments
|