@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
|
@@ -16,11 +16,12 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
16
16
|
fixable: 'code',
|
|
17
17
|
schema: [],
|
|
18
18
|
messages: {
|
|
19
|
-
preferTimestampNow: '
|
|
19
|
+
preferTimestampNow: '{{expression}} builds a timestamp from the local JS clock. Firestore timestamps should come from the SDK so they stay server-synced and avoid drift between app servers and clients. Use {{timestampAlias}}.now() so the timestamp is generated by Firestore; replace this expression with {{timestampAlias}}.now().',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
|
+
const sourceCode = context.getSourceCode();
|
|
24
25
|
// Only apply this rule to backend code (functions/src/)
|
|
25
26
|
const filename = context.getFilename();
|
|
26
27
|
if (!filename.includes('functions/src/')) {
|
|
@@ -151,14 +152,19 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
151
152
|
},
|
|
152
153
|
CallExpression(node) {
|
|
153
154
|
if (isTimestampFromDateWithNewDate(node)) {
|
|
155
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const timestampObj = sourceCode.getText(node.callee.object);
|
|
154
159
|
context.report({
|
|
155
160
|
node,
|
|
156
161
|
messageId: 'preferTimestampNow',
|
|
162
|
+
data: {
|
|
163
|
+
expression: sourceCode.getText(node),
|
|
164
|
+
timestampAlias: timestampObj,
|
|
165
|
+
},
|
|
157
166
|
fix(fixer) {
|
|
158
167
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
159
|
-
const timestampObj = context
|
|
160
|
-
.getSourceCode()
|
|
161
|
-
.getText(node.callee.object);
|
|
162
168
|
return fixer.replaceText(node, `${timestampObj}.now()`);
|
|
163
169
|
}
|
|
164
170
|
return null;
|
|
@@ -166,14 +172,19 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
166
172
|
});
|
|
167
173
|
}
|
|
168
174
|
else if (isTimestampFromMillisWithDateNow(node)) {
|
|
175
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const timestampObj = sourceCode.getText(node.callee.object);
|
|
169
179
|
context.report({
|
|
170
180
|
node,
|
|
171
181
|
messageId: 'preferTimestampNow',
|
|
182
|
+
data: {
|
|
183
|
+
expression: sourceCode.getText(node),
|
|
184
|
+
timestampAlias: timestampObj,
|
|
185
|
+
},
|
|
172
186
|
fix(fixer) {
|
|
173
187
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
174
|
-
const timestampObj = context
|
|
175
|
-
.getSourceCode()
|
|
176
|
-
.getText(node.callee.object);
|
|
177
188
|
return fixer.replaceText(node, `${timestampObj}.now()`);
|
|
178
189
|
}
|
|
179
190
|
return null;
|
|
@@ -204,9 +215,14 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
|
204
215
|
// Check if we have a Timestamp import before suggesting
|
|
205
216
|
if (timestampAliases.size > 0) {
|
|
206
217
|
const timestampName = Array.from(timestampAliases)[0];
|
|
218
|
+
const expressionText = sourceCode.getText(node);
|
|
207
219
|
context.report({
|
|
208
220
|
node,
|
|
209
221
|
messageId: 'preferTimestampNow',
|
|
222
|
+
data: {
|
|
223
|
+
expression: expressionText,
|
|
224
|
+
timestampAlias: timestampName,
|
|
225
|
+
},
|
|
210
226
|
fix(fixer) {
|
|
211
227
|
return fixer.replaceText(node, `${timestampName}.now()`);
|
|
212
228
|
},
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageIds = 'memoizeTransformValue' | 'memoizeTransformOnChange' | 'useCorrectHook' | 'missingDependencies';
|
|
3
|
+
export declare const enforceTransformMemoization: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceTransformMemoization = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'enforce-transform-memoization',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce memoization of adaptValue transformValue/transformOnChange so the adapted component receives stable handlers and avoids unnecessary re-renders.',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
memoizeTransformValue: 'transformValue is recreated on every render. Wrap it in useMemo (or reference a memoized helper) so adaptValue passes a stable transformer and avoids rerendering the adapted component.',
|
|
17
|
+
memoizeTransformOnChange: 'transformOnChange handler is recreated on every render. Wrap it in useCallback (or another memoized helper) so adaptValue passes a stable onChange and prevents extra renders and stale closures.',
|
|
18
|
+
useCorrectHook: '{{propName}} should be memoized with {{expectedHook}}. {{actualHook}} recreates a new transform on every render and hides intent; wrap the transform with {{expectedHook}} so React preserves its identity and avoids churn.',
|
|
19
|
+
missingDependencies: '{{hook}} for {{propName}} is missing {{deps}} in its dependency array. Include every external value the transform closes over so the memoized function stays in sync and does not capture stale data.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
const sourceCode = context.getSourceCode();
|
|
25
|
+
const scopeManager = sourceCode.scopeManager;
|
|
26
|
+
const adaptValueNames = new Set(['adaptValue']);
|
|
27
|
+
const memoizingHooks = new Set(['useMemo', 'useCallback']);
|
|
28
|
+
const stabilizingUtilities = new Set(['useEvent']);
|
|
29
|
+
const getPropertyName = (key) => {
|
|
30
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
31
|
+
return key.name;
|
|
32
|
+
}
|
|
33
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
34
|
+
typeof key.value === 'string') {
|
|
35
|
+
return key.value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
const isFunctionExpression = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
40
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
41
|
+
const unwrapExpression = (node) => {
|
|
42
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
43
|
+
node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
44
|
+
return unwrapExpression(node.expression);
|
|
45
|
+
}
|
|
46
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
47
|
+
return unwrapExpression(node.expression);
|
|
48
|
+
}
|
|
49
|
+
return node;
|
|
50
|
+
};
|
|
51
|
+
const getCalleeName = (callee) => {
|
|
52
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
53
|
+
return callee.name;
|
|
54
|
+
}
|
|
55
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
56
|
+
!callee.computed &&
|
|
57
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
58
|
+
return callee.property.name;
|
|
59
|
+
}
|
|
60
|
+
return '';
|
|
61
|
+
};
|
|
62
|
+
const isTopLevelScope = (scope) => scope.type === 'global' || scope.type === 'module';
|
|
63
|
+
const findVariableInScopeChain = (identifier) => {
|
|
64
|
+
if (!scopeManager)
|
|
65
|
+
return null;
|
|
66
|
+
let currentNode = identifier;
|
|
67
|
+
let scope = null;
|
|
68
|
+
while (currentNode && !scope) {
|
|
69
|
+
scope = scopeManager.acquire(currentNode, true);
|
|
70
|
+
currentNode = currentNode.parent ?? undefined;
|
|
71
|
+
}
|
|
72
|
+
if (!scope) {
|
|
73
|
+
scope = scopeManager.globalScope;
|
|
74
|
+
}
|
|
75
|
+
if (!scope)
|
|
76
|
+
return null;
|
|
77
|
+
let currentScope = scope;
|
|
78
|
+
while (currentScope) {
|
|
79
|
+
const variable = currentScope.variables.find((v) => v.name === identifier.name);
|
|
80
|
+
if (variable)
|
|
81
|
+
return variable;
|
|
82
|
+
currentScope = currentScope.upper;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
const isPropertyKeyIdentifier = (identifier) => identifier.parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
87
|
+
identifier.parent.property === identifier &&
|
|
88
|
+
!identifier.parent.computed;
|
|
89
|
+
const isScopeAncestor = (maybeAncestor, scope) => {
|
|
90
|
+
let current = scope.upper;
|
|
91
|
+
while (current) {
|
|
92
|
+
if (current === maybeAncestor)
|
|
93
|
+
return true;
|
|
94
|
+
current = current.upper;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
const collectExternalDependencies = (callback) => {
|
|
99
|
+
const deps = new Set();
|
|
100
|
+
if (!scopeManager)
|
|
101
|
+
return deps;
|
|
102
|
+
const rootScope = scopeManager.acquire(callback);
|
|
103
|
+
if (!rootScope)
|
|
104
|
+
return deps;
|
|
105
|
+
const walkScopes = (scope) => {
|
|
106
|
+
for (const ref of scope.references) {
|
|
107
|
+
const resolved = ref.resolved;
|
|
108
|
+
if (!resolved)
|
|
109
|
+
continue;
|
|
110
|
+
if (isPropertyKeyIdentifier(ref.identifier))
|
|
111
|
+
continue;
|
|
112
|
+
const resolvedScope = resolved.scope;
|
|
113
|
+
if (isScopeAncestor(resolvedScope, rootScope) &&
|
|
114
|
+
!isTopLevelScope(resolvedScope)) {
|
|
115
|
+
deps.add(ref.identifier.name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const child of scope.childScopes) {
|
|
119
|
+
walkScopes(child);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
walkScopes(rootScope);
|
|
123
|
+
return deps;
|
|
124
|
+
};
|
|
125
|
+
const extractDependencyNames = (depsArg) => {
|
|
126
|
+
const dependencyNames = new Set();
|
|
127
|
+
const addName = (name) => {
|
|
128
|
+
if (name)
|
|
129
|
+
dependencyNames.add(name);
|
|
130
|
+
};
|
|
131
|
+
const extractFromExpression = (expression) => {
|
|
132
|
+
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
133
|
+
return expression.name;
|
|
134
|
+
}
|
|
135
|
+
if (expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
136
|
+
!expression.computed) {
|
|
137
|
+
const objectExpr = expression.object;
|
|
138
|
+
if (objectExpr.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
139
|
+
return objectExpr.name;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
143
|
+
return extractFromExpression(expression.expression);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
for (const element of depsArg.elements) {
|
|
148
|
+
if (!element)
|
|
149
|
+
continue;
|
|
150
|
+
if (element.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
151
|
+
continue;
|
|
152
|
+
addName(extractFromExpression(element));
|
|
153
|
+
}
|
|
154
|
+
return dependencyNames;
|
|
155
|
+
};
|
|
156
|
+
const formatMissingDeps = (missing, hasArray) => {
|
|
157
|
+
if (missing.size === 0 && !hasArray) {
|
|
158
|
+
return 'a dependency array (use [] when the transform has no external values)';
|
|
159
|
+
}
|
|
160
|
+
if (missing.size === 0) {
|
|
161
|
+
return 'all external values referenced by the transform';
|
|
162
|
+
}
|
|
163
|
+
return Array.from(missing).join(', ');
|
|
164
|
+
};
|
|
165
|
+
const getVariableInitializer = (variable) => {
|
|
166
|
+
for (const def of variable.defs) {
|
|
167
|
+
if (def.type === 'Variable' && def.node.init) {
|
|
168
|
+
return def.node.init;
|
|
169
|
+
}
|
|
170
|
+
if (def.type === 'FunctionName') {
|
|
171
|
+
return def.node;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
const resolveValueForKey = (expression, key, depth = 0) => {
|
|
177
|
+
if (!expression || depth > 5)
|
|
178
|
+
return null;
|
|
179
|
+
const unwrapped = unwrapExpression(expression);
|
|
180
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
181
|
+
let found = null;
|
|
182
|
+
for (const prop of unwrapped.properties) {
|
|
183
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
184
|
+
const keyName = getPropertyName(prop.key);
|
|
185
|
+
if (keyName === key) {
|
|
186
|
+
found = prop.value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
190
|
+
const nested = resolveValueForKey(prop.argument, key, depth + 1);
|
|
191
|
+
if (nested) {
|
|
192
|
+
found = nested;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (found)
|
|
197
|
+
return found;
|
|
198
|
+
}
|
|
199
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
200
|
+
const variable = findVariableInScopeChain(unwrapped);
|
|
201
|
+
const init = variable ? getVariableInitializer(variable) : null;
|
|
202
|
+
if (init) {
|
|
203
|
+
return resolveValueForKey(init, key, depth + 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
207
|
+
!unwrapped.computed) {
|
|
208
|
+
const propertyName = getPropertyName(unwrapped.property);
|
|
209
|
+
if (!propertyName)
|
|
210
|
+
return null;
|
|
211
|
+
if (unwrapped.object.type !== utils_1.AST_NODE_TYPES.Super) {
|
|
212
|
+
const resolvedMember = resolveValueForKey(unwrapped.object, propertyName, depth + 1);
|
|
213
|
+
if (!resolvedMember)
|
|
214
|
+
return null;
|
|
215
|
+
if (propertyName !== key &&
|
|
216
|
+
resolvedMember.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
217
|
+
return resolveValueForKey(resolvedMember, key, depth + 1);
|
|
218
|
+
}
|
|
219
|
+
return resolvedMember;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
const resolveOptionsObject = (node) => {
|
|
225
|
+
if (!node)
|
|
226
|
+
return null;
|
|
227
|
+
const unwrapped = unwrapExpression(node);
|
|
228
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
229
|
+
return unwrapped;
|
|
230
|
+
}
|
|
231
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
232
|
+
const variable = findVariableInScopeChain(unwrapped);
|
|
233
|
+
const init = variable ? getVariableInitializer(variable) : null;
|
|
234
|
+
if (init && init.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
235
|
+
return init;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
239
|
+
!unwrapped.computed) {
|
|
240
|
+
const objectExpression = unwrapped.object.type === utils_1.AST_NODE_TYPES.Super
|
|
241
|
+
? null
|
|
242
|
+
: resolveOptionsObject(unwrapped.object);
|
|
243
|
+
if (objectExpression) {
|
|
244
|
+
const propertyName = getPropertyName(unwrapped.property);
|
|
245
|
+
if (propertyName) {
|
|
246
|
+
const nested = resolveValueForKey(objectExpression, propertyName);
|
|
247
|
+
if (nested && nested.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
248
|
+
return nested;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
const analyzeMemoization = (node, propName, expectedHook, seenIdentifiers) => {
|
|
256
|
+
const unwrapped = unwrapExpression(node);
|
|
257
|
+
if (isFunctionExpression(unwrapped)) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
messageId: propName === 'transformValue'
|
|
261
|
+
? 'memoizeTransformValue'
|
|
262
|
+
: 'memoizeTransformOnChange',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
messageId: propName === 'transformValue'
|
|
269
|
+
? 'memoizeTransformValue'
|
|
270
|
+
: 'memoizeTransformOnChange',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
274
|
+
const calleeName = getCalleeName(unwrapped.callee);
|
|
275
|
+
if (memoizingHooks.has(calleeName)) {
|
|
276
|
+
if ((expectedHook === 'useMemo' && calleeName === 'useCallback') ||
|
|
277
|
+
(expectedHook === 'useCallback' && calleeName === 'useMemo')) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
messageId: 'useCorrectHook',
|
|
281
|
+
data: {
|
|
282
|
+
propName,
|
|
283
|
+
expectedHook,
|
|
284
|
+
actualHook: calleeName,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const callback = unwrapped.arguments[0];
|
|
289
|
+
const depsArg = unwrapped.arguments[unwrapped.arguments.length - 1];
|
|
290
|
+
const externalDeps = callback && isFunctionExpression(callback)
|
|
291
|
+
? collectExternalDependencies(callback)
|
|
292
|
+
: new Set();
|
|
293
|
+
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
messageId: 'missingDependencies',
|
|
297
|
+
data: {
|
|
298
|
+
hook: calleeName,
|
|
299
|
+
propName,
|
|
300
|
+
deps: formatMissingDeps(externalDeps, false),
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (externalDeps.size > 0) {
|
|
305
|
+
const declaredDeps = extractDependencyNames(depsArg);
|
|
306
|
+
const missingDeps = new Set();
|
|
307
|
+
for (const dep of externalDeps) {
|
|
308
|
+
if (!declaredDeps.has(dep)) {
|
|
309
|
+
missingDeps.add(dep);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (missingDeps.size > 0) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
messageId: 'missingDependencies',
|
|
316
|
+
data: {
|
|
317
|
+
hook: calleeName,
|
|
318
|
+
propName,
|
|
319
|
+
deps: formatMissingDeps(missingDeps, true),
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { ok: true };
|
|
325
|
+
}
|
|
326
|
+
if (stabilizingUtilities.has(calleeName)) {
|
|
327
|
+
return { ok: true };
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
messageId: propName === 'transformValue'
|
|
332
|
+
? 'memoizeTransformValue'
|
|
333
|
+
: 'memoizeTransformOnChange',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
337
|
+
if (seenIdentifiers.has(unwrapped.name)) {
|
|
338
|
+
return { ok: true };
|
|
339
|
+
}
|
|
340
|
+
seenIdentifiers.add(unwrapped.name);
|
|
341
|
+
const variable = findVariableInScopeChain(unwrapped);
|
|
342
|
+
if (!variable) {
|
|
343
|
+
return { ok: true };
|
|
344
|
+
}
|
|
345
|
+
if (variable.defs.some((def) => def.type === 'Parameter') ||
|
|
346
|
+
isTopLevelScope(variable.scope)) {
|
|
347
|
+
return { ok: true };
|
|
348
|
+
}
|
|
349
|
+
const init = getVariableInitializer(variable);
|
|
350
|
+
if (!init) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
messageId: propName === 'transformValue'
|
|
354
|
+
? 'memoizeTransformValue'
|
|
355
|
+
: 'memoizeTransformOnChange',
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return analyzeMemoization(init, propName, expectedHook, seenIdentifiers);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
messageId: propName === 'transformValue'
|
|
363
|
+
? 'memoizeTransformValue'
|
|
364
|
+
: 'memoizeTransformOnChange',
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
const checkTransformProperty = (node, propName, expectedHook) => {
|
|
368
|
+
const result = analyzeMemoization(node, propName, expectedHook, new Set());
|
|
369
|
+
if (result.ok)
|
|
370
|
+
return;
|
|
371
|
+
context.report({
|
|
372
|
+
node,
|
|
373
|
+
messageId: result.messageId,
|
|
374
|
+
data: result.data,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
ImportDeclaration(node) {
|
|
379
|
+
const sourceValue = typeof node.source.value === 'string' ? node.source.value : '';
|
|
380
|
+
for (const specifier of node.specifiers) {
|
|
381
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
382
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
383
|
+
specifier.imported.name === 'adaptValue') {
|
|
384
|
+
adaptValueNames.add(specifier.local.name);
|
|
385
|
+
}
|
|
386
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
|
|
387
|
+
(specifier.local.name === 'adaptValue' ||
|
|
388
|
+
sourceValue.includes('adaptValue'))) {
|
|
389
|
+
adaptValueNames.add(specifier.local.name);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
CallExpression(node) {
|
|
394
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
395
|
+
!adaptValueNames.has(node.callee.name)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const optionsArg = node.arguments[0];
|
|
399
|
+
if (!optionsArg)
|
|
400
|
+
return;
|
|
401
|
+
const optionsObject = resolveOptionsObject(optionsArg);
|
|
402
|
+
if (!optionsObject)
|
|
403
|
+
return;
|
|
404
|
+
const transformValueNode = resolveValueForKey(optionsObject, 'transformValue');
|
|
405
|
+
if (transformValueNode) {
|
|
406
|
+
checkTransformProperty(transformValueNode, 'transformValue', 'useMemo');
|
|
407
|
+
}
|
|
408
|
+
const transformOnChangeNode = resolveValueForKey(optionsObject, 'transformOnChange');
|
|
409
|
+
if (transformOnChangeNode) {
|
|
410
|
+
checkTransformProperty(transformOnChangeNode, 'transformOnChange', 'useCallback');
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
//# sourceMappingURL=enforce-transform-memoization.js.map
|
|
@@ -5,8 +5,12 @@ const createRule_1 = require("../utils/createRule");
|
|
|
5
5
|
const FENCE = '```';
|
|
6
6
|
function isFenceLine(text, fenceIndex, indent) {
|
|
7
7
|
const lineEnd = text.indexOf('\n', fenceIndex);
|
|
8
|
-
const afterFence = lineEnd === -1
|
|
9
|
-
|
|
8
|
+
const afterFence = lineEnd === -1
|
|
9
|
+
? text.slice(fenceIndex + FENCE.length)
|
|
10
|
+
: text.slice(fenceIndex + FENCE.length, lineEnd);
|
|
11
|
+
return (afterFence.trim().length === 0 &&
|
|
12
|
+
text.slice(text.lastIndexOf('\n', fenceIndex - 1) + 1, fenceIndex) ===
|
|
13
|
+
indent);
|
|
10
14
|
}
|
|
11
15
|
function findClosingFence(text, startIndex, indent) {
|
|
12
16
|
let searchIndex = startIndex;
|
|
@@ -47,7 +51,7 @@ exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
|
|
|
47
51
|
}
|
|
48
52
|
return {
|
|
49
53
|
Program() {
|
|
50
|
-
const sourceCode = context.
|
|
54
|
+
const sourceCode = context.sourceCode;
|
|
51
55
|
const text = sourceCode.getText();
|
|
52
56
|
let index = 0;
|
|
53
57
|
while (index < text.length) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type NormalizedOptions = {
|
|
3
|
+
requiredPatterns: string[];
|
|
4
|
+
excludedPatterns: string[];
|
|
5
|
+
requiredTags: string[];
|
|
6
|
+
allowSplitHeaders: boolean;
|
|
7
|
+
headerTemplate: string | null;
|
|
8
|
+
ignoreGeneratedFiles: boolean;
|
|
9
|
+
generatedMarkers: string[];
|
|
10
|
+
excludedAtDirectives: string[];
|
|
11
|
+
};
|
|
12
|
+
type Options = [Partial<NormalizedOptions>];
|
|
13
|
+
type MessageIds = 'missingHeader' | 'duplicateHeader' | 'splitHeaderFragment';
|
|
14
|
+
export declare const RULE_NAME = "enforce-unique-cursor-headers";
|
|
15
|
+
export declare const enforceUniqueCursorHeaders: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
|
|
16
|
+
export {};
|