@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
|
@@ -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 {};
|
|
@@ -104,7 +104,7 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
104
104
|
},
|
|
105
105
|
],
|
|
106
106
|
messages: {
|
|
107
|
-
preferDestructuring: '
|
|
107
|
+
preferDestructuring: 'Property "{{property}}" from "{{object}}" is assigned via dot access{{targetNote}}. Destructure the property so the dependency is declared once and stays aligned with the source object. Use destructuring{{renamingHint}} (e.g., {{example}}).',
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
defaultOptions,
|
|
@@ -114,6 +114,7 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
114
114
|
enforceForRenamedProperties: defaultOptions[0].enforceForRenamedProperties,
|
|
115
115
|
...context.options[0],
|
|
116
116
|
};
|
|
117
|
+
const sourceCode = context.getSourceCode();
|
|
117
118
|
/**
|
|
118
119
|
* Check if we're inside a class method
|
|
119
120
|
*/
|
|
@@ -128,32 +129,228 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
128
129
|
}
|
|
129
130
|
return false;
|
|
130
131
|
}
|
|
132
|
+
function isIdentifierTarget(node) {
|
|
133
|
+
return node.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
134
|
+
}
|
|
135
|
+
function isSkippedClassMemberAccess(memberExpression) {
|
|
136
|
+
return (isClassInstance(memberExpression, context) ||
|
|
137
|
+
isStaticClassMember(memberExpression, context));
|
|
138
|
+
}
|
|
139
|
+
function isThisMemberInClassMethod(memberExpression) {
|
|
140
|
+
return (memberExpression.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
141
|
+
isInsideClassMethod(memberExpression));
|
|
142
|
+
}
|
|
143
|
+
function findEnclosingMethodDefinition(node) {
|
|
144
|
+
let current = node;
|
|
145
|
+
while (current) {
|
|
146
|
+
if (current.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
147
|
+
return current;
|
|
148
|
+
}
|
|
149
|
+
current = current.parent ?? undefined;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function isInsideConstructorBody(node) {
|
|
154
|
+
let current = node;
|
|
155
|
+
while (current) {
|
|
156
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
157
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
158
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
159
|
+
const enclosingMethod = findEnclosingMethodDefinition(current);
|
|
160
|
+
return (!!enclosingMethod &&
|
|
161
|
+
enclosingMethod.kind === 'constructor' &&
|
|
162
|
+
enclosingMethod.value === current);
|
|
163
|
+
}
|
|
164
|
+
current = current.parent ?? undefined;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function isPrivateIdentifierProperty(property) {
|
|
169
|
+
return property.type === utils_1.AST_NODE_TYPES.PrivateIdentifier;
|
|
170
|
+
}
|
|
171
|
+
function canDestructureObjectProperty(memberExpression, identifier) {
|
|
172
|
+
if (isPrivateIdentifierProperty(memberExpression.property)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (!options.object) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (options.enforceForRenamedProperties) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return isMatchingPropertyName(memberExpression.property, identifier.name);
|
|
182
|
+
}
|
|
183
|
+
function getPatternKeyText(memberExpression, propertyText) {
|
|
184
|
+
if (memberExpression.computed) {
|
|
185
|
+
return `[${sourceCode.getText(memberExpression.property)}]`;
|
|
186
|
+
}
|
|
187
|
+
return propertyText;
|
|
188
|
+
}
|
|
189
|
+
function getDestructuringBindingText(memberExpression, propertyText, targetName) {
|
|
190
|
+
if (isPrivateIdentifierProperty(memberExpression.property)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const patternKeyText = getPatternKeyText(memberExpression, propertyText);
|
|
194
|
+
if (memberExpression.computed ||
|
|
195
|
+
(options.enforceForRenamedProperties &&
|
|
196
|
+
!isMatchingPropertyName(memberExpression.property, targetName))) {
|
|
197
|
+
return `${patternKeyText}: ${targetName}`;
|
|
198
|
+
}
|
|
199
|
+
return patternKeyText;
|
|
200
|
+
}
|
|
131
201
|
/**
|
|
132
202
|
* Check if destructuring should be used for this node
|
|
133
203
|
*/
|
|
134
204
|
function shouldUseDestructuring(node, leftNode) {
|
|
135
|
-
|
|
136
|
-
if (isClassInstance(node, context) ||
|
|
137
|
-
isStaticClassMember(node, context)) {
|
|
205
|
+
if (!isIdentifierTarget(leftNode)) {
|
|
138
206
|
return false;
|
|
139
207
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
208
|
+
return (!isSkippedClassMemberAccess(node) &&
|
|
209
|
+
!isThisMemberInClassMethod(node) &&
|
|
210
|
+
canDestructureObjectProperty(node, leftNode));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extracts the property name from a MemberExpression when it can be safely compared.
|
|
214
|
+
*/
|
|
215
|
+
function getMemberExpressionPropertyName(memberExpression) {
|
|
216
|
+
if (isPrivateIdentifierProperty(memberExpression.property)) {
|
|
217
|
+
return null;
|
|
144
218
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return true;
|
|
219
|
+
if (!memberExpression.computed) {
|
|
220
|
+
if (memberExpression.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
221
|
+
return memberExpression.property.name;
|
|
149
222
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (memberExpression.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
226
|
+
typeof memberExpression.property.value === 'string') {
|
|
227
|
+
return memberExpression.property.value;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Look up a variable by name within a scope.
|
|
233
|
+
*/
|
|
234
|
+
function findVariableInScope(scope, name) {
|
|
235
|
+
return scope?.variables.find((variable) => variable.name === name);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Check whether a variable definition originates from a parameter.
|
|
239
|
+
*/
|
|
240
|
+
function isParameterDefinition(variable) {
|
|
241
|
+
if (!variable || !Array.isArray(variable.defs)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return variable.defs.some((definition) => definition.type === 'Parameter');
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Determine whether an identifier refers to a function or method parameter.
|
|
248
|
+
*/
|
|
249
|
+
function isFunctionParameter(identifier) {
|
|
250
|
+
let scope = context.getScope();
|
|
251
|
+
while (scope) {
|
|
252
|
+
const variable = findVariableInScope(scope, identifier.name);
|
|
253
|
+
if (variable) {
|
|
254
|
+
return isParameterDefinition(variable);
|
|
153
255
|
}
|
|
256
|
+
scope = scope.upper;
|
|
154
257
|
}
|
|
155
258
|
return false;
|
|
156
259
|
}
|
|
260
|
+
function buildReportDetails(memberExpr, targetName, examplePrefix, exampleSuffix) {
|
|
261
|
+
const objectText = sourceCode.getText(memberExpr.object);
|
|
262
|
+
const propertyText = getPropertyText(memberExpr.property, memberExpr.computed, sourceCode);
|
|
263
|
+
const usesRenaming = options.enforceForRenamedProperties &&
|
|
264
|
+
!!targetName &&
|
|
265
|
+
!isMatchingPropertyName(memberExpr.property, targetName);
|
|
266
|
+
const aliasName = targetName ?? propertyText;
|
|
267
|
+
const patternKeyText = getPatternKeyText(memberExpr, propertyText);
|
|
268
|
+
const destructuringBinding = getDestructuringBindingText(memberExpr, propertyText, aliasName) ??
|
|
269
|
+
patternKeyText;
|
|
270
|
+
return {
|
|
271
|
+
propertyText,
|
|
272
|
+
objectText,
|
|
273
|
+
destructuringBinding,
|
|
274
|
+
data: {
|
|
275
|
+
property: propertyText,
|
|
276
|
+
object: objectText,
|
|
277
|
+
targetNote: usesRenaming && targetName ? ` to "${targetName}"` : '',
|
|
278
|
+
renamingHint: usesRenaming ? ' with renaming' : '',
|
|
279
|
+
example: usesRenaming
|
|
280
|
+
? `${examplePrefix}{ ${destructuringBinding} } = ${objectText}${exampleSuffix}`
|
|
281
|
+
: `${examplePrefix}{ ${destructuringBinding} } = ${objectText}${exampleSuffix}`,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function generateVariableDeclaratorFix(fixer, node, propertyText, objectText, memberExpr) {
|
|
286
|
+
const parentNode = node.parent;
|
|
287
|
+
if (!parentNode ||
|
|
288
|
+
parentNode.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (parentNode.declarations.length > 1) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const kind = parentNode.kind;
|
|
295
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const destructuringBinding = getDestructuringBindingText(memberExpr, propertyText, node.id.name);
|
|
299
|
+
if (!destructuringBinding) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return fixer.replaceText(parentNode, `${kind} { ${destructuringBinding} } = ${objectText};`);
|
|
303
|
+
}
|
|
304
|
+
function generateAssignmentExpressionFix(fixer, node, propertyText, objectText, memberExpr) {
|
|
305
|
+
if (node.left.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const destructuringBinding = getDestructuringBindingText(memberExpr, propertyText, node.left.name);
|
|
309
|
+
if (!destructuringBinding) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return fixer.replaceText(node, `({ ${destructuringBinding} } = ${objectText})`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Report assignments that copy properties from parameter objects to class fields.
|
|
316
|
+
* These are reported without a fixer to avoid changing function signatures.
|
|
317
|
+
*/
|
|
318
|
+
function handleClassPropertyAssignment(node) {
|
|
319
|
+
// Caller ensures node.right is a MemberExpression.
|
|
320
|
+
if (!options.object ||
|
|
321
|
+
node.left.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
322
|
+
node.left.object.type !== utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!isInsideConstructorBody(node)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const rightObject = node.right.object;
|
|
329
|
+
if (rightObject.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
330
|
+
!isFunctionParameter(rightObject)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const leftPropertyName = getMemberExpressionPropertyName(node.left);
|
|
334
|
+
const rightPropertyName = getMemberExpressionPropertyName(node.right);
|
|
335
|
+
if (!leftPropertyName || !rightPropertyName) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!options.enforceForRenamedProperties &&
|
|
339
|
+
leftPropertyName !== rightPropertyName) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (isClassInstance(node.right, context) ||
|
|
343
|
+
isStaticClassMember(node.right, context)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const { data } = buildReportDetails(node.right, leftPropertyName, '(', ')');
|
|
347
|
+
// No fixer here because destructuring parameters changes the function signature and must stay manual.
|
|
348
|
+
context.report({
|
|
349
|
+
node,
|
|
350
|
+
messageId: 'preferDestructuring',
|
|
351
|
+
data,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
157
354
|
return {
|
|
158
355
|
VariableDeclarator(node) {
|
|
159
356
|
// Skip if variable is declared without assignment or if init is not a MemberExpression
|
|
@@ -161,57 +358,45 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
161
358
|
return;
|
|
162
359
|
if (node.init.type !== utils_1.AST_NODE_TYPES.MemberExpression)
|
|
163
360
|
return;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
361
|
+
const memberInit = node.init;
|
|
362
|
+
if (!shouldUseDestructuring(memberInit, node.id)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const targetName = node.id.type === utils_1.AST_NODE_TYPES.Identifier ? node.id.name : null;
|
|
366
|
+
const { propertyText, objectText, data } = buildReportDetails(memberInit, targetName, `${node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration
|
|
367
|
+
? node.parent.kind
|
|
368
|
+
: 'const'} `, ';');
|
|
369
|
+
context.report({
|
|
370
|
+
node,
|
|
371
|
+
messageId: 'preferDestructuring',
|
|
372
|
+
data,
|
|
373
|
+
fix(fixer) {
|
|
374
|
+
return generateVariableDeclaratorFix(fixer, node, propertyText, objectText, memberInit);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
AssignmentExpression(node) {
|
|
379
|
+
if (node.operator !== '=' ||
|
|
380
|
+
node.right.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const memberRight = node.right;
|
|
384
|
+
if (shouldUseDestructuring(memberRight, node.left)) {
|
|
385
|
+
const targetName = node.left.type === utils_1.AST_NODE_TYPES.Identifier
|
|
386
|
+
? node.left.name
|
|
387
|
+
: null;
|
|
388
|
+
const { propertyText, objectText, data } = buildReportDetails(memberRight, targetName, '(', ')');
|
|
168
389
|
context.report({
|
|
169
390
|
node,
|
|
170
391
|
messageId: 'preferDestructuring',
|
|
392
|
+
data,
|
|
171
393
|
fix(fixer) {
|
|
172
|
-
|
|
173
|
-
const parentNode = node.parent;
|
|
174
|
-
if (!parentNode ||
|
|
175
|
-
parentNode.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const kind = parentNode.kind;
|
|
179
|
-
// Handle renamed properties
|
|
180
|
-
if (options.enforceForRenamedProperties &&
|
|
181
|
-
node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
182
|
-
node.init &&
|
|
183
|
-
node.init.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
184
|
-
!isMatchingPropertyName(node.init.property, node.id.name)) {
|
|
185
|
-
return fixer.replaceText(parentNode, `${kind} { ${propertyText}: ${node.id.name} } = ${objectText};`);
|
|
186
|
-
}
|
|
187
|
-
return fixer.replaceText(parentNode, `${kind} { ${propertyText} } = ${objectText};`);
|
|
394
|
+
return generateAssignmentExpressionFix(fixer, node, propertyText, objectText, memberRight);
|
|
188
395
|
},
|
|
189
396
|
});
|
|
397
|
+
return;
|
|
190
398
|
}
|
|
191
|
-
|
|
192
|
-
AssignmentExpression(node) {
|
|
193
|
-
if (node.operator === '=' &&
|
|
194
|
-
node.right.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
195
|
-
if (shouldUseDestructuring(node.right, node.left)) {
|
|
196
|
-
const sourceCode = context.getSourceCode();
|
|
197
|
-
const objectText = sourceCode.getText(node.right.object);
|
|
198
|
-
const propertyText = getPropertyText(node.right.property, node.right.computed, sourceCode);
|
|
199
|
-
context.report({
|
|
200
|
-
node,
|
|
201
|
-
messageId: 'preferDestructuring',
|
|
202
|
-
fix(fixer) {
|
|
203
|
-
// Handle renamed properties
|
|
204
|
-
if (options.enforceForRenamedProperties &&
|
|
205
|
-
node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
206
|
-
node.right.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
207
|
-
!isMatchingPropertyName(node.right.property, node.left.name)) {
|
|
208
|
-
return fixer.replaceText(node, `({ ${propertyText}: ${node.left.name} } = ${objectText})`);
|
|
209
|
-
}
|
|
210
|
-
return fixer.replaceText(node, `({ ${propertyText} } = ${objectText})`);
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
399
|
+
handleClassPropertyAssignment(node);
|
|
215
400
|
},
|
|
216
401
|
};
|
|
217
402
|
},
|