@blumintinc/eslint-plugin-blumint 1.13.0 → 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 +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- 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.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- 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-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- 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-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 +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- 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 +2 -2
- 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 +5 -4
- 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 +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/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 +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 +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 +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.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -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 +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.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.js +38 -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 +459 -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 +347 -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 +24 -5
- 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 +9 -4
- 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 +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- 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-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.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 +136 -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 +1 -1
- 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 +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- 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: [],
|
|
@@ -44,12 +44,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
44
44
|
},
|
|
45
45
|
],
|
|
46
46
|
messages: {
|
|
47
|
-
parallelizeAsyncOperations: '
|
|
47
|
+
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
48
|
},
|
|
49
49
|
},
|
|
50
50
|
defaultOptions,
|
|
51
51
|
create(context, [options]) {
|
|
52
|
-
const sourceCode = context.
|
|
52
|
+
const sourceCode = context.sourceCode;
|
|
53
53
|
const sideEffectMatchers = (options?.sideEffectPatterns ?? []).map((pattern) => typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern);
|
|
54
54
|
const reportedRanges = new Set();
|
|
55
55
|
/**
|
|
@@ -269,8 +269,13 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
269
269
|
}
|
|
270
270
|
/**
|
|
271
271
|
* Generates a fix for sequential awaits
|
|
272
|
+
*
|
|
273
|
+
* Returns null when the sequential awaits cannot be safely rewritten as a Promise.all.
|
|
272
274
|
*/
|
|
273
275
|
function generateFix(fixer, awaitNodes) {
|
|
276
|
+
if (awaitNodes.length < 2) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
274
279
|
// Extract the await expressions
|
|
275
280
|
const awaitExpressions = awaitNodes
|
|
276
281
|
.map((node) => getAwaitExpression(node))
|
|
@@ -312,11 +317,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
312
317
|
promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
|
|
313
318
|
}
|
|
314
319
|
// Find the start position, accounting for leading comments
|
|
315
|
-
|
|
320
|
+
const startPos = awaitNodes[0].range[0];
|
|
316
321
|
// Replace the range from the start of the first await to the end of the last await
|
|
317
322
|
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
318
323
|
return fixer.replaceTextRange([startPos, endPos], promiseAllText);
|
|
319
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Generates a deduplication key from await nodes' range metadata.
|
|
327
|
+
*/
|
|
328
|
+
function getDeduplicationKey(awaitNodes) {
|
|
329
|
+
const rangeStart = awaitNodes[0].range[0];
|
|
330
|
+
const rangeEnd = awaitNodes[awaitNodes.length - 1].range[1];
|
|
331
|
+
return `${rangeStart}-${rangeEnd}`;
|
|
332
|
+
}
|
|
320
333
|
const processStatementList = (statements) => {
|
|
321
334
|
const awaitNodes = [];
|
|
322
335
|
for (const statement of statements) {
|
|
@@ -329,12 +342,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
329
342
|
if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
|
|
330
343
|
!areInTryCatchBlocks(awaitNodes) &&
|
|
331
344
|
!areInLoop(awaitNodes)) {
|
|
332
|
-
const key =
|
|
345
|
+
const key = getDeduplicationKey(awaitNodes);
|
|
333
346
|
if (!reportedRanges.has(key)) {
|
|
334
347
|
reportedRanges.add(key);
|
|
335
348
|
context.report({
|
|
336
349
|
node: awaitNodes[0],
|
|
337
350
|
messageId: 'parallelizeAsyncOperations',
|
|
351
|
+
data: {
|
|
352
|
+
awaitCount: awaitNodes.length.toString(),
|
|
353
|
+
},
|
|
338
354
|
fix: (fixer) => generateFix(fixer, awaitNodes),
|
|
339
355
|
});
|
|
340
356
|
}
|
|
@@ -350,12 +366,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
350
366
|
if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
|
|
351
367
|
!areInTryCatchBlocks(awaitNodes) &&
|
|
352
368
|
!areInLoop(awaitNodes)) {
|
|
353
|
-
const key =
|
|
369
|
+
const key = getDeduplicationKey(awaitNodes);
|
|
354
370
|
if (!reportedRanges.has(key)) {
|
|
355
371
|
reportedRanges.add(key);
|
|
356
372
|
context.report({
|
|
357
373
|
node: awaitNodes[0],
|
|
358
374
|
messageId: 'parallelizeAsyncOperations',
|
|
375
|
+
data: {
|
|
376
|
+
awaitCount: awaitNodes.length.toString(),
|
|
377
|
+
},
|
|
359
378
|
fix: (fixer) => generateFix(fixer, awaitNodes),
|
|
360
379
|
});
|
|
361
380
|
}
|
|
@@ -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>;
|
|
@@ -224,6 +224,27 @@ function findLoopNode(node) {
|
|
|
224
224
|
}
|
|
225
225
|
return undefined;
|
|
226
226
|
}
|
|
227
|
+
function describeLoopContext(loopInfo) {
|
|
228
|
+
if (loopInfo.isPromiseAll)
|
|
229
|
+
return 'Promise.all()';
|
|
230
|
+
if (loopInfo.isArrayMethod) {
|
|
231
|
+
return `${loopInfo.isArrayMethod}() callback`;
|
|
232
|
+
}
|
|
233
|
+
switch (loopInfo.node.type) {
|
|
234
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
235
|
+
return 'for loop';
|
|
236
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
237
|
+
return 'for...in loop';
|
|
238
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
239
|
+
return 'for...of loop';
|
|
240
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
241
|
+
return 'while loop';
|
|
242
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
243
|
+
return 'do...while loop';
|
|
244
|
+
default:
|
|
245
|
+
return 'this control flow';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
227
248
|
function isFirestoreSetterInstance(node) {
|
|
228
249
|
// Check if it's a DocSetter instance
|
|
229
250
|
if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
@@ -301,8 +322,7 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
301
322
|
fixable: 'code',
|
|
302
323
|
schema: [],
|
|
303
324
|
messages: {
|
|
304
|
-
|
|
305
|
-
preferOverwriteAll: 'Use overwriteAll() instead of multiple overwrite() calls for better performance',
|
|
325
|
+
preferBatch: 'DocSetter.{{setterMethod}} is invoked repeatedly inside {{contextDescription}}, which issues separate Firestore writes per document and can leave partial updates when later calls fail. Batch the documents with DocSetter.{{batchMethod}} so the writes stay grouped and latency stays predictable.',
|
|
306
326
|
},
|
|
307
327
|
},
|
|
308
328
|
defaultOptions: [],
|
|
@@ -321,6 +341,13 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
321
341
|
const loopInfo = findLoopNode(node);
|
|
322
342
|
if (!loopInfo)
|
|
323
343
|
return;
|
|
344
|
+
const batchMethod = methodName === 'set' ? 'setAll()' : 'overwriteAll()';
|
|
345
|
+
const messageData = {
|
|
346
|
+
setterMethod: `${methodName}()`,
|
|
347
|
+
batchMethod,
|
|
348
|
+
contextDescription: describeLoopContext(loopInfo),
|
|
349
|
+
};
|
|
350
|
+
const messageId = 'preferBatch';
|
|
324
351
|
// Get or create the setter calls map for this loop
|
|
325
352
|
let setterCalls = loopSetterCalls.get(loopInfo.node);
|
|
326
353
|
if (!setterCalls) {
|
|
@@ -348,10 +375,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
348
375
|
existing.count === 2 &&
|
|
349
376
|
!reportedLoops.has(loopInfo.node)) {
|
|
350
377
|
reportedLoops.add(loopInfo.node);
|
|
351
|
-
const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
|
|
352
378
|
context.report({
|
|
353
379
|
node: existing.firstNode,
|
|
354
380
|
messageId,
|
|
381
|
+
data: messageData,
|
|
355
382
|
fix: () => null, // We can't provide a fix because we don't know the array structure
|
|
356
383
|
});
|
|
357
384
|
}
|
|
@@ -371,10 +398,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
371
398
|
// For traditional loops, report on the first occurrence since we know it's in a loop
|
|
372
399
|
if (!reportedLoops.has(loopInfo.node)) {
|
|
373
400
|
reportedLoops.add(loopInfo.node);
|
|
374
|
-
const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
|
|
375
401
|
context.report({
|
|
376
402
|
node,
|
|
377
403
|
messageId,
|
|
404
|
+
data: messageData,
|
|
378
405
|
fix: () => null, // We can't provide a fix because we don't know the array structure
|
|
379
406
|
});
|
|
380
407
|
}
|
|
@@ -383,10 +410,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
383
410
|
else if (loopInfo.isArrayMethod) {
|
|
384
411
|
if (!reportedLoops.has(loopInfo.node)) {
|
|
385
412
|
reportedLoops.add(loopInfo.node);
|
|
386
|
-
const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
|
|
387
413
|
context.report({
|
|
388
414
|
node,
|
|
389
415
|
messageId,
|
|
416
|
+
data: messageData,
|
|
390
417
|
fix: () => null, // We can't provide a fix because we don't know the array structure
|
|
391
418
|
});
|
|
392
419
|
}
|
|
@@ -65,17 +65,22 @@ exports.preferBlockCommentsForDeclarations = (0, createRule_1.createRule)({
|
|
|
65
65
|
if (isInsideFunctionBody(node)) {
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
const sourceCode = context.
|
|
68
|
+
const sourceCode = context.sourceCode;
|
|
69
69
|
const comments = sourceCode.getCommentsBefore(node);
|
|
70
70
|
// Find the closest comment to the node
|
|
71
71
|
const lastComment = comments[comments.length - 1];
|
|
72
72
|
if (lastComment && isLineCommentBeforeDeclaration(lastComment, node)) {
|
|
73
|
+
const commentText = lastComment.value.trim();
|
|
74
|
+
const commentLabel = commentText || 'declaration comment';
|
|
73
75
|
context.report({
|
|
74
76
|
loc: lastComment.loc,
|
|
75
77
|
messageId: 'preferBlockComment',
|
|
78
|
+
data: { commentText: commentLabel },
|
|
76
79
|
fix: (fixer) => {
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
if (commentLabel.includes('*/')) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return fixer.replaceText(lastComment, `/** ${commentLabel} */`);
|
|
79
84
|
},
|
|
80
85
|
});
|
|
81
86
|
}
|
|
@@ -129,7 +134,7 @@ exports.preferBlockCommentsForDeclarations = (0, createRule_1.createRule)({
|
|
|
129
134
|
fixable: 'code',
|
|
130
135
|
schema: [],
|
|
131
136
|
messages: {
|
|
132
|
-
preferBlockComment: '
|
|
137
|
+
preferBlockComment: 'Line comment "{{commentText}}" sits on a declaration, and IDE hovers and TypeScript docs ignore it as documentation. Rewrite it as a block comment (/** ... */) so the text stays attached to the declaration and tooling surfaces it.',
|
|
133
138
|
},
|
|
134
139
|
},
|
|
135
140
|
defaultOptions: [],
|
|
@@ -14,7 +14,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
preferCloneDeep: 'Use cloneDeep from functions/src/util/cloneDeep.ts
|
|
17
|
+
preferCloneDeep: 'Nested spread copies only clone one level, so inner objects still point at the original and later mutations leak back. Use cloneDeep from functions/src/util/cloneDeep.ts and pass overrides as the second argument so the base object is deeply cloned before applying updates.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -74,7 +74,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
74
74
|
!hasSymbol);
|
|
75
75
|
}
|
|
76
76
|
function generateCloneDeepFix(node) {
|
|
77
|
-
const sourceCode = context.
|
|
77
|
+
const sourceCode = context.sourceCode;
|
|
78
78
|
// Find the base object (first spread element)
|
|
79
79
|
let baseObj = null;
|
|
80
80
|
// Extract the base object from the first spread element
|
|
@@ -248,7 +248,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
248
248
|
}
|
|
249
249
|
// Helper function to extract nested overrides without spread elements
|
|
250
250
|
function extractNestedOverrides(node) {
|
|
251
|
-
const sourceCode = context.
|
|
251
|
+
const sourceCode = context.sourceCode;
|
|
252
252
|
const overrides = [];
|
|
253
253
|
for (const prop of node.properties) {
|
|
254
254
|
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [
|
|
2
3
|
{
|
|
3
4
|
object?: boolean;
|
|
4
5
|
enforceForRenamedProperties?: boolean;
|
|
5
6
|
}
|
|
6
7
|
];
|
|
7
|
-
export declare const preferDestructuringNoClass:
|
|
8
|
+
export declare const preferDestructuringNoClass: TSESLint.RuleModule<"preferDestructuring", Options, TSESLint.RuleListener>;
|
|
8
9
|
export {};
|