@blumintinc/eslint-plugin-blumint 1.16.1 → 1.17.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 +21 -0
- package/lib/index.js +64 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
- package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
- package/lib/rules/enforce-cloud-function-id-length.js +104 -0
- package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
- package/lib/rules/enforce-is-prefix-validators.js +304 -0
- package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
- package/lib/rules/enforce-m3-sentence-case.js +430 -0
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
- package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
- package/lib/rules/enforce-types-directory-placement.js +276 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +87 -19
- package/lib/rules/no-direct-function-state.d.ts +8 -0
- package/lib/rules/no-direct-function-state.js +285 -0
- package/lib/rules/no-fill-template-mutation.d.ts +1 -0
- package/lib/rules/no-fill-template-mutation.js +324 -0
- package/lib/rules/no-hungarian.js +11 -0
- package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
- package/lib/rules/no-portal-inside-tooltip.js +219 -0
- package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
- package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
- package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
- package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
- package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
- package/lib/rules/no-stablehash-react-nodes.js +325 -0
- package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
- package/lib/rules/parallelize-loop-awaits.js +582 -0
- package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
- package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
- package/lib/rules/prefer-spread-over-reassembly.js +401 -0
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
- package/lib/rules/prefer-use-base62-id.d.ts +8 -0
- package/lib/rules/prefer-use-base62-id.js +483 -0
- package/lib/rules/prefer-use-theme.d.ts +1 -0
- package/lib/rules/prefer-use-theme.js +206 -0
- package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
- package/lib/rules/prefer-utility-function-own-file.js +505 -0
- package/lib/rules/require-props-composition.d.ts +10 -0
- package/lib/rules/require-props-composition.js +433 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
- package/package.json +1 -1
- package/release-manifest.json +212 -0
|
@@ -368,6 +368,18 @@ function statementReferencesAny(statement, names) {
|
|
|
368
368
|
}
|
|
369
369
|
return false;
|
|
370
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Find the index of the first statement that references any of the given names,
|
|
373
|
+
* searching forward from afterIndex.
|
|
374
|
+
*/
|
|
375
|
+
function findFirstUsageIndex(body, names, afterIndex) {
|
|
376
|
+
for (let cursor = afterIndex; cursor < body.length; cursor += 1) {
|
|
377
|
+
if (statementReferencesAny(body[cursor], names)) {
|
|
378
|
+
return cursor;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return -1;
|
|
382
|
+
}
|
|
371
383
|
function collectAssignedNamesFromPattern(target, names) {
|
|
372
384
|
if (TYPE_EXPRESSION_WRAPPERS.has(target.type) &&
|
|
373
385
|
'expression' in target) {
|
|
@@ -671,10 +683,13 @@ function findEarliestSafeIndex(body, startIndex, dependencies, { allowHooks }) {
|
|
|
671
683
|
}
|
|
672
684
|
function getStartWithComments(statement, sourceCode) {
|
|
673
685
|
const comments = sourceCode.getCommentsBefore(statement);
|
|
674
|
-
|
|
675
|
-
|
|
686
|
+
const start = comments.length === 0 ? statement.range[0] : comments[0].range[0];
|
|
687
|
+
const text = sourceCode.getText();
|
|
688
|
+
let cursor = start - 1;
|
|
689
|
+
while (cursor >= 0 && (text[cursor] === ' ' || text[cursor] === '\t')) {
|
|
690
|
+
cursor -= 1;
|
|
676
691
|
}
|
|
677
|
-
return
|
|
692
|
+
return cursor + 1;
|
|
678
693
|
}
|
|
679
694
|
function getNextStart(body, index, parent, sourceCode) {
|
|
680
695
|
const nextStatement = body[index + 1];
|
|
@@ -907,42 +922,95 @@ function isSiblingSourceDerivation(statement, sourceNodes, sourceDeclarators) {
|
|
|
907
922
|
return !sourceDeclarators.has(name);
|
|
908
923
|
});
|
|
909
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Restrict late-declaration candidates to simple variables with at most an Identifier or
|
|
927
|
+
* Literal initializer. This ensures they are pure values that do not have side effects or
|
|
928
|
+
* change execution order when moved closer to their usage. More complex initializers are
|
|
929
|
+
* excluded to maintain temporal safety.
|
|
930
|
+
*/
|
|
931
|
+
function isLateDeclarationCandidate(statement) {
|
|
932
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
933
|
+
statement.declarations.length !== 1) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const [declarator] = statement.declarations;
|
|
937
|
+
return (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
938
|
+
(!declarator.init ||
|
|
939
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
940
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.Literal));
|
|
941
|
+
}
|
|
942
|
+
const LOOP_TYPES = new Set([
|
|
943
|
+
utils_1.AST_NODE_TYPES.ForStatement,
|
|
944
|
+
utils_1.AST_NODE_TYPES.ForInStatement,
|
|
945
|
+
utils_1.AST_NODE_TYPES.ForOfStatement,
|
|
946
|
+
utils_1.AST_NODE_TYPES.WhileStatement,
|
|
947
|
+
utils_1.AST_NODE_TYPES.DoWhileStatement,
|
|
948
|
+
]);
|
|
949
|
+
/**
|
|
950
|
+
* Treat a loop as an ordering barrier for late-declaration moves when it mutates any
|
|
951
|
+
* tracked variable inside the loop body.
|
|
952
|
+
*
|
|
953
|
+
* Variables that are declared before a loop and mutated inside it (accumulators,
|
|
954
|
+
* counters, collectors) must remain before the loop so their declaration is visible
|
|
955
|
+
* across all iterations. Moving them inside would reset them on every iteration,
|
|
956
|
+
* changing program semantics. This guard applies regardless of whether the variable
|
|
957
|
+
* is also read after the loop.
|
|
958
|
+
*
|
|
959
|
+
* Assumptions:
|
|
960
|
+
* - nameSet contains the name(s) being tracked for late declaration.
|
|
961
|
+
* - body is the array of statements in the current block.
|
|
962
|
+
* - usageIndex is the index of the first statement that references the variable(s).
|
|
963
|
+
*/
|
|
964
|
+
function isMutatedInLoop(body, usageIndex, nameSet) {
|
|
965
|
+
const firstUsage = body[usageIndex];
|
|
966
|
+
if (!firstUsage) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (!LOOP_TYPES.has(firstUsage.type)) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
return statementMutatesAny(firstUsage, nameSet);
|
|
973
|
+
}
|
|
910
974
|
function handleLateDeclarations(ruleContext, body, parent) {
|
|
911
975
|
const { sourceCode } = ruleContext;
|
|
912
976
|
body.forEach((statement, index) => {
|
|
913
|
-
if (statement
|
|
914
|
-
statement.declarations.length !== 1) {
|
|
977
|
+
if (!isLateDeclarationCandidate(statement)) {
|
|
915
978
|
return;
|
|
916
979
|
}
|
|
917
980
|
const [declarator] = statement.declarations;
|
|
918
|
-
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
919
|
-
(declarator.init &&
|
|
920
|
-
declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
921
|
-
declarator.init.type !== utils_1.AST_NODE_TYPES.Literal)) {
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
981
|
const name = declarator.id.name;
|
|
925
982
|
const dependencies = new Set();
|
|
926
983
|
if (declarator.init && declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
927
984
|
dependencies.add(declarator.init.name);
|
|
928
985
|
}
|
|
929
986
|
const nameSet = new Set([name]);
|
|
930
|
-
|
|
931
|
-
for (let cursor = index + 1; cursor < body.length; cursor += 1) {
|
|
932
|
-
if (statementReferencesAny(body[cursor], nameSet)) {
|
|
933
|
-
usageIndex = cursor;
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
987
|
+
const usageIndex = findFirstUsageIndex(body, nameSet, index + 1);
|
|
937
988
|
if (usageIndex === -1 || usageIndex <= index + 1) {
|
|
938
989
|
return;
|
|
939
990
|
}
|
|
991
|
+
// Loop mutations create a dependency barrier: declarations that precede loops and are
|
|
992
|
+
// mutated inside them cannot be safely moved, as the declaration must be visible across
|
|
993
|
+
// all iterations. Prevent false positives for this pattern.
|
|
994
|
+
if (isMutatedInLoop(body, usageIndex, nameSet)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
940
997
|
const intervening = body.slice(index + 1, usageIndex);
|
|
941
998
|
// Only move across pure declarations that do not mention the placeholder or its initializer dependencies to avoid changing closure timing or TDZ behavior.
|
|
942
|
-
const crossesImpureOrTracked = intervening.some((stmt) => {
|
|
999
|
+
const crossesImpureOrTracked = intervening.some((stmt, i) => {
|
|
943
1000
|
if (!isPureDeclaration(stmt, { allowHooks: false })) {
|
|
944
1001
|
return true;
|
|
945
1002
|
}
|
|
1003
|
+
// Do not hop over another declaration that is used at the same index or earlier
|
|
1004
|
+
// if it is also a candidate for being moved.
|
|
1005
|
+
// This prevents circular swapping of related declarations (like resolve/reject pairs).
|
|
1006
|
+
if (isLateDeclarationCandidate(stmt)) {
|
|
1007
|
+
const declaredNames = getDeclaredNames(stmt);
|
|
1008
|
+
const firstUsageOfIntervening = findFirstUsageIndex(body, declaredNames, index + 1 + i + 1);
|
|
1009
|
+
if (firstUsageOfIntervening !== -1 &&
|
|
1010
|
+
firstUsageOfIntervening <= usageIndex) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
946
1014
|
if (statementDeclaresAny(stmt, nameSet) ||
|
|
947
1015
|
statementMutatesAny(stmt, nameSet)) {
|
|
948
1016
|
return true;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noDirectFunctionState = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_FUNCTION_PATTERNS = [
|
|
7
|
+
'callback',
|
|
8
|
+
'handler',
|
|
9
|
+
'fn',
|
|
10
|
+
'func',
|
|
11
|
+
'on[A-Z].*',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if the TSTypeAnnotation node (from useState's type parameter)
|
|
15
|
+
* represents a function type — either directly or as part of a union with
|
|
16
|
+
* null/undefined. We check purely syntactically; no type-checker required.
|
|
17
|
+
*/
|
|
18
|
+
function isFunctionTypeAnnotation(typeNode) {
|
|
19
|
+
switch (typeNode.type) {
|
|
20
|
+
case utils_1.AST_NODE_TYPES.TSFunctionType:
|
|
21
|
+
case utils_1.AST_NODE_TYPES.TSConstructorType:
|
|
22
|
+
return true;
|
|
23
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
24
|
+
// Union like `(() => void) | null` — any member being a function type suffices
|
|
25
|
+
return typeNode.types.some(isFunctionTypeAnnotation);
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether a useState call expression has a type parameter that
|
|
32
|
+
* includes a function type, e.g. useState<(() => void) | null>(null).
|
|
33
|
+
*/
|
|
34
|
+
function useStateHasFunctionTypeParam(callNode) {
|
|
35
|
+
const typeParams = callNode.typeParameters;
|
|
36
|
+
if (!typeParams || typeParams.params.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return isFunctionTypeAnnotation(typeParams.params[0]);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns true when the AST node is a safe value to pass to a setter — i.e.,
|
|
43
|
+
* NOT a bare identifier or member expression that could be a function reference.
|
|
44
|
+
* Arrow/function expressions are always safe (they are intentional).
|
|
45
|
+
* Literals, null, undefined, call expressions, arrays, objects are all safe.
|
|
46
|
+
*/
|
|
47
|
+
function isDefinitelySafeArg(node) {
|
|
48
|
+
switch (node.type) {
|
|
49
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
50
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
51
|
+
// Inline function/arrow — always intentional (updater or thunk)
|
|
52
|
+
return true;
|
|
53
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
54
|
+
// Literal values (numbers, strings, booleans, null, regex)
|
|
55
|
+
return true;
|
|
56
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
57
|
+
return true;
|
|
58
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
59
|
+
// `undefined` is safe; generic identifiers may be function refs
|
|
60
|
+
return node.name === 'undefined';
|
|
61
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
62
|
+
// `void 0`, `!flag`, `typeof x` etc. — all non-function values
|
|
63
|
+
return true;
|
|
64
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
65
|
+
// `a + b`, `a * b`, etc. — never callable
|
|
66
|
+
return true;
|
|
67
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
68
|
+
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
69
|
+
// Call/new expressions — return value unknown without types; skip (no FP)
|
|
70
|
+
return true;
|
|
71
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
72
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
73
|
+
return true;
|
|
74
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
75
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
76
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
77
|
+
// Unwrap type assertions and recurse
|
|
78
|
+
return isDefinitelySafeArg(node.expression);
|
|
79
|
+
default:
|
|
80
|
+
// MemberExpression, Identifier (non-undefined), etc. are NOT definitely safe
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Checks whether an identifier name matches any of the function-naming patterns
|
|
86
|
+
* (e.g. onClose, handler, fn, callback).
|
|
87
|
+
*/
|
|
88
|
+
function matchesFunctionPattern(name, patterns) {
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
try {
|
|
91
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
92
|
+
if (regex.test(name)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Ignore invalid regex patterns
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extracts the identifier name from an argument node for pattern matching.
|
|
104
|
+
* For MemberExpression like `obj.handler`, returns `handler`.
|
|
105
|
+
* For Identifier like `myCallback`, returns `myCallback`.
|
|
106
|
+
*/
|
|
107
|
+
function getArgName(node) {
|
|
108
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
109
|
+
return node.name;
|
|
110
|
+
}
|
|
111
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
112
|
+
const prop = node.property;
|
|
113
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Identifier && !node.computed) {
|
|
114
|
+
return prop.name;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Walks up the scope to find if an identifier is bound (in scope) to a
|
|
121
|
+
* function — an arrow function expression or function expression/declaration.
|
|
122
|
+
* This covers: `const x = () => ...` and `function x() {...}`.
|
|
123
|
+
*/
|
|
124
|
+
function isIdentifierBoundToFunction(name, scope) {
|
|
125
|
+
let currentScope = scope;
|
|
126
|
+
while (currentScope) {
|
|
127
|
+
for (const variable of currentScope.variables) {
|
|
128
|
+
if (variable.name !== name)
|
|
129
|
+
continue;
|
|
130
|
+
for (const def of variable.defs) {
|
|
131
|
+
if (def.type === 'Variable' &&
|
|
132
|
+
def.node.init &&
|
|
133
|
+
(def.node.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
134
|
+
def.node.init.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (def.type === 'FunctionName' ||
|
|
138
|
+
def.type === 'ImplicitGlobalVariable') {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
currentScope = currentScope.upper;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
exports.noDirectFunctionState = (0, createRule_1.createRule)({
|
|
148
|
+
name: 'no-direct-function-state',
|
|
149
|
+
meta: {
|
|
150
|
+
type: 'problem',
|
|
151
|
+
docs: {
|
|
152
|
+
description: 'Prevent passing a function directly to a useState setter — React will invoke it as a functional updater instead of storing it. Wrap in a thunk: setState(() => fn).',
|
|
153
|
+
recommended: 'error',
|
|
154
|
+
},
|
|
155
|
+
fixable: 'code',
|
|
156
|
+
schema: [
|
|
157
|
+
{
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
functionPatterns: {
|
|
161
|
+
type: 'array',
|
|
162
|
+
items: { type: 'string' },
|
|
163
|
+
default: DEFAULT_FUNCTION_PATTERNS,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
messages: {
|
|
170
|
+
noDirectFunctionState: 'What\'s wrong: "{{argText}}" is passed directly to "{{setterName}}", but React invokes a function argument as a functional updater (prev => next) instead of storing it. ' +
|
|
171
|
+
'Why it matters: The function will be called with the previous state value and its return value stored — a silent bug with no error. ' +
|
|
172
|
+
'How to fix: Wrap it in a thunk so React stores the function as a value: {{setterName}}(() => {{argText}})',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
defaultOptions: [{ functionPatterns: DEFAULT_FUNCTION_PATTERNS }],
|
|
176
|
+
create(context) {
|
|
177
|
+
const options = context.options[0] ?? {};
|
|
178
|
+
const functionPatterns = options.functionPatterns ?? DEFAULT_FUNCTION_PATTERNS;
|
|
179
|
+
/**
|
|
180
|
+
* Maps setter-variable names to whether the corresponding useState has
|
|
181
|
+
* an explicit function type parameter. This is populated as we encounter
|
|
182
|
+
* useState array-destructuring declarations.
|
|
183
|
+
*/
|
|
184
|
+
const setterFunctionTyped = new Map();
|
|
185
|
+
return {
|
|
186
|
+
VariableDeclarator(node) {
|
|
187
|
+
// Look for `const [state, setter] = useState<T>(...)` or
|
|
188
|
+
// `const [state, setter] = React.useState<T>(...)`.
|
|
189
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.ArrayPattern ||
|
|
190
|
+
!node.init ||
|
|
191
|
+
node.init.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const callNode = node.init;
|
|
195
|
+
const callee = callNode.callee;
|
|
196
|
+
const isUseStateCall = (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
197
|
+
callee.name === 'useState') ||
|
|
198
|
+
(callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
199
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
200
|
+
callee.property.name === 'useState');
|
|
201
|
+
if (!isUseStateCall)
|
|
202
|
+
return;
|
|
203
|
+
// The setter is the second element of the destructured array
|
|
204
|
+
const elements = node.id.elements;
|
|
205
|
+
if (elements.length < 2)
|
|
206
|
+
return;
|
|
207
|
+
const setterElement = elements[1];
|
|
208
|
+
if (!setterElement ||
|
|
209
|
+
setterElement.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const setterName = setterElement.name;
|
|
213
|
+
const hasFunctionType = useStateHasFunctionTypeParam(callNode);
|
|
214
|
+
setterFunctionTyped.set(setterName, hasFunctionType);
|
|
215
|
+
},
|
|
216
|
+
CallExpression(node) {
|
|
217
|
+
// We are looking for calls like `setter(arg)` where:
|
|
218
|
+
// 1. setter is known (tracked from useState destructuring), AND
|
|
219
|
+
// - the useState type is function-typed, OR
|
|
220
|
+
// - the arg name matches a function pattern, OR
|
|
221
|
+
// - the arg is bound to a function in scope
|
|
222
|
+
// 2. The arg is NOT already a safe value (arrow/function expr, literal, etc.)
|
|
223
|
+
const callee = node.callee;
|
|
224
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
225
|
+
return;
|
|
226
|
+
const setterName = callee.name;
|
|
227
|
+
// Only flag calls to tracked setters
|
|
228
|
+
if (!setterFunctionTyped.has(setterName))
|
|
229
|
+
return;
|
|
230
|
+
// Only consider single-argument calls (setters take exactly one value arg)
|
|
231
|
+
if (node.arguments.length !== 1)
|
|
232
|
+
return;
|
|
233
|
+
const arg = node.arguments[0];
|
|
234
|
+
// SpreadElement is not a plain expression; skip
|
|
235
|
+
if (arg.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
236
|
+
return;
|
|
237
|
+
// If arg is a definitely-safe type (inline arrow, literal, undefined, etc.),
|
|
238
|
+
// skip without further checks
|
|
239
|
+
if (isDefinitelySafeArg(arg))
|
|
240
|
+
return;
|
|
241
|
+
// At this point arg is an Identifier (non-undefined) or MemberExpression.
|
|
242
|
+
// Decide whether it is a function reference.
|
|
243
|
+
const isFunctionTypedState = setterFunctionTyped.get(setterName) === true;
|
|
244
|
+
if (isFunctionTypedState) {
|
|
245
|
+
// Type annotation says the state holds a function — any bare
|
|
246
|
+
// identifier or member expression is suspect.
|
|
247
|
+
reportAndFix(node, arg, setterName, context);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// No explicit function type. Fall back to heuristic: name pattern match
|
|
251
|
+
// or scope-level binding to a function.
|
|
252
|
+
const argName = getArgName(arg);
|
|
253
|
+
if (argName && matchesFunctionPattern(argName, functionPatterns)) {
|
|
254
|
+
reportAndFix(node, arg, setterName, context);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Check if the identifier is bound to a function in scope
|
|
258
|
+
if (arg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
259
|
+
arg.name !== 'undefined') {
|
|
260
|
+
const scope = context.getScope();
|
|
261
|
+
if (isIdentifierBoundToFunction(arg.name, scope)) {
|
|
262
|
+
reportAndFix(node, arg, setterName, context);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
function reportAndFix(callNode, arg, setterName, context) {
|
|
271
|
+
const sourceCode = context.getSourceCode();
|
|
272
|
+
const argText = sourceCode.getText(arg);
|
|
273
|
+
context.report({
|
|
274
|
+
node: callNode,
|
|
275
|
+
messageId: 'noDirectFunctionState',
|
|
276
|
+
data: {
|
|
277
|
+
argText,
|
|
278
|
+
setterName,
|
|
279
|
+
},
|
|
280
|
+
fix(fixer) {
|
|
281
|
+
return fixer.replaceText(arg, `() => ${argText}`);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=no-direct-function-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noFillTemplateMutation: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noFillTemplateMutation", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|