@blumintinc/eslint-plugin-blumint 1.12.6 → 1.14.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 +171 -99
- package/lib/index.js +300 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -11
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +52 -96
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +820 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +5 -2
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +278 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +4 -4
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +262 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +350 -3
- package/lib/rules/enforce-firestore-facade.js +349 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +92 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +147 -0
- package/lib/rules/enforce-microdiff.js +3 -2
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +9 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +10 -6
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +100 -0
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +90 -14
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +92 -53
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +351 -83
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +277 -0
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +314 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +22 -5
- package/lib/rules/no-compositing-layer-props.js +6 -4
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +320 -68
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +398 -28
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +14 -14
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +22 -9
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +331 -0
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +272 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +206 -0
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +328 -0
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +173 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +133 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +49 -49
- package/lib/rules/no-undefined-null-passthrough.js +217 -40
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -3
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +5 -2
- package/lib/rules/no-unused-props.js +451 -112
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +236 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +394 -0
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +228 -35
- package/lib/rules/prefer-block-comments-for-declarations.js +22 -7
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +251 -0
- package/lib/rules/prefer-fragment-component.js +4 -4
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +305 -31
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +357 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +435 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +7 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +188 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +277 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +169 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +431 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +25 -2
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +2 -2
- package/lib/rules/require-memo.js +12 -2
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +9 -4
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +271 -0
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +34 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +23 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -13,16 +13,69 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
alwaysTrueCondition: '
|
|
17
|
-
alwaysFalseCondition: '
|
|
16
|
+
alwaysTrueCondition: 'What\'s wrong → Condition "{{condition}}" is always true.\nWhy it matters → The guarded branch runs every time, which can hide logic errors and leave redundant checks.\nHow to fix → Remove the check, or rewrite "{{condition}}" so it depends on runtime values instead of constants.',
|
|
17
|
+
alwaysFalseCondition: 'What\'s wrong → Condition "{{condition}}" is always false.\nWhy it matters → The guarded branch is unreachable, which leaves misleading or dead code.\nHow to fix → Remove the unreachable branch, or adjust "{{condition}}" so it can evaluate to true when intended.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
const normalizedConditionText = (node) => sourceCode.getText(node).replace(/\s+/g, ' ').trim();
|
|
24
|
+
const conditionTextFor = (node) => ({
|
|
25
|
+
condition: normalizedConditionText(node),
|
|
26
|
+
});
|
|
22
27
|
// Track nodes that have already been reported to prevent duplicate reports
|
|
23
28
|
const reportedNodes = new Set();
|
|
24
29
|
// Track parent nodes that have been evaluated to prevent duplicate reports on children
|
|
25
30
|
const evaluatedParentNodes = new Set();
|
|
31
|
+
// Sentinel distinguishes "no const literal resolved" from valid literal values (including null/undefined)
|
|
32
|
+
const NOT_FOUND = Symbol('literal-not-found');
|
|
33
|
+
/**
|
|
34
|
+
* Walks the current scope chain to locate a variable by name so identifier
|
|
35
|
+
* lookups can resolve const initializers even when declared in parent scopes.
|
|
36
|
+
*/
|
|
37
|
+
function findVariableInScopes(name) {
|
|
38
|
+
let currentScope = context.getScope();
|
|
39
|
+
while (currentScope) {
|
|
40
|
+
const variable = currentScope.variables.find((variableItem) => variableItem.name === name);
|
|
41
|
+
if (variable) {
|
|
42
|
+
return variable;
|
|
43
|
+
}
|
|
44
|
+
currentScope = currentScope.upper;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function isConstIdentifierDefinition(def, identifierName) {
|
|
49
|
+
return (def.type === 'Variable' &&
|
|
50
|
+
def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
51
|
+
def.node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
52
|
+
def.node.id.name === identifierName &&
|
|
53
|
+
def.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
54
|
+
def.parent.kind === 'const');
|
|
55
|
+
}
|
|
56
|
+
function findConstIdentifierDefinition(variable, identifierName) {
|
|
57
|
+
return variable.defs.find((def) => isConstIdentifierDefinition(def, identifierName));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves an identifier to the literal it is initialized with when declared
|
|
61
|
+
* as a const. Returns the NOT_FOUND sentinel when the binding is absent,
|
|
62
|
+
* not a const, or not initialized with a literal value.
|
|
63
|
+
*/
|
|
64
|
+
function resolveIdentifierValue(identifier) {
|
|
65
|
+
const variable = findVariableInScopes(identifier.name);
|
|
66
|
+
if (!variable) {
|
|
67
|
+
return NOT_FOUND;
|
|
68
|
+
}
|
|
69
|
+
const definition = findConstIdentifierDefinition(variable, identifier.name);
|
|
70
|
+
if (!definition) {
|
|
71
|
+
return NOT_FOUND;
|
|
72
|
+
}
|
|
73
|
+
const initializer = definition.node.init;
|
|
74
|
+
if (initializer?.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
75
|
+
return initializer.value;
|
|
76
|
+
}
|
|
77
|
+
return NOT_FOUND;
|
|
78
|
+
}
|
|
26
79
|
/**
|
|
27
80
|
* Checks if a literal value is always truthy or falsy
|
|
28
81
|
*/
|
|
@@ -1309,6 +1362,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1309
1362
|
context.report({
|
|
1310
1363
|
node,
|
|
1311
1364
|
messageId: 'alwaysTrueCondition',
|
|
1365
|
+
data: conditionTextFor(node),
|
|
1312
1366
|
});
|
|
1313
1367
|
reportedNodes.add(node);
|
|
1314
1368
|
}
|
|
@@ -1316,6 +1370,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1316
1370
|
context.report({
|
|
1317
1371
|
node,
|
|
1318
1372
|
messageId: 'alwaysFalseCondition',
|
|
1373
|
+
data: conditionTextFor(node),
|
|
1319
1374
|
});
|
|
1320
1375
|
reportedNodes.add(node);
|
|
1321
1376
|
}
|
|
@@ -1372,12 +1427,14 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1372
1427
|
context.report({
|
|
1373
1428
|
node: node.test,
|
|
1374
1429
|
messageId: 'alwaysTrueCondition',
|
|
1430
|
+
data: conditionTextFor(node.test),
|
|
1375
1431
|
});
|
|
1376
1432
|
}
|
|
1377
1433
|
else {
|
|
1378
1434
|
context.report({
|
|
1379
1435
|
node: node.test,
|
|
1380
1436
|
messageId: 'alwaysFalseCondition',
|
|
1437
|
+
data: conditionTextFor(node.test),
|
|
1381
1438
|
});
|
|
1382
1439
|
}
|
|
1383
1440
|
reportedNodes.add(node.test);
|
|
@@ -1385,14 +1442,17 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1385
1442
|
}
|
|
1386
1443
|
// Special cases for tests with variables
|
|
1387
1444
|
if (switchStatement.discriminant.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1388
|
-
node.test.type === utils_1.AST_NODE_TYPES.Identifier
|
|
1389
|
-
node.test
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1445
|
+
node.test.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1446
|
+
const identifierValue = resolveIdentifierValue(node.test);
|
|
1447
|
+
if (identifierValue !== NOT_FOUND &&
|
|
1448
|
+
!reportedNodes.has(node.test)) {
|
|
1449
|
+
const messageId = identifierValue === switchStatement.discriminant.value
|
|
1450
|
+
? 'alwaysTrueCondition'
|
|
1451
|
+
: 'alwaysFalseCondition';
|
|
1393
1452
|
context.report({
|
|
1394
1453
|
node: node.test,
|
|
1395
|
-
messageId
|
|
1454
|
+
messageId,
|
|
1455
|
+
data: conditionTextFor(node.test),
|
|
1396
1456
|
});
|
|
1397
1457
|
reportedNodes.add(node.test);
|
|
1398
1458
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export type MessageIds = 'noArrayLengthInDeps';
|
|
3
|
+
type Options = [
|
|
4
|
+
{
|
|
5
|
+
hashImport?: {
|
|
6
|
+
source?: string;
|
|
7
|
+
importName?: string;
|
|
8
|
+
};
|
|
9
|
+
}?
|
|
10
|
+
];
|
|
11
|
+
export declare const noArrayLengthInDeps: TSESLint.RuleModule<"noArrayLengthInDeps", Options, TSESLint.RuleListener>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noArrayLengthInDeps = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// React hooks to check
|
|
7
|
+
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
8
|
+
const DEFAULT_HASH_IMPORT = {
|
|
9
|
+
source: 'functions/src/util/hash/stableHash',
|
|
10
|
+
importName: 'stableHash',
|
|
11
|
+
};
|
|
12
|
+
function isHookCall(node) {
|
|
13
|
+
const callee = node.callee;
|
|
14
|
+
return (callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name));
|
|
15
|
+
}
|
|
16
|
+
function isLengthMemberExpression(node) {
|
|
17
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
18
|
+
return (!node.computed &&
|
|
19
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
20
|
+
node.property.name === 'length');
|
|
21
|
+
}
|
|
22
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
23
|
+
return isLengthMemberExpression(node.expression);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function getLengthMember(node) {
|
|
28
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
29
|
+
isLengthMemberExpression(node)) {
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
33
|
+
const expr = node.expression;
|
|
34
|
+
return getLengthMember(expr);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function getBaseExpression(member) {
|
|
39
|
+
// For foo?.bar.length we want foo?.bar as base
|
|
40
|
+
return member.object;
|
|
41
|
+
}
|
|
42
|
+
function getLastPropertyName(expr) {
|
|
43
|
+
let current = expr;
|
|
44
|
+
while (current.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
45
|
+
current = current.expression;
|
|
46
|
+
}
|
|
47
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
48
|
+
return current.name;
|
|
49
|
+
}
|
|
50
|
+
if (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
51
|
+
if (!current.computed &&
|
|
52
|
+
current.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
53
|
+
return current.property.name;
|
|
54
|
+
}
|
|
55
|
+
// Fallback to walking further up the chain
|
|
56
|
+
return getLastPropertyName(current.object);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function generateUniqueName(base, taken) {
|
|
61
|
+
let candidate = `${base}Hash`;
|
|
62
|
+
if (!taken.has(candidate))
|
|
63
|
+
return candidate;
|
|
64
|
+
let i = 2;
|
|
65
|
+
while (taken.has(`${candidate}${i}`)) {
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
return `${candidate}${i}`;
|
|
69
|
+
}
|
|
70
|
+
function collectAllTakenNames(sourceCode) {
|
|
71
|
+
const names = new Set();
|
|
72
|
+
const scopeManager = sourceCode.scopeManager;
|
|
73
|
+
const visit = (scope) => {
|
|
74
|
+
if (!scope)
|
|
75
|
+
return;
|
|
76
|
+
for (const v of scope.variables) {
|
|
77
|
+
names.add(v.name);
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(scope.childScopes)) {
|
|
80
|
+
for (const child of scope.childScopes)
|
|
81
|
+
visit(child);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
visit(scopeManager?.globalScope);
|
|
85
|
+
return names;
|
|
86
|
+
}
|
|
87
|
+
function findEnclosingFunction(node) {
|
|
88
|
+
let current = node;
|
|
89
|
+
while (current) {
|
|
90
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
91
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
92
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
current = current.parent;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function ensureWeakMapEntry(map, key, factory) {
|
|
100
|
+
const existing = map.get(key);
|
|
101
|
+
if (existing)
|
|
102
|
+
return existing;
|
|
103
|
+
const next = factory();
|
|
104
|
+
map.set(key, next);
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
function isUseMemoImported(sourceCode) {
|
|
108
|
+
const program = sourceCode.ast;
|
|
109
|
+
for (const node of program.body) {
|
|
110
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
111
|
+
node.source.value === 'react') {
|
|
112
|
+
for (const spec of node.specifiers) {
|
|
113
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
114
|
+
spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
115
|
+
spec.imported.name === 'useMemo') {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
function isStableHashImported(sourceCode, hashSource, hashImportName) {
|
|
124
|
+
const program = sourceCode.ast;
|
|
125
|
+
for (const node of program.body) {
|
|
126
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
127
|
+
node.source.value === hashSource) {
|
|
128
|
+
for (const spec of node.specifiers) {
|
|
129
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
130
|
+
spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
131
|
+
spec.imported.name === hashImportName) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
140
|
+
name: 'no-array-length-in-deps',
|
|
141
|
+
meta: {
|
|
142
|
+
type: 'problem',
|
|
143
|
+
docs: {
|
|
144
|
+
description: 'Detects array.length entries in React hook dependency arrays because length ignores content changes; auto-fixes by memoizing stableHash(array) with useMemo and depending on the hash instead.',
|
|
145
|
+
recommended: 'error',
|
|
146
|
+
},
|
|
147
|
+
fixable: 'code',
|
|
148
|
+
schema: [
|
|
149
|
+
{
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
hashImport: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
source: { type: 'string' },
|
|
156
|
+
importName: { type: 'string' },
|
|
157
|
+
},
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
messages: {
|
|
165
|
+
noArrayLengthInDeps: 'Dependency array includes length-based entries ({{dependencies}}). Array length only changes when items are added or removed, so hooks miss updates when array contents change at the same size. Memoize a stableHash of each array with useMemo and depend on that hash so the hook reruns when contents change.',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
defaultOptions: [{}],
|
|
169
|
+
create(context) {
|
|
170
|
+
const [options = {}] = context.options;
|
|
171
|
+
const { hashImport } = options;
|
|
172
|
+
const hashImportConfig = {
|
|
173
|
+
source: hashImport?.source ?? DEFAULT_HASH_IMPORT.source,
|
|
174
|
+
importName: hashImport?.importName ?? DEFAULT_HASH_IMPORT.importName,
|
|
175
|
+
};
|
|
176
|
+
// Track planned file-wide changes to avoid overlapping fixers
|
|
177
|
+
let importsPlanned = false;
|
|
178
|
+
const perFuncDeclaredBases = new WeakMap();
|
|
179
|
+
const perFuncBaseToVar = new WeakMap();
|
|
180
|
+
return {
|
|
181
|
+
CallExpression(node) {
|
|
182
|
+
if (!isHookCall(node))
|
|
183
|
+
return;
|
|
184
|
+
if (node.arguments.length < 2)
|
|
185
|
+
return;
|
|
186
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
187
|
+
if (depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
188
|
+
return;
|
|
189
|
+
// Collect .length deps
|
|
190
|
+
const lengthDeps = [];
|
|
191
|
+
for (const el of depsArg.elements) {
|
|
192
|
+
if (!el)
|
|
193
|
+
continue;
|
|
194
|
+
if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
195
|
+
continue;
|
|
196
|
+
const member = getLengthMember(el);
|
|
197
|
+
if (member) {
|
|
198
|
+
lengthDeps.push({ element: el, member });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (lengthDeps.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
const sourceCode = context.getSourceCode();
|
|
204
|
+
const dependencies = lengthDeps
|
|
205
|
+
.map(({ element }) => sourceCode.getText(element))
|
|
206
|
+
.join(', ');
|
|
207
|
+
// Report once on the dependency array
|
|
208
|
+
context.report({
|
|
209
|
+
node: depsArg,
|
|
210
|
+
messageId: 'noArrayLengthInDeps',
|
|
211
|
+
data: {
|
|
212
|
+
dependencies,
|
|
213
|
+
},
|
|
214
|
+
fix(fixer) {
|
|
215
|
+
const fixes = [];
|
|
216
|
+
const hostFn = findEnclosingFunction(node) ?? sourceCode.ast;
|
|
217
|
+
const declaredBases = ensureWeakMapEntry(perFuncDeclaredBases, hostFn, () => new Set());
|
|
218
|
+
const baseToVar = ensureWeakMapEntry(perFuncBaseToVar, hostFn, () => new Map());
|
|
219
|
+
// Prepare variable names (consistent across file) and taken names (across all scopes)
|
|
220
|
+
const allTaken = collectAllTakenNames(sourceCode);
|
|
221
|
+
for (const name of baseToVar.values()) {
|
|
222
|
+
allTaken.add(name);
|
|
223
|
+
}
|
|
224
|
+
for (const { member } of lengthDeps) {
|
|
225
|
+
const baseExpr = getBaseExpression(member);
|
|
226
|
+
const baseText = sourceCode.getText(baseExpr);
|
|
227
|
+
if (!baseToVar.has(baseText)) {
|
|
228
|
+
const lastPropName = getLastPropertyName(baseExpr) || 'array';
|
|
229
|
+
const varName = generateUniqueName(lastPropName, allTaken);
|
|
230
|
+
baseToVar.set(baseText, varName);
|
|
231
|
+
allTaken.add(varName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Build declarations text (one per base)
|
|
235
|
+
let declText = '';
|
|
236
|
+
for (const { member } of lengthDeps) {
|
|
237
|
+
const baseExpr = getBaseExpression(member);
|
|
238
|
+
const baseText = sourceCode.getText(baseExpr);
|
|
239
|
+
if (!declaredBases.has(baseText)) {
|
|
240
|
+
const varName = baseToVar.get(baseText);
|
|
241
|
+
declText += `const ${varName} = useMemo(() => ${hashImportConfig.importName}(${baseText}), [${baseText}]);\n`;
|
|
242
|
+
declaredBases.add(baseText);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (declText) {
|
|
246
|
+
// Add a blank line after declarations block
|
|
247
|
+
declText += `\n`;
|
|
248
|
+
}
|
|
249
|
+
// Determine import text and insertion strategy
|
|
250
|
+
const program = sourceCode.ast;
|
|
251
|
+
const importDecls = program.body.filter((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
252
|
+
// Compute indentation based on the whitespace before the first token
|
|
253
|
+
const fullText = sourceCode.getText();
|
|
254
|
+
const prefixBeforeProgram = fullText.slice(0, program.range[0]);
|
|
255
|
+
const lastNewlineIndex = prefixBeforeProgram.lastIndexOf('\n');
|
|
256
|
+
const indent = lastNewlineIndex >= 0
|
|
257
|
+
? prefixBeforeProgram.slice(lastNewlineIndex + 1)
|
|
258
|
+
: prefixBeforeProgram;
|
|
259
|
+
let importText = '';
|
|
260
|
+
const needUseMemo = !isUseMemoImported(sourceCode);
|
|
261
|
+
const needStableHash = !isStableHashImported(sourceCode, hashImportConfig.source, hashImportConfig.importName);
|
|
262
|
+
if (needUseMemo)
|
|
263
|
+
importText += `${indent}import { useMemo } from 'react';\n`;
|
|
264
|
+
if (needStableHash)
|
|
265
|
+
importText += `${indent}import { ${hashImportConfig.importName} } from '${hashImportConfig.source}';\n`;
|
|
266
|
+
if (importDecls.length === 0) {
|
|
267
|
+
// No existing imports. Normalize by removing leading whitespace and inserting at file start with no indentation.
|
|
268
|
+
if (declText || importText) {
|
|
269
|
+
// Build non-indented versions of import and decl blocks
|
|
270
|
+
let importTextNoIndent = '';
|
|
271
|
+
if (needUseMemo)
|
|
272
|
+
importTextNoIndent += `import { useMemo } from 'react';\n`;
|
|
273
|
+
if (needStableHash)
|
|
274
|
+
importTextNoIndent += `import { ${hashImportConfig.importName} } from '${hashImportConfig.source}';\n`;
|
|
275
|
+
const declNoIndent = declText;
|
|
276
|
+
const combined = `${importTextNoIndent}${importTextNoIndent && declNoIndent ? '\n' : ''}${declNoIndent}`;
|
|
277
|
+
// Remove leading whitespace
|
|
278
|
+
fixes.push(fixer.replaceTextRange([0, program.range[0]], ''));
|
|
279
|
+
// Insert at column 0
|
|
280
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], combined));
|
|
281
|
+
}
|
|
282
|
+
importsPlanned = true;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// Existing imports present: insert missing import lines before the first import, and declarations after the last import
|
|
286
|
+
const firstImport = importDecls[0];
|
|
287
|
+
const lastImport = importDecls[importDecls.length - 1];
|
|
288
|
+
if (importText && !importsPlanned) {
|
|
289
|
+
fixes.push(fixer.insertTextBefore(firstImport, importText));
|
|
290
|
+
importsPlanned = true;
|
|
291
|
+
}
|
|
292
|
+
if (declText) {
|
|
293
|
+
const declWithIndent = declText
|
|
294
|
+
.split('\n')
|
|
295
|
+
.map((line) => (line ? `${indent}${line}` : line))
|
|
296
|
+
.join('\n');
|
|
297
|
+
fixes.push(fixer.insertTextAfter(lastImport, `\n${declWithIndent}`));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Replace each .length dep with the corresponding var name
|
|
301
|
+
for (const { element, member } of lengthDeps) {
|
|
302
|
+
const baseExpr = getBaseExpression(member);
|
|
303
|
+
const baseText = sourceCode.getText(baseExpr);
|
|
304
|
+
const varName = baseToVar.get(baseText);
|
|
305
|
+
fixes.push(fixer.replaceText(element, varName));
|
|
306
|
+
}
|
|
307
|
+
return fixes;
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
//# sourceMappingURL=no-array-length-in-deps.js.map
|
|
@@ -17,6 +17,9 @@ exports.noAsyncArrayFilter = (0, createRule_1.createRule)({
|
|
|
17
17
|
context.report({
|
|
18
18
|
node: callback,
|
|
19
19
|
messageId: 'unexpected',
|
|
20
|
+
data: {
|
|
21
|
+
methodName: node.callee.property.name,
|
|
22
|
+
},
|
|
20
23
|
});
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -27,12 +30,12 @@ exports.noAsyncArrayFilter = (0, createRule_1.createRule)({
|
|
|
27
30
|
meta: {
|
|
28
31
|
type: 'problem',
|
|
29
32
|
docs: {
|
|
30
|
-
description:
|
|
33
|
+
description: 'Disallow async callbacks in Array.filter(). Async predicates return Promises that are always truthy to the filter, so no element is ever removed. Resolve async checks first (Promise.all + map) or use a synchronous predicate to decide which items to keep.',
|
|
31
34
|
recommended: 'error',
|
|
32
35
|
},
|
|
33
36
|
schema: [],
|
|
34
37
|
messages: {
|
|
35
|
-
unexpected: 'Async
|
|
38
|
+
unexpected: 'Async predicate in {{methodName}} keeps every element because Array.filter does not await Promises, so the returned Promise is always treated as truthy. Resolve the async checks before filtering (e.g., const results = await Promise.all(items.map(check)); const filtered = items.filter((_, i) => results[i]);) or switch to a synchronous predicate.',
|
|
36
39
|
},
|
|
37
40
|
},
|
|
38
41
|
defaultOptions: [],
|