@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- 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.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- 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 +1 -1
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +44 -18
- 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 +1 -1
- 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.js +7 -3
- 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 +3818 -4644
- 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 +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +66 -23
- 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 +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -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.js +9 -3
- package/lib/rules/no-async-foreach.js +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- 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 +527 -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.js +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -1
- 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 +67 -12
- 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 +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +465 -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.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- 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.js +29 -17
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -2
- 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 +508 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- 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.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- 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 +344 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- 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.js +6 -4
- package/lib/rules/parallelize-async-operations.js +143 -59
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- 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-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- 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.js +88 -13
- 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.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- 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 +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- 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 +155 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +18 -10
- 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 +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- 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 +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -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
|
}
|
|
@@ -6,7 +6,8 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
7
|
function isHookCall(node) {
|
|
8
8
|
const callee = node.callee;
|
|
9
|
-
return ((callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
9
|
+
return ((callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
10
|
+
HOOK_NAMES.has(callee.name)) ||
|
|
10
11
|
(callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
11
12
|
!callee.computed &&
|
|
12
13
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -32,7 +33,8 @@ function isObjectExistenceCheck(node) {
|
|
|
32
33
|
// Check for patterns like !obj
|
|
33
34
|
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
34
35
|
// !obj
|
|
35
|
-
if (node.operator === '!' &&
|
|
36
|
+
if (node.operator === '!' &&
|
|
37
|
+
node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
36
38
|
return true;
|
|
37
39
|
}
|
|
38
40
|
// !!obj
|
|
@@ -111,7 +113,7 @@ function extractObjectName(node) {
|
|
|
111
113
|
return null;
|
|
112
114
|
}
|
|
113
115
|
function analyzeBooleanCondition(node, context) {
|
|
114
|
-
const sourceCode = context.
|
|
116
|
+
const sourceCode = context.sourceCode;
|
|
115
117
|
const expression = sourceCode.getText(node);
|
|
116
118
|
if (isObjectExistenceCheck(node)) {
|
|
117
119
|
const objectName = extractObjectName(node);
|
|
@@ -195,7 +197,7 @@ exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
|
|
|
195
197
|
},
|
|
196
198
|
schema: [],
|
|
197
199
|
messages: {
|
|
198
|
-
extractBooleanCondition: '
|
|
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.',
|
|
199
201
|
},
|
|
200
202
|
},
|
|
201
203
|
defaultOptions: [],
|
|
@@ -13,6 +13,9 @@ const defaultOptions = [
|
|
|
13
13
|
'updatethreshold',
|
|
14
14
|
'setthreshold',
|
|
15
15
|
'checkthreshold',
|
|
16
|
+
'commit',
|
|
17
|
+
'flush',
|
|
18
|
+
'saveall',
|
|
16
19
|
],
|
|
17
20
|
},
|
|
18
21
|
];
|
|
@@ -44,12 +47,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
44
47
|
},
|
|
45
48
|
],
|
|
46
49
|
messages: {
|
|
47
|
-
parallelizeAsyncOperations: '
|
|
50
|
+
parallelizeAsyncOperations: 'Awaiting {{awaitCount}} independent async operations sequentially makes their network and I/O latency add up, which slows responses and wastes compute. These awaits have no data dependency or per-call error handling, so run them together with Promise.all([...]) and destructure the results when you need individual values.',
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
53
|
defaultOptions,
|
|
51
54
|
create(context, [options]) {
|
|
52
|
-
const sourceCode = context.
|
|
55
|
+
const sourceCode = context.sourceCode;
|
|
53
56
|
const sideEffectMatchers = (options?.sideEffectPatterns ?? []).map((pattern) => typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern);
|
|
54
57
|
const reportedRanges = new Set();
|
|
55
58
|
/**
|
|
@@ -95,81 +98,149 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
95
98
|
return null;
|
|
96
99
|
}
|
|
97
100
|
/**
|
|
98
|
-
*
|
|
101
|
+
* Generic AST visitor for identifier collection
|
|
99
102
|
*/
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
function visitIdentifiers(node, callback, options = {}) {
|
|
104
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
105
|
+
const parent = node.parent;
|
|
106
|
+
// Skip non-computed properties in MemberExpressions as they are not value uses
|
|
107
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
108
|
+
if (parent.property === node &&
|
|
109
|
+
!parent.computed &&
|
|
110
|
+
!options.includeMemberProperties) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
107
113
|
}
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
// Skip non-shorthand keys in object literals as they are not value uses
|
|
115
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.Property) {
|
|
116
|
+
if (parent.key === node &&
|
|
117
|
+
!parent.computed &&
|
|
118
|
+
!parent.shorthand) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (callback(node.name) === true)
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Recursively traverses child nodes while skipping 'parent' to avoid
|
|
127
|
+
* circular back-references, and 'range'/'loc' which are metadata.
|
|
128
|
+
*/
|
|
129
|
+
for (const key in node) {
|
|
130
|
+
if (key === 'parent' || key === 'range' || key === 'loc')
|
|
131
|
+
continue;
|
|
132
|
+
const child = node[key];
|
|
133
|
+
if (child && typeof child === 'object') {
|
|
134
|
+
if (Array.isArray(child)) {
|
|
135
|
+
for (const item of child) {
|
|
136
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
137
|
+
if (visitIdentifiers(item, callback, options)) {
|
|
138
|
+
return true;
|
|
118
139
|
}
|
|
119
140
|
}
|
|
120
141
|
}
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
}
|
|
143
|
+
else if ('type' in child) {
|
|
144
|
+
if (visitIdentifiers(child, callback, options)) {
|
|
145
|
+
return true;
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
|
-
|
|
128
|
-
return isUsed;
|
|
150
|
+
return false;
|
|
129
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Checks if an identifier is used in a node
|
|
154
|
+
*/
|
|
155
|
+
function isIdentifierUsedInNode(identifier, node) {
|
|
156
|
+
return visitIdentifiers(node, (name) => name === identifier);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extracts all identifiers used in a node
|
|
160
|
+
*/
|
|
161
|
+
function getAllIdentifiers(node, options) {
|
|
162
|
+
const identifiers = new Set();
|
|
163
|
+
visitIdentifiers(node, (name) => {
|
|
164
|
+
identifiers.add(name);
|
|
165
|
+
}, options);
|
|
166
|
+
return identifiers;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Matches coordinator-like identifiers. Intentionally uses substring
|
|
170
|
+
* matching (no word boundaries) to catch patterns like "batchManager",
|
|
171
|
+
* "transactionCollector", etc. This may produce false positives for
|
|
172
|
+
* unrelated managers, but errs on the side of safety.
|
|
173
|
+
*/
|
|
174
|
+
const COORDINATOR_PATTERN = /batch|manager|collector|transaction|tx|unitofwork|accumulator/i;
|
|
130
175
|
/**
|
|
131
176
|
* Checks if there are dependencies between await expressions
|
|
132
177
|
*/
|
|
133
178
|
function hasDependencies(awaitNodes, variableNames, sideEffectPatterns) {
|
|
134
|
-
// If we have fewer than 2 nodes, there are no dependencies to check
|
|
135
179
|
if (awaitNodes.length < 2) {
|
|
136
180
|
return false;
|
|
137
181
|
}
|
|
138
|
-
|
|
139
|
-
|
|
182
|
+
const allIdentifiers = awaitNodes.map((node) => {
|
|
183
|
+
const awaitExpr = getAwaitExpression(node);
|
|
184
|
+
return awaitExpr
|
|
185
|
+
? getAllIdentifiers(awaitExpr.argument, {
|
|
186
|
+
includeMemberProperties: true,
|
|
187
|
+
})
|
|
188
|
+
: new Set();
|
|
189
|
+
});
|
|
190
|
+
// Check all nodes for side effects and shared coordinators
|
|
191
|
+
for (let i = 0; i < awaitNodes.length; i++) {
|
|
140
192
|
const currentNode = awaitNodes[i];
|
|
141
|
-
// Check if any previous variable is used in the current await expression
|
|
142
|
-
for (const varName of variableNames) {
|
|
143
|
-
const awaitExpr = getAwaitExpression(currentNode);
|
|
144
|
-
if (awaitExpr &&
|
|
145
|
-
isIdentifierUsedInNode(varName, awaitExpr.argument)) {
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// Check for operations that might have side effects that affect subsequent operations
|
|
150
|
-
// This is a conservative heuristic - we only flag very specific patterns
|
|
151
193
|
const awaitExpr = getAwaitExpression(currentNode);
|
|
152
|
-
if (awaitExpr
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const methodName = callee.property.name;
|
|
159
|
-
if (sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
194
|
+
if (!awaitExpr)
|
|
195
|
+
continue;
|
|
196
|
+
// 1. Check if current node depends on variables DECLARED in previous awaits
|
|
197
|
+
if (i > 0) {
|
|
198
|
+
for (const varName of variableNames) {
|
|
199
|
+
if (isIdentifierUsedInNode(varName, awaitExpr.argument)) {
|
|
160
200
|
return true;
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
}
|
|
204
|
+
// 2. Check for shared coordinators between this node and ANY previous node
|
|
205
|
+
if (i > 0) {
|
|
206
|
+
const currentIds = allIdentifiers[i];
|
|
207
|
+
for (const id of currentIds) {
|
|
208
|
+
if (COORDINATOR_PATTERN.test(id)) {
|
|
209
|
+
for (let j = 0; j < i; j++) {
|
|
210
|
+
if (allIdentifiers[j].has(id)) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
167
214
|
}
|
|
168
215
|
}
|
|
169
216
|
}
|
|
217
|
+
// 3. Check for operations that might have side effects
|
|
218
|
+
// If any node has a side effect, we should not parallelize the sequence
|
|
219
|
+
let callExpr = null;
|
|
220
|
+
if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
221
|
+
callExpr = awaitExpr.argument;
|
|
222
|
+
}
|
|
223
|
+
else if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
224
|
+
awaitExpr.argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
225
|
+
callExpr = awaitExpr.argument.expression;
|
|
226
|
+
}
|
|
227
|
+
if (callExpr) {
|
|
228
|
+
const callee = callExpr.callee;
|
|
229
|
+
let methodName = null;
|
|
230
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
231
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
232
|
+
methodName = callee.property.name;
|
|
233
|
+
}
|
|
234
|
+
else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
235
|
+
methodName = callee.name;
|
|
236
|
+
}
|
|
237
|
+
if (methodName &&
|
|
238
|
+
sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
170
242
|
}
|
|
171
243
|
// If any node is a variable declaration with destructuring, consider it as having dependencies
|
|
172
|
-
// This is because destructuring often creates variables that are used later
|
|
173
244
|
for (const node of awaitNodes) {
|
|
174
245
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
175
246
|
for (const declaration of node.declarations) {
|
|
@@ -234,7 +305,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
234
305
|
function areInTryCatchBlocks(nodes) {
|
|
235
306
|
for (const node of nodes) {
|
|
236
307
|
let current = node;
|
|
237
|
-
// Traverse up to find if the node is in a try block
|
|
238
308
|
while (current && current.parent) {
|
|
239
309
|
if (current.parent.type === utils_1.AST_NODE_TYPES.TryStatement &&
|
|
240
310
|
current.parent.block === current) {
|
|
@@ -253,7 +323,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
253
323
|
function areInLoop(nodes) {
|
|
254
324
|
for (const node of nodes) {
|
|
255
325
|
let current = node;
|
|
256
|
-
// Traverse up to find if the node is in a loop
|
|
257
326
|
while (current && current.parent) {
|
|
258
327
|
if (current.parent.type === utils_1.AST_NODE_TYPES.ForStatement ||
|
|
259
328
|
current.parent.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
@@ -269,16 +338,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
269
338
|
}
|
|
270
339
|
/**
|
|
271
340
|
* Generates a fix for sequential awaits
|
|
341
|
+
*
|
|
342
|
+
* Returns null when the sequential awaits cannot be safely rewritten as a Promise.all.
|
|
272
343
|
*/
|
|
273
344
|
function generateFix(fixer, awaitNodes) {
|
|
274
|
-
|
|
345
|
+
if (awaitNodes.length < 2) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
275
348
|
const awaitExpressions = awaitNodes
|
|
276
349
|
.map((node) => getAwaitExpression(node))
|
|
277
350
|
.filter((node) => node !== null);
|
|
278
351
|
if (awaitExpressions.length < 2) {
|
|
279
352
|
return null;
|
|
280
353
|
}
|
|
281
|
-
// Get the text of each await argument
|
|
282
354
|
const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
|
|
283
355
|
const idsText = [];
|
|
284
356
|
const declKinds = new Set();
|
|
@@ -311,12 +383,18 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
311
383
|
// Simple Promise.all without variable assignments
|
|
312
384
|
promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
|
|
313
385
|
}
|
|
314
|
-
|
|
315
|
-
let startPos = awaitNodes[0].range[0];
|
|
316
|
-
// Replace the range from the start of the first await to the end of the last await
|
|
386
|
+
const startPos = awaitNodes[0].range[0];
|
|
317
387
|
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
318
388
|
return fixer.replaceTextRange([startPos, endPos], promiseAllText);
|
|
319
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Generates a deduplication key from await nodes' range metadata.
|
|
392
|
+
*/
|
|
393
|
+
function getDeduplicationKey(awaitNodes) {
|
|
394
|
+
const rangeStart = awaitNodes[0].range[0];
|
|
395
|
+
const rangeEnd = awaitNodes[awaitNodes.length - 1].range[1];
|
|
396
|
+
return `${rangeStart}-${rangeEnd}`;
|
|
397
|
+
}
|
|
320
398
|
const processStatementList = (statements) => {
|
|
321
399
|
const awaitNodes = [];
|
|
322
400
|
for (const statement of statements) {
|
|
@@ -329,12 +407,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
329
407
|
if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
|
|
330
408
|
!areInTryCatchBlocks(awaitNodes) &&
|
|
331
409
|
!areInLoop(awaitNodes)) {
|
|
332
|
-
const key =
|
|
410
|
+
const key = getDeduplicationKey(awaitNodes);
|
|
333
411
|
if (!reportedRanges.has(key)) {
|
|
334
412
|
reportedRanges.add(key);
|
|
335
413
|
context.report({
|
|
336
414
|
node: awaitNodes[0],
|
|
337
415
|
messageId: 'parallelizeAsyncOperations',
|
|
416
|
+
data: {
|
|
417
|
+
awaitCount: awaitNodes.length.toString(),
|
|
418
|
+
},
|
|
338
419
|
fix: (fixer) => generateFix(fixer, awaitNodes),
|
|
339
420
|
});
|
|
340
421
|
}
|
|
@@ -350,12 +431,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
350
431
|
if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
|
|
351
432
|
!areInTryCatchBlocks(awaitNodes) &&
|
|
352
433
|
!areInLoop(awaitNodes)) {
|
|
353
|
-
const key =
|
|
434
|
+
const key = getDeduplicationKey(awaitNodes);
|
|
354
435
|
if (!reportedRanges.has(key)) {
|
|
355
436
|
reportedRanges.add(key);
|
|
356
437
|
context.report({
|
|
357
438
|
node: awaitNodes[0],
|
|
358
439
|
messageId: 'parallelizeAsyncOperations',
|
|
440
|
+
data: {
|
|
441
|
+
awaitCount: awaitNodes.length.toString(),
|
|
442
|
+
},
|
|
359
443
|
fix: (fixer) => generateFix(fixer, awaitNodes),
|
|
360
444
|
});
|
|
361
445
|
}
|
|
@@ -1,3 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const preferBatchOperations: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
|
-
export {};
|
|
1
|
+
export declare const preferBatchOperations: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferBatch", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|