@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.enforceFieldPathSyntaxInDocSetter = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const getMethodName_1 = require("../utils/getMethodName");
|
|
6
7
|
exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'enforce-fieldpath-syntax-in-docsetter',
|
|
8
9
|
meta: {
|
|
@@ -16,11 +17,12 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
16
17
|
fixable: 'code',
|
|
17
18
|
schema: [],
|
|
18
19
|
messages: {
|
|
19
|
-
enforceFieldPathSyntax: '
|
|
20
|
+
enforceFieldPathSyntax: 'What’s wrong: DocSetter {{methodName}} receives nested object data under "{{topLevelKey}}". → Why it matters: Firestore treats that nested map as a whole sub-document write, so partial updates can overwrite sibling fields you did not include. → How to fix: Flatten nested properties into FieldPath (dot) keys before passing documentData (e.g., "{{exampleFieldPath}}") so only the intended leaves are written.',
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
defaultOptions: [],
|
|
23
24
|
create(context) {
|
|
25
|
+
const sourceCode = context.getSourceCode();
|
|
24
26
|
// Track DocSetter variables
|
|
25
27
|
const docSetterVariables = new Set();
|
|
26
28
|
// Helper function to check if a node is a DocSetter method call
|
|
@@ -51,45 +53,14 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
51
53
|
return (prop.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
52
54
|
(prop.type === utils_1.AST_NODE_TYPES.Property && prop.computed === true));
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
// Skip computed properties (dynamic keys)
|
|
65
|
-
if (property.computed) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const isNumericKey = property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
69
|
-
(typeof property.key.value === 'number' ||
|
|
70
|
-
(typeof property.key.value === 'string' &&
|
|
71
|
-
/^\d+$/.test(property.key.value)));
|
|
72
|
-
// Allow top-level numeric keys (treated like array indexes)
|
|
73
|
-
if (prefix === '' && isNumericKey) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const value = property.value;
|
|
77
|
-
// Skip if the property key is already using dot notation
|
|
78
|
-
if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
79
|
-
typeof property.key.value === 'string' &&
|
|
80
|
-
property.key.value.includes('.')) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
// Check if the value is an object (but not an array)
|
|
84
|
-
if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
85
|
-
// Skip nested objects that contain spread elements or computed properties
|
|
86
|
-
const hasSpreadOrComputed = value.properties.some((prop) => isSpreadOrComputed(prop));
|
|
87
|
-
if (!hasSpreadOrComputed) {
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return false;
|
|
56
|
+
function isNumericKey(property) {
|
|
57
|
+
return (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
58
|
+
(typeof property.key.value === 'number' ||
|
|
59
|
+
(typeof property.key.value === 'string' &&
|
|
60
|
+
/^\d+$/.test(property.key.value))));
|
|
61
|
+
}
|
|
62
|
+
function hasRootNumericKey(node) {
|
|
63
|
+
return node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.Property && isNumericKey(property));
|
|
93
64
|
}
|
|
94
65
|
// Helper function to flatten nested objects into FieldPath syntax
|
|
95
66
|
function flattenObject(obj, sourceCode, prefix = '') {
|
|
@@ -127,11 +98,8 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
127
98
|
// Skip other key types
|
|
128
99
|
continue;
|
|
129
100
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
(typeof property.key.value === 'string' &&
|
|
133
|
-
/^\d+$/.test(property.key.value)));
|
|
134
|
-
if (prefix === '' && isNumericKey) {
|
|
101
|
+
const numericKey = isNumericKey(property);
|
|
102
|
+
if (prefix === '' && numericKey) {
|
|
135
103
|
continue;
|
|
136
104
|
}
|
|
137
105
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
@@ -152,11 +120,13 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
152
120
|
return key.includes('.') || !/^(?:[$_A-Za-z][$\w]*)$/u.test(key);
|
|
153
121
|
}
|
|
154
122
|
// Helper function to convert an object to FieldPath syntax
|
|
155
|
-
function convertToFieldPathSyntax(node, sourceCode) {
|
|
123
|
+
function convertToFieldPathSyntax(node, sourceCode, preflattened) {
|
|
156
124
|
const idProperty = node.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
157
125
|
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
158
126
|
prop.key.name === 'id');
|
|
159
|
-
const flattenedProperties =
|
|
127
|
+
const flattenedProperties = {
|
|
128
|
+
...(preflattened ?? flattenObject(node, sourceCode)),
|
|
129
|
+
};
|
|
160
130
|
if (idProperty && idProperty.type === utils_1.AST_NODE_TYPES.Property) {
|
|
161
131
|
delete flattenedProperties['id'];
|
|
162
132
|
}
|
|
@@ -175,6 +145,72 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
175
145
|
result += '}';
|
|
176
146
|
return result;
|
|
177
147
|
}
|
|
148
|
+
function getPropertyKeyText(property) {
|
|
149
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
150
|
+
return property.key.name;
|
|
151
|
+
}
|
|
152
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
153
|
+
(typeof property.key.value === 'string' ||
|
|
154
|
+
typeof property.key.value === 'number')) {
|
|
155
|
+
return String(property.key.value);
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function getFirstNestedObjectProperty(node, keyPredicate) {
|
|
160
|
+
for (const property of node.properties) {
|
|
161
|
+
if (isSpreadOrComputed(property)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const propertyKeyText = getPropertyKeyText(property);
|
|
168
|
+
if (!propertyKeyText) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (keyPredicate && !keyPredicate(propertyKeyText)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Root-level numeric keys typically model array-style buckets rather than
|
|
175
|
+
// Firestore document fields, so ignore them when identifying nested objects
|
|
176
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.Property &&
|
|
177
|
+
isNumericKey(property)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const hasSpreadOrComputed = property.value.properties.some((prop) => isSpreadOrComputed(prop));
|
|
184
|
+
if (hasSpreadOrComputed) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
return property;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function extractViolationDetails(firstArg, sourceCode) {
|
|
192
|
+
const firstNestedPropertyWithoutDots = getFirstNestedObjectProperty(firstArg, (key) => !key.includes('.'));
|
|
193
|
+
const firstNestedProperty = firstNestedPropertyWithoutDots ||
|
|
194
|
+
getFirstNestedObjectProperty(firstArg);
|
|
195
|
+
if (!firstNestedProperty) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const propertyKeyText = getPropertyKeyText(firstNestedProperty);
|
|
199
|
+
const exampleFieldPathFromProperty = propertyKeyText &&
|
|
200
|
+
flattenObject(firstNestedProperty.value, sourceCode, propertyKeyText);
|
|
201
|
+
const flattenedProperties = flattenObject(firstArg, sourceCode);
|
|
202
|
+
const exampleFieldPath = (exampleFieldPathFromProperty &&
|
|
203
|
+
Object.keys(exampleFieldPathFromProperty).find((key) => key.includes('.'))) ??
|
|
204
|
+
(propertyKeyText &&
|
|
205
|
+
Object.keys(flattenedProperties).find((key) => key.startsWith(`${propertyKeyText}.`))) ??
|
|
206
|
+
Object.keys(flattenedProperties).find((key) => key.includes('.')) ??
|
|
207
|
+
'field.nested';
|
|
208
|
+
return {
|
|
209
|
+
topLevelKey: propertyKeyText ?? 'nested field',
|
|
210
|
+
exampleFieldPath,
|
|
211
|
+
flattenedProperties,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
178
214
|
return {
|
|
179
215
|
// Track DocSetter variable declarations
|
|
180
216
|
VariableDeclarator(node) {
|
|
@@ -192,16 +228,30 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
192
228
|
}
|
|
193
229
|
// Check if the first argument is an object literal
|
|
194
230
|
const firstArg = node.arguments[0];
|
|
195
|
-
if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression
|
|
196
|
-
|
|
231
|
+
if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (hasRootNumericKey(firstArg)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const violationDetails = extractViolationDetails(firstArg, sourceCode);
|
|
238
|
+
if (!violationDetails) {
|
|
197
239
|
return;
|
|
198
240
|
}
|
|
199
241
|
// Report and fix the issue
|
|
242
|
+
const callee = node.callee;
|
|
200
243
|
context.report({
|
|
201
244
|
node: firstArg,
|
|
202
245
|
messageId: 'enforceFieldPathSyntax',
|
|
246
|
+
data: {
|
|
247
|
+
methodName: `${(0, getMethodName_1.getMemberExpressionName)(callee, sourceCode, {
|
|
248
|
+
computedFallbackToText: false,
|
|
249
|
+
}) || 'set'}()`,
|
|
250
|
+
topLevelKey: violationDetails.topLevelKey,
|
|
251
|
+
exampleFieldPath: violationDetails.exampleFieldPath,
|
|
252
|
+
},
|
|
203
253
|
fix(fixer) {
|
|
204
|
-
const newText = convertToFieldPathSyntax(firstArg,
|
|
254
|
+
const newText = convertToFieldPathSyntax(firstArg, sourceCode, violationDetails.flattenedProperties);
|
|
205
255
|
return fixer.replaceText(firstArg, newText);
|
|
206
256
|
},
|
|
207
257
|
});
|
|
@@ -24,8 +24,20 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
27
|
-
missingGeneric:
|
|
28
|
-
|
|
27
|
+
missingGeneric: [
|
|
28
|
+
"What's wrong: {{ type }} is missing its document schema generic (the document data type).",
|
|
29
|
+
'',
|
|
30
|
+
'Why it matters: Without the generic, Firestore references fall back to loose DocumentData, so TypeScript cannot catch field typos or missing required properties before they reach Firestore.',
|
|
31
|
+
'',
|
|
32
|
+
'How to fix: Add the document interface/type as the generic (e.g., const ref: {{ type }}<UserDoc> = ... or doc<UserDoc>(collection)).',
|
|
33
|
+
].join('\n'),
|
|
34
|
+
invalidGeneric: [
|
|
35
|
+
'What\'s wrong: {{ type }} uses "any" or an empty object ({}) in its schema generic.',
|
|
36
|
+
'',
|
|
37
|
+
'Why it matters: This erases the document schema and disables TypeScript checks on Firestore reads and writes, so malformed payloads and missing fields can pass silently.',
|
|
38
|
+
'',
|
|
39
|
+
'How to fix: Define a concrete interface/type for the document (e.g., interface UserDoc { name: string }) and use it as the generic instead of "any" or {}.',
|
|
40
|
+
].join('\n'),
|
|
29
41
|
},
|
|
30
42
|
},
|
|
31
43
|
defaultOptions: [],
|
|
@@ -61,7 +73,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
61
73
|
}
|
|
62
74
|
// Prevent infinite recursion
|
|
63
75
|
typeCache.set(typeName, false);
|
|
64
|
-
const program = context.
|
|
76
|
+
const program = context.sourceCode.ast;
|
|
65
77
|
const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
|
|
66
78
|
n.id.name === typeName);
|
|
67
79
|
if (interfaceDecl) {
|
|
@@ -94,7 +94,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
94
94
|
const firestoreTransactionVariables = new Set();
|
|
95
95
|
const docSetterVariables = new Set();
|
|
96
96
|
const batchManagerVariables = new Set();
|
|
97
|
-
const sourceCode = context.
|
|
97
|
+
const sourceCode = context.sourceCode;
|
|
98
98
|
const clearFirestoreTrackingFor = (name) => {
|
|
99
99
|
firestoreDocRefVariables.delete(name);
|
|
100
100
|
firestoreCollectionVariables.delete(name);
|
|
@@ -149,7 +149,9 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
149
149
|
return true;
|
|
150
150
|
}
|
|
151
151
|
if (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
152
|
-
(target.name === 'transaction' ||
|
|
152
|
+
(target.name === 'transaction' ||
|
|
153
|
+
target.name === 'tx' ||
|
|
154
|
+
target.name === 't')) {
|
|
153
155
|
firestoreTransactionVariables.add(varName);
|
|
154
156
|
return true;
|
|
155
157
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceFirestoreSetMerge = 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.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'enforce-firestore-set-merge',
|
|
8
9
|
meta: {
|
|
@@ -93,9 +94,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
93
94
|
return true;
|
|
94
95
|
}
|
|
95
96
|
// Check if it's a Firestore document reference by looking at imports
|
|
96
|
-
const program = context
|
|
97
|
-
.getAncestors()
|
|
98
|
-
.find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
|
|
97
|
+
const program = ASTHelpers_1.ASTHelpers.getAncestors(context, node).find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
|
|
99
98
|
if (program) {
|
|
100
99
|
for (const node of program.body) {
|
|
101
100
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
@@ -212,7 +211,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
212
211
|
node,
|
|
213
212
|
messageId: 'preferSetMerge',
|
|
214
213
|
fix(fixer) {
|
|
215
|
-
const newText = convertUpdateToSetMerge(node, context.
|
|
214
|
+
const newText = convertUpdateToSetMerge(node, context.sourceCode);
|
|
216
215
|
return fixer.replaceText(node, newText);
|
|
217
216
|
},
|
|
218
217
|
});
|
|
@@ -21,7 +21,7 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
|
-
const sourceCode = context.
|
|
24
|
+
const sourceCode = context.sourceCode;
|
|
25
25
|
function isHookName(name) {
|
|
26
26
|
return /^use[A-Z]/.test(name);
|
|
27
27
|
}
|
|
@@ -5,8 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.enforceIdentifiableFirestoreType = void 0;
|
|
7
7
|
const utils_1 = require("@typescript-eslint/utils");
|
|
8
|
-
const createRule_1 = require("../utils/createRule");
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const TRANSPARENT_TYPE_NAMES = new Set(['Readonly', 'Resolve']);
|
|
10
11
|
exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
|
|
11
12
|
name: 'enforce-identifiable-firestore-type',
|
|
12
13
|
meta: {
|
|
@@ -63,94 +64,143 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
|
|
|
63
64
|
TSTypeAliasDeclaration(node) {
|
|
64
65
|
if (node.id.name === folderName) {
|
|
65
66
|
hasExpectedType = true;
|
|
66
|
-
|
|
67
|
+
const findTypeAliasAnnotation = (typeName) => {
|
|
68
|
+
let scope = context.getScope();
|
|
69
|
+
while (scope) {
|
|
70
|
+
const variable = scope.variables.find((variableNode) => variableNode.name === typeName);
|
|
71
|
+
if (variable) {
|
|
72
|
+
const definition = variable.defs.find((definition) => definition.node.type ===
|
|
73
|
+
utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
|
|
74
|
+
if (definition &&
|
|
75
|
+
definition.node.type ===
|
|
76
|
+
utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
77
|
+
definition.node.typeAnnotation) {
|
|
78
|
+
return definition.node.typeAnnotation;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
scope = scope.upper;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const isParenthesizedType = (node) => node?.type === 'TSParenthesizedType';
|
|
86
|
+
const isReadonlyTypeOperator = (node) => node?.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
87
|
+
node.operator === 'readonly';
|
|
88
|
+
const unwrapTransparentType = (typeNode) => {
|
|
89
|
+
let current = typeNode;
|
|
90
|
+
while (current) {
|
|
91
|
+
if (isParenthesizedType(current)) {
|
|
92
|
+
const parenthesized = current;
|
|
93
|
+
current = parenthesized.typeAnnotation ?? null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (isReadonlyTypeOperator(current)) {
|
|
97
|
+
current = current.typeAnnotation ?? null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return current ?? null;
|
|
103
|
+
};
|
|
67
104
|
const findIdentifiable = (type, checkedTypes = new Set()) => {
|
|
68
|
-
|
|
105
|
+
const resolvedType = unwrapTransparentType(type);
|
|
106
|
+
if (!resolvedType) {
|
|
69
107
|
return false;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
}
|
|
109
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
110
|
+
resolvedType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
111
|
+
const typeName = resolvedType.typeName.name;
|
|
73
112
|
if (typeName === 'Identifiable') {
|
|
74
113
|
return true;
|
|
75
114
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const def = variable.defs.find((d) => d.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
|
|
83
|
-
if (def &&
|
|
84
|
-
'typeAnnotation' in def.node &&
|
|
85
|
-
def.node.typeAnnotation) {
|
|
86
|
-
return findIdentifiable(def.node.typeAnnotation, checkedTypes);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Try looking in the parent scope
|
|
90
|
-
if (scope.upper) {
|
|
91
|
-
const parentVariable = scope.upper.variables.find((v) => v.name === typeName);
|
|
92
|
-
if (parentVariable) {
|
|
93
|
-
const def = parentVariable.defs.find((d) => d.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
|
|
94
|
-
if (def &&
|
|
95
|
-
'typeAnnotation' in def.node &&
|
|
96
|
-
def.node.typeAnnotation) {
|
|
97
|
-
return findIdentifiable(def.node.typeAnnotation, checkedTypes);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
115
|
+
if (TRANSPARENT_TYPE_NAMES.has(typeName) &&
|
|
116
|
+
resolvedType.typeParameters?.params?.some((param) => findIdentifiable(param, checkedTypes))) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (checkedTypes.has(typeName)) {
|
|
120
|
+
return false;
|
|
101
121
|
}
|
|
122
|
+
checkedTypes.add(typeName);
|
|
123
|
+
const aliasAnnotation = findTypeAliasAnnotation(typeName);
|
|
124
|
+
return findIdentifiable(aliasAnnotation, checkedTypes);
|
|
102
125
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return type.types.some((part) => findIdentifiable(part, checkedTypes));
|
|
126
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
127
|
+
return resolvedType.types.some((part) => findIdentifiable(part, new Set(checkedTypes)));
|
|
106
128
|
}
|
|
107
129
|
return false;
|
|
108
130
|
};
|
|
109
|
-
// Check if type extends Identifiable
|
|
110
|
-
const checkIdentifiableExtension = (type) => {
|
|
111
|
-
if (!type)
|
|
112
|
-
return false;
|
|
113
|
-
return findIdentifiable(type);
|
|
114
|
-
};
|
|
115
131
|
// Check if type has id: string field
|
|
116
|
-
const checkIdField = (type) => {
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
132
|
+
const checkIdField = (type, visitedTypes = new Set()) => {
|
|
133
|
+
const resolvedType = unwrapTransparentType(type);
|
|
134
|
+
if (!resolvedType) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
138
|
+
return resolvedType.members.some((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
120
139
|
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
121
140
|
member.key.name === 'id' &&
|
|
122
141
|
member.typeAnnotation?.typeAnnotation.type ===
|
|
123
142
|
utils_1.AST_NODE_TYPES.TSStringKeyword);
|
|
124
143
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
145
|
+
return resolvedType.types.some((part) => checkIdField(part, new Set(visitedTypes)));
|
|
146
|
+
}
|
|
147
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
148
|
+
if (resolvedType.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const typeName = resolvedType.typeName.name;
|
|
152
|
+
if (typeName === 'Identifiable') {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (TRANSPARENT_TYPE_NAMES.has(typeName) &&
|
|
156
|
+
resolvedType.typeParameters?.params?.some((param) => checkIdField(param, visitedTypes))) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (visitedTypes.has(typeName)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
visitedTypes.add(typeName);
|
|
163
|
+
const referencedType = findTypeAliasAnnotation(typeName);
|
|
164
|
+
return checkIdField(referencedType, visitedTypes);
|
|
128
165
|
}
|
|
129
166
|
return false;
|
|
130
167
|
};
|
|
131
168
|
// Check if type is wrapped in a utility type
|
|
132
169
|
const isUtilityType = (type) => {
|
|
170
|
+
if (!type) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
133
173
|
return (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
134
174
|
type.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
135
175
|
type.typeName.name === 'Resolve');
|
|
136
176
|
};
|
|
137
177
|
// Recursively check the type and its parameters
|
|
138
|
-
const checkType = (type) => {
|
|
139
|
-
|
|
140
|
-
if (
|
|
178
|
+
const checkType = (type, visitedTypes = new Set()) => {
|
|
179
|
+
const resolvedType = unwrapTransparentType(type);
|
|
180
|
+
if (!resolvedType) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (findIdentifiable(resolvedType)) {
|
|
141
184
|
return true;
|
|
142
185
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
checkIdField(
|
|
186
|
+
if (isUtilityType(resolvedType) &&
|
|
187
|
+
resolvedType.typeParameters?.params?.[0] &&
|
|
188
|
+
checkIdField(resolvedType.typeParameters.params[0])) {
|
|
146
189
|
return true;
|
|
147
190
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
191
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
192
|
+
resolvedType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
193
|
+
const typeName = resolvedType.typeName.name;
|
|
194
|
+
if (visitedTypes.has(typeName)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
visitedTypes.add(typeName);
|
|
198
|
+
const aliasAnnotation = findTypeAliasAnnotation(typeName);
|
|
199
|
+
return checkType(aliasAnnotation, visitedTypes);
|
|
200
|
+
}
|
|
201
|
+
if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
202
|
+
return resolvedType.types.some((part) => checkType(part, new Set(visitedTypes)));
|
|
152
203
|
}
|
|
153
|
-
// For direct type definitions, require extending Identifiable
|
|
154
204
|
return false;
|
|
155
205
|
};
|
|
156
206
|
typeHasIdentifiable = checkType(node.typeAnnotation);
|
|
@@ -90,7 +90,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
90
90
|
messageId: 'requireMemoize',
|
|
91
91
|
fix(fixer) {
|
|
92
92
|
const fixes = [];
|
|
93
|
-
const sourceCode = context.
|
|
93
|
+
const sourceCode = context.sourceCode;
|
|
94
94
|
const decoratorIdent = memoizeNamespace
|
|
95
95
|
? `${memoizeNamespace}.Memoize`
|
|
96
96
|
: memoizeAlias;
|
|
@@ -122,11 +122,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
122
122
|
const insertionTarget = node.decorators && node.decorators.length > 0
|
|
123
123
|
? node.decorators[0]
|
|
124
124
|
: node;
|
|
125
|
-
const insertionStart = insertionTarget.range
|
|
126
|
-
? insertionTarget.range[0]
|
|
127
|
-
: node.range
|
|
128
|
-
? node.range[0]
|
|
129
|
-
: 0;
|
|
125
|
+
const insertionStart = insertionTarget.range?.[0] ?? node.range?.[0] ?? 0;
|
|
130
126
|
const text = sourceCode.text;
|
|
131
127
|
const lineStart = text.lastIndexOf('\n', insertionStart - 1) + 1;
|
|
132
128
|
const leadingWhitespace = text.slice(lineStart, insertionStart).match(/^[ \t]*/)?.[0] ?? '';
|
|
@@ -42,7 +42,7 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
42
42
|
fixable: 'code',
|
|
43
43
|
schema: [],
|
|
44
44
|
messages: {
|
|
45
|
-
requireMemoizeGetter: 'Private
|
|
45
|
+
requireMemoizeGetter: 'Private getter "{{name}}" should use @Memoize() so repeated accesses reuse the same instance instead of re-instantiating and losing internal state. Add @Memoize() and import Memoize from "@blumintinc/typescript-memoize" to avoid redundant setup work.',
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
defaultOptions: [],
|
|
@@ -52,17 +52,28 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
52
52
|
if (!/\.tsx?$/i.test(filename)) {
|
|
53
53
|
return {};
|
|
54
54
|
}
|
|
55
|
+
const sourceCode = context.getSourceCode();
|
|
55
56
|
let hasMemoizeImport = false;
|
|
56
57
|
let memoizeAlias = 'Memoize';
|
|
58
|
+
let memoizeNamespace = null;
|
|
59
|
+
let hasNamedImport = false;
|
|
57
60
|
let scheduledImportFix = false;
|
|
58
61
|
return {
|
|
59
62
|
ImportDeclaration(node) {
|
|
60
63
|
if (MEMOIZE_MODULES.has(String(node.source.value))) {
|
|
61
|
-
const spec
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
for (const spec of node.specifiers) {
|
|
65
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
66
|
+
spec.imported.name === 'Memoize') {
|
|
67
|
+
hasMemoizeImport = true;
|
|
68
|
+
hasNamedImport = true;
|
|
69
|
+
memoizeAlias = spec.local.name;
|
|
70
|
+
}
|
|
71
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
72
|
+
hasMemoizeImport = true;
|
|
73
|
+
if (!hasNamedImport) {
|
|
74
|
+
memoizeNamespace = spec.local.name;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
},
|
|
@@ -76,16 +87,29 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
76
87
|
// enforce only "private" accessibility (undefined => public)
|
|
77
88
|
if (node.accessibility !== 'private')
|
|
78
89
|
return;
|
|
79
|
-
|
|
80
|
-
const hasDecorator = node.decorators?.some((
|
|
90
|
+
const decoratorAliases = memoizeAlias === 'Memoize' ? ['Memoize'] : ['Memoize', memoizeAlias];
|
|
91
|
+
const hasDecorator = node.decorators?.some((decorator) => decoratorAliases.some((alias) => isMemoizeDecorator(decorator, alias)));
|
|
81
92
|
if (hasDecorator)
|
|
82
93
|
return;
|
|
94
|
+
const propertyName = node.computed
|
|
95
|
+
? '[computed]'
|
|
96
|
+
: sourceCode.getText(node.key);
|
|
83
97
|
context.report({
|
|
84
98
|
node,
|
|
85
99
|
messageId: 'requireMemoizeGetter',
|
|
100
|
+
data: { name: propertyName },
|
|
86
101
|
fix(fixer) {
|
|
87
102
|
const fixes = [];
|
|
88
|
-
const
|
|
103
|
+
const getDecoratorIdent = () => {
|
|
104
|
+
if (hasNamedImport) {
|
|
105
|
+
return memoizeAlias;
|
|
106
|
+
}
|
|
107
|
+
if (memoizeNamespace) {
|
|
108
|
+
return `${memoizeNamespace}.Memoize`;
|
|
109
|
+
}
|
|
110
|
+
return memoizeAlias;
|
|
111
|
+
};
|
|
112
|
+
const decoratorIdent = getDecoratorIdent();
|
|
89
113
|
// Insert import if needed, at the top alongside other imports
|
|
90
114
|
if (!hasMemoizeImport && !scheduledImportFix) {
|
|
91
115
|
const programBody = sourceCode.ast.body;
|
|
@@ -106,12 +130,12 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
106
130
|
scheduledImportFix = true;
|
|
107
131
|
}
|
|
108
132
|
// Insert decorator above the getter (or before the first decorator), preserving indentation
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
fixes.push(fixer.insertTextBeforeRange([
|
|
133
|
+
const insertionTarget = node.decorators?.[0] ?? node;
|
|
134
|
+
const insertionStart = insertionTarget.range[0];
|
|
135
|
+
const text = sourceCode.text;
|
|
136
|
+
const lineStart = text.lastIndexOf('\n', insertionStart - 1) + 1;
|
|
137
|
+
const leadingWhitespace = text.slice(lineStart, insertionStart).match(/^[ \t]*/)?.[0] ?? '';
|
|
138
|
+
fixes.push(fixer.insertTextBeforeRange([lineStart, lineStart], `${leadingWhitespace}@${decoratorIdent}()\n`));
|
|
115
139
|
return fixes;
|
|
116
140
|
},
|
|
117
141
|
});
|