@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.
Files changed (51) hide show
  1. package/README.md +21 -0
  2. package/lib/index.js +64 -1
  3. package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
  4. package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
  5. package/lib/rules/enforce-cloud-function-id-length.js +104 -0
  6. package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
  7. package/lib/rules/enforce-is-prefix-validators.js +304 -0
  8. package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
  9. package/lib/rules/enforce-m3-sentence-case.js +430 -0
  10. package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
  11. package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
  12. package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
  13. package/lib/rules/enforce-types-directory-placement.js +276 -0
  14. package/lib/rules/logical-top-to-bottom-grouping.js +87 -19
  15. package/lib/rules/no-direct-function-state.d.ts +8 -0
  16. package/lib/rules/no-direct-function-state.js +285 -0
  17. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  18. package/lib/rules/no-fill-template-mutation.js +324 -0
  19. package/lib/rules/no-hungarian.js +11 -0
  20. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  21. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  22. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  23. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  24. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  25. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  26. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  27. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  28. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  29. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  30. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  31. package/lib/rules/parallelize-loop-awaits.js +582 -0
  32. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  33. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  34. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  35. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  36. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  37. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  38. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  39. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  40. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  41. package/lib/rules/prefer-use-base62-id.js +483 -0
  42. package/lib/rules/prefer-use-theme.d.ts +1 -0
  43. package/lib/rules/prefer-use-theme.js +206 -0
  44. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  45. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  46. package/lib/rules/require-props-composition.d.ts +10 -0
  47. package/lib/rules/require-props-composition.js +433 -0
  48. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  49. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  50. package/package.json +1 -1
  51. 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
- if (comments.length === 0) {
675
- return statement.range[0];
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 comments[0].range[0];
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.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
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
- let usageIndex = -1;
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,8 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type Options = [
3
+ {
4
+ functionPatterns?: string[];
5
+ }
6
+ ];
7
+ export declare const noDirectFunctionState: TSESLint.RuleModule<"noDirectFunctionState", Options, TSESLint.RuleListener>;
8
+ export {};
@@ -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>;