@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
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noUuidv4Base62AsKey = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'no-uuidv4-base62-as-key',
|
|
8
9
|
meta: {
|
|
@@ -13,13 +14,13 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
13
14
|
},
|
|
14
15
|
schema: [],
|
|
15
16
|
messages: {
|
|
16
|
-
noUuidv4Base62AsKey: '
|
|
17
|
-
'
|
|
18
|
-
'Use a stable identifier from your data instead.',
|
|
17
|
+
noUuidv4Base62AsKey: 'Key "{{keyExpression}}" comes from uuidv4Base62(), which regenerates on every render so React cannot reconcile list items and may remount components, drop local state, or reorder DOM nodes. ' +
|
|
18
|
+
'Use a stable identifier from your data (for example a database id, slug, or memoized array of ids) instead of generating a new UUID inside render.',
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
|
+
const sourceCode = context.sourceCode;
|
|
23
24
|
// Track imported uuidv4Base62 identifiers
|
|
24
25
|
const importedUuidv4Base62 = new Set();
|
|
25
26
|
// Track elements that we've already reported to avoid duplicate reports
|
|
@@ -31,14 +32,18 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
31
32
|
// Flag to indicate we've seen the itemKeys pattern
|
|
32
33
|
let hasPreGeneratedKeys = false;
|
|
33
34
|
// Helper function to report a rule violation while avoiding duplicates
|
|
34
|
-
function reportViolation(node, messageId = 'noUuidv4Base62AsKey') {
|
|
35
|
+
function reportViolation(node, keyExpression, messageId = 'noUuidv4Base62AsKey') {
|
|
35
36
|
// Skip if we've already reported this element
|
|
36
37
|
if (reportedElements.has(node))
|
|
37
38
|
return;
|
|
38
39
|
reportedElements.add(node);
|
|
40
|
+
const keyExpressionText = keyExpression
|
|
41
|
+
? sourceCode.getText(keyExpression).trim()
|
|
42
|
+
: 'uuidv4Base62()';
|
|
39
43
|
context.report({
|
|
40
44
|
node,
|
|
41
45
|
messageId,
|
|
46
|
+
data: { keyExpression: keyExpressionText },
|
|
42
47
|
});
|
|
43
48
|
}
|
|
44
49
|
// Helper to check if a node is a call to uuidv4Base62()
|
|
@@ -117,7 +122,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
117
122
|
const { expression } = attr.value;
|
|
118
123
|
// Direct uuidv4Base62() call in key
|
|
119
124
|
if (containsUuidV4Base62Call(expression)) {
|
|
120
|
-
reportViolation(jsxElement);
|
|
125
|
+
reportViolation(jsxElement, expression);
|
|
121
126
|
return;
|
|
122
127
|
}
|
|
123
128
|
// Check for member expressions (item.key pattern)
|
|
@@ -128,12 +133,12 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
128
133
|
const objName = expression.object.name;
|
|
129
134
|
// Check if this variable has been marked as containing keys with uuidv4Base62
|
|
130
135
|
if (variablesWithUuidv4Base62Keys.has(objName)) {
|
|
131
|
-
reportViolation(jsxElement);
|
|
136
|
+
reportViolation(jsxElement, expression);
|
|
132
137
|
return;
|
|
133
138
|
}
|
|
134
139
|
// Special case for the failing 'itemKeys' test
|
|
135
140
|
if (hasPreGeneratedKeys) {
|
|
136
|
-
const ancestors =
|
|
141
|
+
const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, jsxElement);
|
|
137
142
|
// Check if we're inside a map callback
|
|
138
143
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
139
144
|
const ancestor = ancestors[i];
|
|
@@ -144,7 +149,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
144
149
|
ancestor.callee.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
145
150
|
// If we're mapping over a variable with pre-generated keys
|
|
146
151
|
if (variablesWithUuidv4Base62Keys.has(ancestor.callee.object.name)) {
|
|
147
|
-
reportViolation(jsxElement);
|
|
152
|
+
reportViolation(jsxElement, expression);
|
|
148
153
|
return;
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -155,8 +160,8 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
162
|
// Helper to check for react mapping functions
|
|
158
|
-
function isInsideMapCallback() {
|
|
159
|
-
const ancestors =
|
|
163
|
+
function isInsideMapCallback(node) {
|
|
164
|
+
const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
|
|
160
165
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
161
166
|
const ancestor = ancestors[i];
|
|
162
167
|
if (ancestor.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -276,7 +281,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
276
281
|
},
|
|
277
282
|
// Check JSX elements for uuidv4Base62 keys
|
|
278
283
|
JSXElement(node) {
|
|
279
|
-
if (!isInsideMapCallback())
|
|
284
|
+
if (!isInsideMapCallback(node))
|
|
280
285
|
return;
|
|
281
286
|
if (node.openingElement.attributes) {
|
|
282
287
|
checkJSXAttributesForUuidv4Base62(node.openingElement.attributes, node);
|
|
@@ -284,7 +289,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
284
289
|
},
|
|
285
290
|
// Check JSX fragments for children with uuidv4Base62 keys
|
|
286
291
|
JSXFragment(node) {
|
|
287
|
-
if (!isInsideMapCallback())
|
|
292
|
+
if (!isInsideMapCallback(node))
|
|
288
293
|
return;
|
|
289
294
|
for (const child of node.children) {
|
|
290
295
|
if (child.type === utils_1.AST_NODE_TYPES.JSXElement &&
|
|
@@ -336,7 +341,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
|
|
|
336
341
|
utils_1.AST_NODE_TYPES.Identifier &&
|
|
337
342
|
attr.value.expression.property.name === 'key') {
|
|
338
343
|
// The test case - directly report this element
|
|
339
|
-
reportViolation(returnExpr);
|
|
344
|
+
reportViolation(returnExpr, attr.value.expression);
|
|
340
345
|
break;
|
|
341
346
|
}
|
|
342
347
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [
|
|
2
3
|
{
|
|
3
4
|
allowWithQueryOrHash?: boolean;
|
|
4
5
|
}
|
|
5
6
|
];
|
|
6
|
-
export declare const omitIndexHtml:
|
|
7
|
+
export declare const omitIndexHtml: TSESLint.RuleModule<"omitIndexHtml", Options, TSESLint.RuleListener>;
|
|
7
8
|
export {};
|
|
@@ -25,8 +25,26 @@ function hasQueryOrHash(url) {
|
|
|
25
25
|
* Removes 'index.html' from a URL and ensures proper trailing slash
|
|
26
26
|
*/
|
|
27
27
|
function fixUrl(url) {
|
|
28
|
-
// Replace /index.html with /
|
|
29
|
-
return url
|
|
28
|
+
// Replace /index.html/ with / and /index.html with / at the end or before ?/#/${
|
|
29
|
+
return url
|
|
30
|
+
.replace(/\/index\.html\//g, '/')
|
|
31
|
+
.replace(/\/index\.html(?=(?:$|[?#]|\$\{))/g, '/');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reconstructs a template literal using cooked values so static checks and
|
|
35
|
+
* reporting align with what runtime sees (escape sequences are resolved).
|
|
36
|
+
*/
|
|
37
|
+
function describeTemplateLiteral(node, sourceCode) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
node.quasis.forEach((quasi, index) => {
|
|
40
|
+
const text = quasi.value.cooked ?? quasi.value.raw;
|
|
41
|
+
parts.push(text);
|
|
42
|
+
const expression = node.expressions[index];
|
|
43
|
+
if (expression) {
|
|
44
|
+
parts.push(`\${${sourceCode.getText(expression)}}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return parts.join('');
|
|
30
48
|
}
|
|
31
49
|
exports.omitIndexHtml = (0, createRule_1.createRule)({
|
|
32
50
|
name: 'omit-index-html',
|
|
@@ -49,11 +67,12 @@ exports.omitIndexHtml = (0, createRule_1.createRule)({
|
|
|
49
67
|
},
|
|
50
68
|
],
|
|
51
69
|
messages: {
|
|
52
|
-
omitIndexHtml: '
|
|
70
|
+
omitIndexHtml: 'URL "{{url}}" includes "index.html", which servers already serve implicitly for directory paths. Keeping the file name creates duplicate URLs, breaks canonical links, and can make caches treat the same page as different assets. {{fixHint}}',
|
|
53
71
|
},
|
|
54
72
|
},
|
|
55
73
|
defaultOptions: [{ allowWithQueryOrHash: true }],
|
|
56
74
|
create(context, [options]) {
|
|
75
|
+
const sourceCode = context.getSourceCode();
|
|
57
76
|
const allowWithQueryOrHash = options.allowWithQueryOrHash !== false;
|
|
58
77
|
return {
|
|
59
78
|
Literal(node) {
|
|
@@ -65,22 +84,58 @@ exports.omitIndexHtml = (0, createRule_1.createRule)({
|
|
|
65
84
|
// Skip if it has query parameters or hash fragments and we're allowing those
|
|
66
85
|
if (allowWithQueryOrHash && hasQueryOrHash(value))
|
|
67
86
|
return;
|
|
87
|
+
const suggestedUrl = fixUrl(value);
|
|
88
|
+
const fixHint = `Replace it with the directory path (e.g., "${suggestedUrl}").`;
|
|
68
89
|
context.report({
|
|
69
90
|
node,
|
|
70
91
|
messageId: 'omitIndexHtml',
|
|
92
|
+
data: {
|
|
93
|
+
url: value,
|
|
94
|
+
suggestedUrl,
|
|
95
|
+
fixHint,
|
|
96
|
+
},
|
|
71
97
|
fix: (fixer) => {
|
|
72
|
-
|
|
98
|
+
const quote = sourceCode.getText(node).startsWith("'")
|
|
99
|
+
? "'"
|
|
100
|
+
: '"';
|
|
101
|
+
const escapedUrl = suggestedUrl.split(quote).join(`\\${quote}`);
|
|
102
|
+
return fixer.replaceText(node, `${quote}${escapedUrl}${quote}`);
|
|
73
103
|
},
|
|
74
104
|
});
|
|
75
105
|
}
|
|
76
106
|
},
|
|
77
107
|
TemplateLiteral(node) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
const hasIndexHtmlInStatic = node.quasis.some((quasi) => {
|
|
109
|
+
const text = quasi.value.cooked ?? quasi.value.raw;
|
|
110
|
+
return text.includes('/index.html');
|
|
111
|
+
});
|
|
112
|
+
if (!hasIndexHtmlInStatic) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const value = describeTemplateLiteral(node, sourceCode);
|
|
116
|
+
// Check if it's a URL and contains index.html
|
|
117
|
+
if (isLikelyUrl(value) && value.includes('/index.html')) {
|
|
118
|
+
// Skip if it has query parameters or hash fragments and we're allowing those
|
|
119
|
+
if (allowWithQueryOrHash && hasQueryOrHash(value))
|
|
120
|
+
return;
|
|
121
|
+
const hasDynamicParts = node.expressions.length > 0;
|
|
122
|
+
const suggestedUrlRaw = fixUrl(value);
|
|
123
|
+
const suggestedUrl = hasDynamicParts
|
|
124
|
+
? `\`${suggestedUrlRaw
|
|
125
|
+
.replace(/\\/g, '\\\\')
|
|
126
|
+
.replace(/`/g, '\\`')}\``
|
|
127
|
+
: suggestedUrlRaw;
|
|
128
|
+
const fixHint = hasDynamicParts
|
|
129
|
+
? `Remove "index.html" from the static portion of the template so it resolves to the directory path (e.g., ${suggestedUrl}).`
|
|
130
|
+
: `Replace it with the directory path (e.g., "${suggestedUrl}").`;
|
|
81
131
|
context.report({
|
|
82
132
|
node,
|
|
83
133
|
messageId: 'omitIndexHtml',
|
|
134
|
+
data: {
|
|
135
|
+
url: value,
|
|
136
|
+
suggestedUrl,
|
|
137
|
+
fixHint,
|
|
138
|
+
},
|
|
84
139
|
// No automatic fix for template literals as they may contain dynamic parts
|
|
85
140
|
});
|
|
86
141
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const optimizeObjectBooleanConditions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"extractBooleanCondition", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optimizeObjectBooleanConditions = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
|
+
function isHookCall(node) {
|
|
8
|
+
const callee = node.callee;
|
|
9
|
+
return ((callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
10
|
+
HOOK_NAMES.has(callee.name)) ||
|
|
11
|
+
(callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
12
|
+
!callee.computed &&
|
|
13
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
14
|
+
callee.object.name === 'React' &&
|
|
15
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
16
|
+
HOOK_NAMES.has(callee.property.name)));
|
|
17
|
+
}
|
|
18
|
+
function generateBooleanVariableName(objectName, conditionType, isNegated) {
|
|
19
|
+
const base = capitalize(objectName);
|
|
20
|
+
if (conditionType === 'existence') {
|
|
21
|
+
return isNegated ? `is${base}Missing` : `has${base}`;
|
|
22
|
+
}
|
|
23
|
+
if (conditionType === 'keyCount') {
|
|
24
|
+
return isNegated ? `is${base}Empty` : `has${base}`;
|
|
25
|
+
}
|
|
26
|
+
// complex: default to hasX to avoid over-guessing semantics
|
|
27
|
+
return `has${base}`;
|
|
28
|
+
}
|
|
29
|
+
function capitalize(str) {
|
|
30
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
31
|
+
}
|
|
32
|
+
function isObjectExistenceCheck(node) {
|
|
33
|
+
// Check for patterns like !obj
|
|
34
|
+
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
35
|
+
// !obj
|
|
36
|
+
if (node.operator === '!' &&
|
|
37
|
+
node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// !!obj
|
|
41
|
+
if (node.operator === '!' &&
|
|
42
|
+
node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
43
|
+
node.argument.operator === '!' &&
|
|
44
|
+
node.argument.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isObjectKeyCountCheck(node) {
|
|
51
|
+
// Check for patterns like Object.keys(obj).length === 0 or Object.keys(obj).length > 0
|
|
52
|
+
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
53
|
+
const { left, operator, right } = node;
|
|
54
|
+
// Check if left side is Object.keys(obj).length
|
|
55
|
+
if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
56
|
+
left.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
57
|
+
left.property.name === 'length' &&
|
|
58
|
+
left.object.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
59
|
+
const callExpr = left.object;
|
|
60
|
+
if (callExpr.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
61
|
+
callExpr.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
62
|
+
callExpr.callee.object.name === 'Object' &&
|
|
63
|
+
callExpr.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
64
|
+
callExpr.callee.property.name === 'keys' &&
|
|
65
|
+
callExpr.arguments.length === 1 &&
|
|
66
|
+
callExpr.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
67
|
+
// Check if right side is a number
|
|
68
|
+
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
69
|
+
typeof right.value === 'number' &&
|
|
70
|
+
['===', '!==', '>', '<', '>=', '<='].includes(operator)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function isComplexBooleanExpression(node) {
|
|
79
|
+
// Check for complex expressions involving objects
|
|
80
|
+
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
81
|
+
return (containsObjectCondition(node.left) || containsObjectCondition(node.right));
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function containsObjectCondition(node) {
|
|
86
|
+
return (isObjectExistenceCheck(node) ||
|
|
87
|
+
isObjectKeyCountCheck(node) ||
|
|
88
|
+
isComplexBooleanExpression(node));
|
|
89
|
+
}
|
|
90
|
+
function extractObjectName(node) {
|
|
91
|
+
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
92
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
93
|
+
return node.argument.name;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
97
|
+
const { left } = node;
|
|
98
|
+
if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
99
|
+
left.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
100
|
+
left.object.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
101
|
+
left.object.arguments.length === 1 &&
|
|
102
|
+
left.object.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
103
|
+
return left.object.arguments[0].name;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
107
|
+
// For complex expressions, try to extract from the first object condition found
|
|
108
|
+
const leftObj = extractObjectName(node.left);
|
|
109
|
+
if (leftObj)
|
|
110
|
+
return leftObj;
|
|
111
|
+
return extractObjectName(node.right);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function analyzeBooleanCondition(node, context) {
|
|
116
|
+
const sourceCode = context.sourceCode;
|
|
117
|
+
const expression = sourceCode.getText(node);
|
|
118
|
+
if (isObjectExistenceCheck(node)) {
|
|
119
|
+
const objectName = extractObjectName(node);
|
|
120
|
+
if (objectName) {
|
|
121
|
+
const isNegated = node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
122
|
+
node.operator === '!' &&
|
|
123
|
+
!(node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
124
|
+
node.argument.operator === '!');
|
|
125
|
+
return {
|
|
126
|
+
type: 'existence',
|
|
127
|
+
objectName,
|
|
128
|
+
expression,
|
|
129
|
+
suggestedName: generateBooleanVariableName(objectName, 'existence', isNegated),
|
|
130
|
+
node,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (isObjectKeyCountCheck(node)) {
|
|
135
|
+
const objectName = extractObjectName(node);
|
|
136
|
+
if (objectName) {
|
|
137
|
+
const isNegated = expression.includes('=== 0') || expression.includes('<= 0');
|
|
138
|
+
return {
|
|
139
|
+
type: 'keyCount',
|
|
140
|
+
objectName,
|
|
141
|
+
expression,
|
|
142
|
+
suggestedName: generateBooleanVariableName(objectName, 'keyCount', isNegated),
|
|
143
|
+
node,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (isComplexBooleanExpression(node)) {
|
|
148
|
+
const objectName = extractObjectName(node);
|
|
149
|
+
if (objectName) {
|
|
150
|
+
return {
|
|
151
|
+
type: 'complex',
|
|
152
|
+
objectName,
|
|
153
|
+
expression,
|
|
154
|
+
suggestedName: generateBooleanVariableName(objectName, 'complex', false),
|
|
155
|
+
node,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function findBooleanConditionsInDependencies(depsArray, context) {
|
|
162
|
+
const patterns = [];
|
|
163
|
+
for (const element of depsArray.elements) {
|
|
164
|
+
if (!element)
|
|
165
|
+
continue; // Skip holes in array
|
|
166
|
+
const pattern = analyzeBooleanCondition(element, context);
|
|
167
|
+
if (pattern) {
|
|
168
|
+
patterns.push(pattern);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return patterns;
|
|
172
|
+
}
|
|
173
|
+
function isAlreadyBooleanVariable(node) {
|
|
174
|
+
// Check if the dependency is already a boolean variable (starts with is, has, can, etc.)
|
|
175
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
176
|
+
const name = node.name;
|
|
177
|
+
const booleanPrefixes = [
|
|
178
|
+
'is',
|
|
179
|
+
'has',
|
|
180
|
+
'can',
|
|
181
|
+
'should',
|
|
182
|
+
'will',
|
|
183
|
+
'was',
|
|
184
|
+
'were',
|
|
185
|
+
];
|
|
186
|
+
return booleanPrefixes.some((prefix) => name.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
|
|
191
|
+
name: 'optimize-object-boolean-conditions',
|
|
192
|
+
meta: {
|
|
193
|
+
type: 'suggestion',
|
|
194
|
+
docs: {
|
|
195
|
+
description: 'Detects and suggests optimizations for boolean conditions formed over objects in React hook dependencies. Suggests extracting boolean conditions into separate variables to reduce unnecessary re-computations when objects change frequently but the boolean condition changes less frequently.',
|
|
196
|
+
recommended: 'error',
|
|
197
|
+
},
|
|
198
|
+
schema: [],
|
|
199
|
+
messages: {
|
|
200
|
+
extractBooleanCondition: 'Dependency array includes boolean condition "{{expression}}" derived from object "{{objectName}}". Hook re-runs every time the object reference changes even when the boolean outcome stays the same, leading to wasted renders and unstable memoization. Extract the condition into a stable boolean like "{{suggestedName}}" and depend on that variable instead.',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
defaultOptions: [],
|
|
204
|
+
create(context) {
|
|
205
|
+
return {
|
|
206
|
+
CallExpression(node) {
|
|
207
|
+
if (!isHookCall(node)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Get the dependency array argument (last argument for hooks)
|
|
211
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
212
|
+
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Find boolean conditions in dependencies
|
|
216
|
+
const patterns = findBooleanConditionsInDependencies(depsArg, context);
|
|
217
|
+
for (const pattern of patterns) {
|
|
218
|
+
// Skip if it's already a boolean variable
|
|
219
|
+
if (isAlreadyBooleanVariable(pattern.node)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
context.report({
|
|
223
|
+
node: pattern.node,
|
|
224
|
+
messageId: 'extractBooleanCondition',
|
|
225
|
+
data: {
|
|
226
|
+
expression: pattern.expression,
|
|
227
|
+
suggestedName: pattern.suggestedName,
|
|
228
|
+
objectName: pattern.objectName,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
//# sourceMappingURL=optimize-object-boolean-conditions.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
sideEffectPatterns?: Array<string | RegExp>;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const parallelizeAsyncOperations: TSESLint.RuleModule<"parallelizeAsyncOperations", Options, TSESLint.RuleListener>;
|
|
8
|
+
export {};
|