@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
|
@@ -1,22 +1,220 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.noAsyncForEach = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const getNodeStart = (node) => node?.range?.[0] ?? Number.POSITIVE_INFINITY;
|
|
6
|
+
const getFunctionDescription = (node, fallbackName) => {
|
|
7
|
+
const declaredName = (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
8
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
9
|
+
node.id?.name
|
|
10
|
+
? node.id.name
|
|
11
|
+
: null;
|
|
12
|
+
const isArrowFunction = node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
13
|
+
const functionName = declaredName ?? (isArrowFunction ? undefined : fallbackName);
|
|
14
|
+
if (functionName) {
|
|
15
|
+
return `function "${functionName}"`;
|
|
16
|
+
}
|
|
17
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
18
|
+
return fallbackName ? `arrow function "${fallbackName}"` : 'arrow function';
|
|
19
|
+
}
|
|
20
|
+
return 'function expression';
|
|
21
|
+
};
|
|
22
|
+
const findVariableInScope = (scope, name) => {
|
|
23
|
+
let currentScope = scope;
|
|
24
|
+
while (currentScope) {
|
|
25
|
+
const variable = currentScope.set.get(name);
|
|
26
|
+
if (variable) {
|
|
27
|
+
return variable;
|
|
28
|
+
}
|
|
29
|
+
currentScope = currentScope.upper;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Function declarations are hoisted across their scope regardless of source order.
|
|
35
|
+
* Returning NEGATIVE_INFINITY makes hoisted declarations always "earlier" than any
|
|
36
|
+
* callback usage when we compare source positions.
|
|
37
|
+
*/
|
|
38
|
+
const getDefinitionStart = (definition) => definition.node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration
|
|
39
|
+
? Number.NEGATIVE_INFINITY
|
|
40
|
+
: getNodeStart(definition.node);
|
|
41
|
+
const getReferenceStart = (reference) => getNodeStart(reference.identifier);
|
|
42
|
+
const isWriteReference = (reference) => typeof reference.isWrite === 'function'
|
|
43
|
+
? reference.isWrite()
|
|
44
|
+
: Boolean(reference.isWrite);
|
|
45
|
+
const isAsyncFunctionExpression = (node) => {
|
|
46
|
+
if (!node || typeof node !== 'object') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const typedNode = node;
|
|
50
|
+
return ((typedNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
51
|
+
typedNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
52
|
+
typedNode.async === true);
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Checks if context has direct sourceCode property
|
|
56
|
+
*/
|
|
57
|
+
const hasSourceCodeProperty = (context) => {
|
|
58
|
+
return 'sourceCode' in context && !!context.sourceCode;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Checks if context has getSourceCode method
|
|
62
|
+
*/
|
|
63
|
+
const hasGetSourceCodeMethod = (context) => {
|
|
64
|
+
return ('getSourceCode' in context && typeof context.getSourceCode === 'function');
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves source code from an ESLint rule context
|
|
68
|
+
*/
|
|
69
|
+
const getSourceCode = (context) => {
|
|
70
|
+
if (hasSourceCodeProperty(context)) {
|
|
71
|
+
return context.sourceCode;
|
|
72
|
+
}
|
|
73
|
+
if (hasGetSourceCodeMethod(context)) {
|
|
74
|
+
return context.getSourceCode();
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Unable to retrieve source code from context in rule "no-async-foreach". ` +
|
|
77
|
+
`File: ${context.filename ??
|
|
78
|
+
context.getFilename?.() ??
|
|
79
|
+
'unknown'}. ` +
|
|
80
|
+
`Available properties: sourceCode=${typeof context
|
|
81
|
+
.sourceCode}, ` +
|
|
82
|
+
`getSourceCode=${typeof context.getSourceCode}.`);
|
|
83
|
+
};
|
|
84
|
+
const getScope = (context, sourceCode, node) => {
|
|
85
|
+
const typedSourceCode = sourceCode;
|
|
86
|
+
return typedSourceCode.getScope?.(node) ?? context.getScope?.() ?? null;
|
|
87
|
+
};
|
|
88
|
+
const analyzeInlineCallback = (callback) => {
|
|
89
|
+
if (!callback.async) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
callbackLabel: getFunctionDescription(callback),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const getAsyncFunctionDeclarationInfo = (definition, callbackName) => {
|
|
97
|
+
if (definition.node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
98
|
+
definition.node.async) {
|
|
99
|
+
return {
|
|
100
|
+
callbackLabel: getFunctionDescription(definition.node, definition.node.id?.name ?? callbackName),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
105
|
+
const getAsyncVariableDeclaratorInfo = (definition, callbackName) => {
|
|
106
|
+
if (definition.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const initializerExpression = definition.node.init;
|
|
110
|
+
if (!isAsyncFunctionExpression(initializerExpression)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const name = (definition.node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
114
|
+
definition.node.id.name) ||
|
|
115
|
+
callbackName;
|
|
116
|
+
return {
|
|
117
|
+
callbackLabel: getFunctionDescription(initializerExpression, name),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const analyzeVariableDefinition = (definition, callbackName) => getAsyncFunctionDeclarationInfo(definition, callbackName) ??
|
|
121
|
+
getAsyncVariableDeclaratorInfo(definition, callbackName);
|
|
122
|
+
const getReferenceWriteExpression = (reference) => {
|
|
123
|
+
const parent = reference.identifier.parent;
|
|
124
|
+
if (reference.writeExpr) {
|
|
125
|
+
return reference.writeExpr;
|
|
126
|
+
}
|
|
127
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
128
|
+
return parent.right;
|
|
129
|
+
}
|
|
130
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator && parent.init) {
|
|
131
|
+
return parent.init;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
135
|
+
const analyzeVariableReference = (reference) => {
|
|
136
|
+
if (!isWriteReference(reference)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const writeExpr = getReferenceWriteExpression(reference);
|
|
140
|
+
if (!isAsyncFunctionExpression(writeExpr)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const name = (writeExpr.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
144
|
+
writeExpr.id?.name) ||
|
|
145
|
+
reference.identifier.name;
|
|
146
|
+
return {
|
|
147
|
+
callbackLabel: getFunctionDescription(writeExpr, name),
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const analyzeCallbackAsyncStatus = (callback, scope) => {
|
|
151
|
+
if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
152
|
+
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
153
|
+
return analyzeInlineCallback(callback);
|
|
154
|
+
}
|
|
155
|
+
if (callback.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const variable = findVariableInScope(scope, callback.name);
|
|
159
|
+
if (!variable) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Track every write to the callback identifier with its source position. Only
|
|
164
|
+
* the last write at or before the callback location determines whether the
|
|
165
|
+
* callback is async when it is passed to forEach. If the callback location is
|
|
166
|
+
* unknown, bail out to avoid blaming writes that might occur after the call
|
|
167
|
+
* (e.g., a later reassignment to async that should not retroactively flag an
|
|
168
|
+
* earlier forEach use).
|
|
169
|
+
*/
|
|
170
|
+
const callbackStart = callback.range?.[0];
|
|
171
|
+
const writes = [];
|
|
172
|
+
for (const definition of variable.defs) {
|
|
173
|
+
const definitionResult = analyzeVariableDefinition(definition, callback.name);
|
|
174
|
+
writes.push({
|
|
175
|
+
start: getDefinitionStart(definition),
|
|
176
|
+
isAsync: Boolean(definitionResult),
|
|
177
|
+
info: definitionResult,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const reference of variable.references) {
|
|
181
|
+
if (!isWriteReference(reference)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const referenceResult = analyzeVariableReference(reference);
|
|
185
|
+
writes.push({
|
|
186
|
+
start: getReferenceStart(reference),
|
|
187
|
+
isAsync: Boolean(referenceResult),
|
|
188
|
+
info: referenceResult,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const relevantWrites = writes.filter(({ start }) => typeof callbackStart === 'number' && start <= callbackStart);
|
|
192
|
+
if (!relevantWrites.length) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const lastWrite = relevantWrites.reduce((latest, current) => !latest || current.start > latest.start ? current : latest, null);
|
|
196
|
+
return lastWrite && lastWrite.isAsync ? lastWrite.info : null;
|
|
197
|
+
};
|
|
4
198
|
exports.noAsyncForEach = {
|
|
5
199
|
create(context) {
|
|
200
|
+
const sourceCode = getSourceCode(context);
|
|
6
201
|
return {
|
|
7
202
|
CallExpression(node) {
|
|
8
203
|
const callee = node.callee;
|
|
9
|
-
|
|
10
|
-
|
|
204
|
+
const callback = node.arguments[0];
|
|
205
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
206
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
11
207
|
callee.property.name === 'forEach' &&
|
|
12
|
-
|
|
13
|
-
(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
208
|
+
callback) {
|
|
209
|
+
const scope = getScope(context, sourceCode, callback);
|
|
210
|
+
const asyncCallbackInfo = analyzeCallbackAsyncStatus(callback, scope);
|
|
211
|
+
if (asyncCallbackInfo) {
|
|
212
|
+
context.report({
|
|
213
|
+
node: callback,
|
|
214
|
+
messageId: 'noAsyncForEach',
|
|
215
|
+
data: asyncCallbackInfo,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
20
218
|
}
|
|
21
219
|
},
|
|
22
220
|
};
|
|
@@ -24,11 +222,11 @@ exports.noAsyncForEach = {
|
|
|
24
222
|
meta: {
|
|
25
223
|
type: 'problem',
|
|
26
224
|
docs: {
|
|
27
|
-
description: 'Disallow
|
|
225
|
+
description: 'Disallow async callbacks to Array.forEach',
|
|
28
226
|
recommended: 'error',
|
|
29
227
|
},
|
|
30
228
|
messages: {
|
|
31
|
-
noAsyncForEach: '
|
|
229
|
+
noAsyncForEach: 'Async {{callbackLabel}} passed to Array.forEach runs without awaiting each item. Array.forEach ignores returned promises, so async work executes in parallel and rejections go unhandled. Use a for...of loop to await sequentially or map with Promise.all when you want controlled concurrency.',
|
|
32
230
|
},
|
|
33
231
|
schema: [],
|
|
34
232
|
},
|
|
@@ -16,11 +16,21 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
16
16
|
},
|
|
17
17
|
schema: [],
|
|
18
18
|
messages: {
|
|
19
|
-
circularReference: '
|
|
19
|
+
circularReference: 'Reference "{{referenceText}}" makes this object point back to itself (directly or through other objects). Circular object graphs throw in `JSON.stringify()` and keep members reachable longer, which causes memory leaks and unexpected mutations. Store a copy or a serialize-safe identifier instead of the original object when assigning.',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
|
+
const sourceCode = context.getSourceCode();
|
|
25
|
+
function reportCircularReference(node, reference) {
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
messageId: 'circularReference',
|
|
29
|
+
data: {
|
|
30
|
+
referenceText: sourceCode.getText(reference),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
24
34
|
function isObjectExpression(node) {
|
|
25
35
|
return node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
|
|
26
36
|
}
|
|
@@ -207,10 +217,7 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
207
217
|
if (targetInfo) {
|
|
208
218
|
targetInfo.references.add(reference);
|
|
209
219
|
if (detectCircularReference(targetObj)) {
|
|
210
|
-
|
|
211
|
-
node: reference,
|
|
212
|
-
messageId: 'circularReference',
|
|
213
|
-
});
|
|
220
|
+
reportCircularReference(reference, reference);
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
}
|
|
@@ -502,10 +509,8 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
502
509
|
if (isIdentifier(leftObj)) {
|
|
503
510
|
const leftObjRef = getReferencedObject(leftObj);
|
|
504
511
|
if (leftObjRef === referencedObj) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
messageId: 'circularReference',
|
|
508
|
-
});
|
|
512
|
+
const reference = assignment.right;
|
|
513
|
+
reportCircularReference(assignment, reference);
|
|
509
514
|
}
|
|
510
515
|
}
|
|
511
516
|
}
|
|
@@ -523,10 +528,8 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
523
528
|
if (isIdentifier(leftObj)) {
|
|
524
529
|
const leftObjRef = getReferencedObject(leftObj);
|
|
525
530
|
if (leftObjRef === referencedObj) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
messageId: 'circularReference',
|
|
529
|
-
});
|
|
531
|
+
const reference = assignment.right;
|
|
532
|
+
reportCircularReference(assignment, reference);
|
|
530
533
|
}
|
|
531
534
|
}
|
|
532
535
|
}
|
|
@@ -568,10 +571,8 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
568
571
|
parent.right === node) {
|
|
569
572
|
const leftObj = getObjectFromMemberExpression(parent.left);
|
|
570
573
|
if (leftObj === referencedObj) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
messageId: 'circularReference',
|
|
574
|
-
});
|
|
574
|
+
const reference = node;
|
|
575
|
+
reportCircularReference(node, reference);
|
|
575
576
|
}
|
|
576
577
|
}
|
|
577
578
|
}
|
|
@@ -14,11 +14,55 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
noClassInstanceDestructuring:
|
|
17
|
+
noClassInstanceDestructuring: [
|
|
18
|
+
"What's wrong: Destructuring {{members}} from class instance {{instance}} detaches those members from the instance.",
|
|
19
|
+
'Why it matters: Methods can run with the wrong `this`, and getters become one-time snapshots that go stale when the instance changes.',
|
|
20
|
+
'How to fix: Access through the instance instead (for example, {{suggestion}}) and bind when you need to pass a method around.',
|
|
21
|
+
].join('\n'),
|
|
18
22
|
},
|
|
19
23
|
},
|
|
20
24
|
defaultOptions: [],
|
|
21
25
|
create(context) {
|
|
26
|
+
const sourceCode = context.getSourceCode();
|
|
27
|
+
function describeMember(prop) {
|
|
28
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
29
|
+
if (prop.computed) {
|
|
30
|
+
const keyText = sourceCode.getText(prop.key);
|
|
31
|
+
return `[${keyText}]`;
|
|
32
|
+
}
|
|
33
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
34
|
+
return prop.key.name;
|
|
35
|
+
}
|
|
36
|
+
return sourceCode.getText(prop.key);
|
|
37
|
+
}
|
|
38
|
+
if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
39
|
+
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return `...${prop.argument.name}`;
|
|
41
|
+
}
|
|
42
|
+
return 'member';
|
|
43
|
+
}
|
|
44
|
+
function buildAccessPath(initText, prop) {
|
|
45
|
+
const keyText = sourceCode.getText(prop.key);
|
|
46
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier && !prop.computed) {
|
|
47
|
+
return `${initText}.${keyText}`;
|
|
48
|
+
}
|
|
49
|
+
return `${initText}[${keyText}]`;
|
|
50
|
+
}
|
|
51
|
+
function formatMembers(properties) {
|
|
52
|
+
const memberNames = properties.map(describeMember).filter(Boolean);
|
|
53
|
+
if (memberNames.length === 0)
|
|
54
|
+
return '`<members>`';
|
|
55
|
+
return memberNames.map((name) => `\`${name}\``).join(', ');
|
|
56
|
+
}
|
|
57
|
+
function formatAccessExamples(properties, initText) {
|
|
58
|
+
const accessPaths = properties
|
|
59
|
+
.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
|
|
60
|
+
.map((prop) => buildAccessPath(initText, prop));
|
|
61
|
+
if (accessPaths.length === 0) {
|
|
62
|
+
return `\`${initText}.<member>\``;
|
|
63
|
+
}
|
|
64
|
+
return accessPaths.map((path) => `\`${path}\``).join(', ');
|
|
65
|
+
}
|
|
22
66
|
function isClassInstance(node) {
|
|
23
67
|
// Check for new expressions
|
|
24
68
|
if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
@@ -43,11 +87,16 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
43
87
|
node.init &&
|
|
44
88
|
isClassInstance(node.init)) {
|
|
45
89
|
const objectPattern = node.id;
|
|
90
|
+
const initText = sourceCode.getText(node.init);
|
|
46
91
|
context.report({
|
|
47
92
|
node,
|
|
48
93
|
messageId: 'noClassInstanceDestructuring',
|
|
94
|
+
data: {
|
|
95
|
+
members: formatMembers(objectPattern.properties),
|
|
96
|
+
instance: `\`${initText}\``,
|
|
97
|
+
suggestion: formatAccessExamples(objectPattern.properties, initText),
|
|
98
|
+
},
|
|
49
99
|
fix(fixer) {
|
|
50
|
-
const sourceCode = context.getSourceCode();
|
|
51
100
|
const properties = objectPattern.properties;
|
|
52
101
|
// Skip if there's no init expression
|
|
53
102
|
if (!node.init)
|
|
@@ -56,14 +105,11 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
56
105
|
if (properties.length === 1) {
|
|
57
106
|
const prop = properties[0];
|
|
58
107
|
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
59
|
-
const key = prop.key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
60
|
-
? prop.key.name
|
|
61
|
-
: sourceCode.getText(prop.key);
|
|
62
108
|
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
63
109
|
? prop.value.name
|
|
64
110
|
: sourceCode.getText(prop.value);
|
|
65
|
-
const
|
|
66
|
-
return fixer.replaceText(node, `${value} = ${
|
|
111
|
+
const accessPath = buildAccessPath(initText, prop);
|
|
112
|
+
return fixer.replaceText(node, `${value} = ${accessPath}`);
|
|
67
113
|
}
|
|
68
114
|
return null;
|
|
69
115
|
}
|
|
@@ -71,14 +117,11 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
71
117
|
const declarations = properties
|
|
72
118
|
.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
|
|
73
119
|
.map((prop) => {
|
|
74
|
-
const key = prop.key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
75
|
-
? prop.key.name
|
|
76
|
-
: sourceCode.getText(prop.key);
|
|
77
120
|
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
78
121
|
? prop.value.name
|
|
79
122
|
: sourceCode.getText(prop.value);
|
|
80
|
-
const
|
|
81
|
-
return `${value} = ${
|
|
123
|
+
const accessPath = buildAccessPath(initText, prop);
|
|
124
|
+
return `${value} = ${accessPath}`;
|
|
82
125
|
})
|
|
83
126
|
.join(';\nconst ');
|
|
84
127
|
// Only apply the fix if we have valid declarations
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noComplexCloudParams = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const PASCAL_CASE_RE = /^[A-Z][a-zA-Z0-9]*$/;
|
|
6
7
|
exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'no-complex-cloud-params',
|
|
8
9
|
meta: {
|
|
@@ -13,7 +14,7 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
|
13
14
|
},
|
|
14
15
|
schema: [],
|
|
15
16
|
messages: {
|
|
16
|
-
noComplexObjects: '
|
|
17
|
+
noComplexObjects: 'Cloud function "{{callee}}" receives a value that is not JSON-serializable. Cloud params must stay plain data; class instances, functions, RegExp/BigInt/TypedArray values, or nested complex properties are dropped or cause runtime errors during transport to Firebase. Send only primitives and plain objects/arrays, or serialize the value first (for example, convert a RegExp to a string or JSON.stringify the payload) before calling "{{callee}}".',
|
|
17
18
|
},
|
|
18
19
|
},
|
|
19
20
|
defaultOptions: [],
|
|
@@ -117,8 +118,20 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
|
117
118
|
return isClassInstance(def.node.init);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
|
-
//
|
|
121
|
-
|
|
121
|
+
// Only consider identifiers as potential class instances if they follow PascalCase naming
|
|
122
|
+
// (not SCREAMING_SNAKE_CASE constants)
|
|
123
|
+
if (node.name[0] === node.name[0].toUpperCase()) {
|
|
124
|
+
// Check if it's a constant (SCREAMING_SNAKE_CASE) - these are not class instances
|
|
125
|
+
if (node.name.includes('_') &&
|
|
126
|
+
node.name === node.name.toUpperCase()) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Check if it's PascalCase (potential class instance)
|
|
130
|
+
// PascalCase: starts with capital, has lowercase letters, no underscores
|
|
131
|
+
const isPascalCase = PASCAL_CASE_RE.test(node.name) && /[a-z]/.test(node.name);
|
|
132
|
+
return isPascalCase;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
122
135
|
}
|
|
123
136
|
return false;
|
|
124
137
|
}
|
|
@@ -311,8 +324,9 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
|
311
324
|
return isComplexValue(node);
|
|
312
325
|
}
|
|
313
326
|
function checkCloudFunctionCall(node) {
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
const callee = node.callee;
|
|
328
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
329
|
+
cloudFunctions.has(callee.name)) {
|
|
316
330
|
// Check each argument for complex objects
|
|
317
331
|
node.arguments.forEach((arg) => {
|
|
318
332
|
if (hasComplexProperties(arg) && !reportedNodes.has(node)) {
|
|
@@ -320,6 +334,9 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
|
320
334
|
context.report({
|
|
321
335
|
node,
|
|
322
336
|
messageId: 'noComplexObjects',
|
|
337
|
+
data: {
|
|
338
|
+
callee: callee.name,
|
|
339
|
+
},
|
|
323
340
|
});
|
|
324
341
|
}
|
|
325
342
|
});
|
|
@@ -37,12 +37,12 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
37
37
|
meta: {
|
|
38
38
|
type: 'suggestion',
|
|
39
39
|
docs: {
|
|
40
|
-
description: '
|
|
40
|
+
description: 'Discourage CSS properties that force GPU compositing layers (e.g., transform, filter, will-change). Extra layers consume GPU memory and split rendering work, which slows scrolling and animation when sprinkled across a page. The rule inspects inline style objects and MUI sx props so layer promotion stays intentional rather than incidental.',
|
|
41
41
|
recommended: 'error',
|
|
42
42
|
},
|
|
43
43
|
schema: [],
|
|
44
44
|
messages: {
|
|
45
|
-
compositingLayer: '{{property}}
|
|
45
|
+
compositingLayer: 'CSS property "{{property}}" promotes this element to its own GPU compositing layer. Extra layers allocate GPU memory and isolate painting, which slows scrolling and animation when used broadly. Remove "{{property}}" or keep it only when the layer promotion is intentional and documented (e.g., eslint-disable with a comment).',
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
defaultOptions: [],
|
|
@@ -79,7 +79,8 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
79
79
|
// Check for JSX style attribute
|
|
80
80
|
if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
81
81
|
current.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
82
|
-
(current.parent.name.name === 'style' ||
|
|
82
|
+
(current.parent.name.name === 'style' ||
|
|
83
|
+
current.parent.name.name === 'sx')) {
|
|
83
84
|
return true;
|
|
84
85
|
}
|
|
85
86
|
// Check for style-related variable names or properties
|
|
@@ -91,7 +92,8 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
91
92
|
// Check for style-related object property assignments
|
|
92
93
|
if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
93
94
|
current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
94
|
-
(/style/i.test(current.parent.key.name) ||
|
|
95
|
+
(/style/i.test(current.parent.key.name) ||
|
|
96
|
+
current.parent.key.name === 'sx')) {
|
|
95
97
|
return true;
|
|
96
98
|
}
|
|
97
99
|
// Skip if we're in a TypeScript type definition
|
|
@@ -9,12 +9,14 @@ exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
|
|
|
9
9
|
meta: {
|
|
10
10
|
type: 'problem',
|
|
11
11
|
docs: {
|
|
12
|
-
description: 'Disallow
|
|
12
|
+
description: 'Disallow conditional string literals beside other JSX text to avoid fragmented text nodes, translation issues, and hydration mismatches.',
|
|
13
13
|
recommended: 'error',
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
unexpected: 'Conditional text
|
|
17
|
+
unexpected: 'Conditional text literal {{literal}} is rendered next to other JSX text or expressions under condition {{condition}}. ' +
|
|
18
|
+
'This fragments text nodes, confusing translation/i18n tools and potentially causing React hydration mismatches when server and client group the text differently. ' +
|
|
19
|
+
'Wrap the conditional expression in its own element (for example, <span>{ {{expression}} }</span>) or move the entire sentence inside the conditional so it renders as a single text node.',
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
22
|
defaultOptions: [],
|
|
@@ -42,17 +44,40 @@ exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
|
|
|
42
44
|
'expression' in n &&
|
|
43
45
|
(n.expression.type === 'Identifier' ||
|
|
44
46
|
n.expression.type === 'MemberExpression'));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
const hasSiblingContent = siblingTextNodes.concat(siblingExpressionNodes).length > 0;
|
|
48
|
+
if (!hasSiblingContent) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const logicalExpression = node.expression;
|
|
52
|
+
const literalNode = logicalExpression.right;
|
|
53
|
+
const conditionalNode = logicalExpression.left;
|
|
54
|
+
// Only enforce when the literal is the expression's return value.
|
|
55
|
+
if (literalNode.type !== utils_1.TSESTree.AST_NODE_TYPES.Literal) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Only enforce for string literals to avoid misleading messages for
|
|
59
|
+
// numeric or boolean literals rendered conditionally.
|
|
60
|
+
if (typeof literalNode.value !== 'string') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Ignore logical expressions that do not actually render the literal
|
|
65
|
+
* conditionally (e.g., literal && condition or literal || condition)
|
|
66
|
+
* and expressions with two literals.
|
|
67
|
+
*/
|
|
68
|
+
if (conditionalNode.type === utils_1.TSESTree.AST_NODE_TYPES.Literal) {
|
|
69
|
+
return;
|
|
55
70
|
}
|
|
71
|
+
const sourceCode = context.getSourceCode();
|
|
72
|
+
context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: 'unexpected',
|
|
75
|
+
data: {
|
|
76
|
+
literal: sourceCode.getText(literalNode),
|
|
77
|
+
condition: sourceCode.getText(conditionalNode),
|
|
78
|
+
expression: sourceCode.getText(logicalExpression),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
56
81
|
},
|
|
57
82
|
};
|
|
58
83
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
ignorePatterns?: string[];
|
|
5
|
+
allowWithUseAlertDialog?: boolean;
|
|
6
|
+
}
|
|
7
|
+
];
|
|
8
|
+
export declare const noConsoleError: TSESLint.RuleModule<"noConsoleError", Options, TSESLint.RuleListener>;
|
|
9
|
+
export {};
|