@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.1

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/no-direct-function-state.d.ts +8 -0
  15. package/lib/rules/no-direct-function-state.js +285 -0
  16. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  17. package/lib/rules/no-fill-template-mutation.js +324 -0
  18. package/lib/rules/no-hungarian.js +18 -26
  19. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  20. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  22. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  24. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  26. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  27. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  28. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  29. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  30. package/lib/rules/parallelize-loop-awaits.js +582 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  32. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  34. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  35. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  36. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  38. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  39. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  40. package/lib/rules/prefer-use-base62-id.js +483 -0
  41. package/lib/rules/prefer-use-theme.d.ts +1 -0
  42. package/lib/rules/prefer-use-theme.js +206 -0
  43. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  44. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  45. package/lib/rules/react-memoize-literals.js +106 -1
  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
@@ -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>;
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noFillTemplateMutation = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * The import path suffix that identifies the Algolia realtime fillTemplate
8
+ * function. Only calls imported from a path ending with this string are
9
+ * tracked, avoiding false positives from the unrelated marketing
10
+ * fillTemplate at src/pages/api/marketing/fillTemplate.ts.
11
+ */
12
+ const FILL_TEMPLATE_MODULE_SUFFIX = 'algoliaRealtime/fillTemplate';
13
+ /**
14
+ * String methods whose invocation on a fillTemplate result constitutes
15
+ * a structural mutation of the filled filter string.
16
+ */
17
+ const MUTATING_STRING_METHODS = new Set([
18
+ 'concat',
19
+ 'replace',
20
+ 'replaceAll',
21
+ 'trim',
22
+ 'trimStart',
23
+ 'trimEnd',
24
+ 'trimLeft',
25
+ 'trimRight',
26
+ 'toLowerCase',
27
+ 'toUpperCase',
28
+ 'toLocaleLowerCase',
29
+ 'toLocaleUpperCase',
30
+ 'slice',
31
+ 'substring',
32
+ 'substr',
33
+ 'padStart',
34
+ 'padEnd',
35
+ 'repeat',
36
+ 'normalize',
37
+ 'split',
38
+ ]);
39
+ exports.noFillTemplateMutation = (0, createRule_1.createRule)({
40
+ name: 'no-fill-template-mutation',
41
+ meta: {
42
+ type: 'problem',
43
+ docs: {
44
+ description: 'Disallow mutating the return value of fillTemplate() via string concatenation, template literals, or string method calls. The filled Algolia filter must be used verbatim so matchesTemplate() can regex-match it; post-fill modification silently breaks realtime hash parity.',
45
+ recommended: 'error',
46
+ },
47
+ fixable: undefined,
48
+ schema: [],
49
+ messages: {
50
+ noFillTemplateMutation: 'The return value of fillTemplate() must not be modified after the call. String operations (concatenation, template literals, string methods) on a filled filter break matchesTemplate() and cause silent realtime hash parity failures. To add conditions, create a new template variant in REALTIME_PREEMPTIVE_FILTER_TEMPLATES or PREEMPTIVE_FILTER_TEMPLATES instead.',
51
+ },
52
+ },
53
+ defaultOptions: [],
54
+ create(context) {
55
+ /**
56
+ * Local names (possibly aliased) that are bound to the Algolia
57
+ * fillTemplate function in the current file.
58
+ * e.g. `import { fillTemplate as fill }` → fill is tracked.
59
+ */
60
+ const filledFunctionNames = new Set();
61
+ /**
62
+ * Namespace import names whose .fillTemplate property is the tracked
63
+ * function. e.g. `import * as ft` → ft.fillTemplate is tracked.
64
+ */
65
+ const namespaceNames = new Set();
66
+ /**
67
+ * Variable names that hold a direct fillTemplate() call result.
68
+ * e.g. `const f = fillTemplate(...)` → f is a filled variable.
69
+ * Only single-level const/let bindings are tracked; no cross-function
70
+ * data-flow.
71
+ */
72
+ const filledVariables = new Set();
73
+ // ------------------------------------------------------------------ //
74
+ // Helpers
75
+ // ------------------------------------------------------------------ //
76
+ /**
77
+ * Returns true when node is a call to the tracked fillTemplate function,
78
+ * either as a named import (filledFunctionNames) or namespace member
79
+ * (namespaceNames.fillTemplate).
80
+ */
81
+ function isFillTemplateCall(node) {
82
+ if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
83
+ return false;
84
+ const { callee } = node;
85
+ // Direct call: fillTemplate(...) or aliased: fill(...)
86
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
87
+ filledFunctionNames.has(callee.name)) {
88
+ return true;
89
+ }
90
+ // Namespace call: ft.fillTemplate(...)
91
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
92
+ !callee.computed &&
93
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
94
+ namespaceNames.has(callee.object.name) &&
95
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
96
+ callee.property.name === 'fillTemplate') {
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ /**
102
+ * Returns true when node is either a direct fillTemplate() call or an
103
+ * identifier that holds a single-level fillTemplate() result.
104
+ */
105
+ function isFilledValue(node) {
106
+ if (isFillTemplateCall(node))
107
+ return true;
108
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
109
+ filledVariables.has(node.name)) {
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ /**
115
+ * Set of AST nodes that have already been reported to prevent
116
+ * double-reporting when a mutation is both the right-hand side of an
117
+ * AssignmentExpression and a BinaryExpression/TemplateLiteral.
118
+ */
119
+ const reportedNodes = new Set();
120
+ /**
121
+ * Reports a mutation violation on the given node.
122
+ */
123
+ function report(node) {
124
+ if (reportedNodes.has(node))
125
+ return;
126
+ reportedNodes.add(node);
127
+ context.report({ node, messageId: 'noFillTemplateMutation' });
128
+ }
129
+ /**
130
+ * Returns true when a TemplateLiteral node is used solely as an argument
131
+ * to a console method (console.log, console.warn, etc.), which is safe
132
+ * per the spec — logging output is not used as a filter value.
133
+ */
134
+ function isInsideConsoleCall(node) {
135
+ const parent = node.parent;
136
+ if (!parent)
137
+ return false;
138
+ if (parent.type !== utils_1.AST_NODE_TYPES.CallExpression)
139
+ return false;
140
+ const { callee } = parent;
141
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
142
+ return false;
143
+ const { object, property } = callee;
144
+ return (object.type === utils_1.AST_NODE_TYPES.Identifier &&
145
+ object.name === 'console' &&
146
+ property.type === utils_1.AST_NODE_TYPES.Identifier);
147
+ }
148
+ /**
149
+ * Checks whether a TemplateLiteral constitutes a mutation of a filled
150
+ * value. A bare `` `${f}` `` with exactly one expression and no
151
+ * surrounding text is considered non-mutating (it is a redundant wrap,
152
+ * not a structural addition). Any template that also contains non-empty
153
+ * quasi (literal text) or additional expressions is flagged.
154
+ */
155
+ function isTemplateLiteralMutation(node) {
156
+ const { expressions, quasis } = node;
157
+ // Find whether any expression is a filled value.
158
+ const filledExprIndices = [];
159
+ for (let i = 0; i < expressions.length; i++) {
160
+ if (isFilledValue(expressions[i])) {
161
+ filledExprIndices.push(i);
162
+ }
163
+ }
164
+ if (filledExprIndices.length === 0) {
165
+ return { isMutation: false, filledExprIndex: -1 };
166
+ }
167
+ // A bare `${f}` has one expression and all quasis are empty strings.
168
+ if (expressions.length === 1 && quasis.every((q) => q.value.raw === '')) {
169
+ return { isMutation: false, filledExprIndex: -1 };
170
+ }
171
+ // Any other pattern: more than one expression, or at least one quasi
172
+ // with non-empty text → mutation.
173
+ return { isMutation: true, filledExprIndex: filledExprIndices[0] };
174
+ }
175
+ // ------------------------------------------------------------------ //
176
+ // Visitor
177
+ // ------------------------------------------------------------------ //
178
+ return {
179
+ // ---- Import tracking ------------------------------------------- //
180
+ ImportDeclaration(node) {
181
+ const src = String(node.source.value);
182
+ if (!src.endsWith(FILL_TEMPLATE_MODULE_SUFFIX))
183
+ return;
184
+ for (const spec of node.specifiers) {
185
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
186
+ // Named import: `import { fillTemplate }` or
187
+ // `import { fillTemplate as fill }`
188
+ // spec.imported is the exported name; spec.local is the local alias.
189
+ const importedName = spec.imported.type === utils_1.AST_NODE_TYPES.Identifier
190
+ ? spec.imported.name
191
+ : '';
192
+ if (importedName === 'fillTemplate') {
193
+ filledFunctionNames.add(spec.local.name);
194
+ }
195
+ }
196
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
197
+ // Namespace import: `import * as ft`
198
+ namespaceNames.add(spec.local.name);
199
+ }
200
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
201
+ // Default import of the module itself, treat as fillTemplate
202
+ filledFunctionNames.add(spec.local.name);
203
+ }
204
+ }
205
+ },
206
+ // ---- Variable binding tracking ---------------------------------- //
207
+ VariableDeclarator(node) {
208
+ // Track: const f = fillTemplate(...)
209
+ if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
210
+ node.init &&
211
+ isFillTemplateCall(node.init)) {
212
+ filledVariables.add(node.id.name);
213
+ }
214
+ },
215
+ // ---- Binary expression: + / += ---------------------------------- //
216
+ BinaryExpression(node) {
217
+ if (node.operator !== '+')
218
+ return;
219
+ if (!isFilledValue(node.left) && !isFilledValue(node.right))
220
+ return;
221
+ // When this BinaryExpression is the right-hand side of a `=`
222
+ // assignment to a tracked variable, the AssignmentExpression visitor
223
+ // handles the report to prevent double errors.
224
+ const parent = node.parent;
225
+ if (parent &&
226
+ parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
227
+ parent.operator === '=' &&
228
+ parent.right === node &&
229
+ parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
230
+ filledVariables.has(parent.left.name)) {
231
+ return;
232
+ }
233
+ report(node);
234
+ },
235
+ // ---- Assignment expression: +=, and reassignment with + --------- //
236
+ AssignmentExpression(node) {
237
+ if (node.operator === '+=') {
238
+ // `f += '...'` — left must be a filled variable
239
+ if (node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
240
+ filledVariables.has(node.left.name)) {
241
+ report(node);
242
+ }
243
+ return;
244
+ }
245
+ if (node.operator === '=') {
246
+ // `f = f + '...'` or `f = \`${f}...\``
247
+ if (node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
248
+ filledVariables.has(node.left.name)) {
249
+ // Right side is a BinaryExpression involving the variable
250
+ if (node.right.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
251
+ node.right.operator === '+' &&
252
+ (isFilledValue(node.right.left) ||
253
+ isFilledValue(node.right.right))) {
254
+ report(node);
255
+ return;
256
+ }
257
+ // Right side is a TemplateLiteral involving the variable
258
+ if (node.right.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
259
+ const { isMutation } = isTemplateLiteralMutation(node.right);
260
+ if (isMutation) {
261
+ report(node);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ },
267
+ // ---- Template literals ------------------------------------------ //
268
+ TemplateLiteral(node) {
269
+ // Template literals used solely for console output are safe.
270
+ if (isInsideConsoleCall(node))
271
+ return;
272
+ const { isMutation } = isTemplateLiteralMutation(node);
273
+ if (!isMutation)
274
+ return;
275
+ // When this template literal is the right-hand side of an assignment
276
+ // to a tracked variable, the AssignmentExpression visitor reports it
277
+ // to avoid duplicate errors. Suppress here in that case.
278
+ const parent = node.parent;
279
+ if (parent &&
280
+ parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
281
+ parent.operator === '=' &&
282
+ parent.right === node &&
283
+ parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
284
+ filledVariables.has(parent.left.name)) {
285
+ return;
286
+ }
287
+ report(node);
288
+ },
289
+ // ---- String method calls ---------------------------------------- //
290
+ CallExpression(node) {
291
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
292
+ return;
293
+ const { object, property, computed } = node.callee;
294
+ if (computed)
295
+ return;
296
+ if (property.type !== utils_1.AST_NODE_TYPES.Identifier)
297
+ return;
298
+ if (!MUTATING_STRING_METHODS.has(property.name))
299
+ return;
300
+ // object is a filled value → mutation
301
+ if (isFilledValue(object)) {
302
+ report(node);
303
+ }
304
+ },
305
+ // ---- Array.join on arrays of filled values ----------------------- //
306
+ // Tracked via CallExpression: array.join(separator)
307
+ // We detect `[...].join(...)` where any element is a filled value.
308
+ // We also detect variables bound to arrays of filled values via map.
309
+ // (Simple cases: literal array in the join call.)
310
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
311
+ 'CallExpression[callee.property.name="join"]'(node) {
312
+ const callee = node.callee;
313
+ const obj = callee.object;
314
+ if (obj.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
315
+ const hasFilledElement = obj.elements.some((el) => el !== null && isFilledValue(el));
316
+ if (hasFilledElement) {
317
+ report(node);
318
+ }
319
+ }
320
+ },
321
+ };
322
+ },
323
+ });
324
+ //# sourceMappingURL=no-fill-template-mutation.js.map
@@ -353,36 +353,28 @@ exports.noHungarian = (0, createRule_1.createRule)({
353
353
  return true;
354
354
  }
355
355
  // Abbreviation markers (str, num, int, bool, arr, obj, fn, func) are
356
- // short enough that the raw-character boundary heuristic below can fire
357
- // on them as substrings inside real English words (e.g. "int" inside
358
- // "Mint", "str" inside "stream"). For these markers, require an exact
359
- // match against a full camelCase token so that "Mint" → ["Mint"] never
360
- // matches "int", while genuine Hungarian like intValue → ["int","Value"]
361
- // still does.
356
+ // short enough that a raw-character boundary check can fire on them as
357
+ // substrings inside real English words (e.g. "int" inside "Mint", "str"
358
+ // inside "stream"). Require an exact match against a full camelCase
359
+ // token so that "Mint" → ["Mint"] never matches "int", while genuine
360
+ // Hungarian like intValue → ["int","Value"] still does.
362
361
  if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
363
362
  const segments = splitCamelSegments(variableName);
364
363
  return segments.some((s) => s.toLowerCase() === normalizedMarker);
365
364
  }
366
- // Check for word boundaries to avoid matching substrings
367
- // For example, avoid matching "int" in "points" or "str" in "stream"
368
- const markerIndex = normalizedVarName.indexOf(normalizedMarker);
369
- if (markerIndex === -1) {
370
- return false;
371
- }
372
- const preMarkerPrefix = markerIndex > 0 ? variableName[markerIndex - 1] : '';
373
- const suffix = variableName[markerIndex + normalizedMarker.length] ?? '';
374
- // Ensure we have proper word boundaries
375
- // A word boundary is defined by:
376
- // 1. Start of string OR underscore OR capital letter before the marker
377
- // 2. End of string OR underscore OR capital letter after the marker
378
- const hasStartBoundary = markerIndex === 0 ||
379
- preMarkerPrefix === '_' ||
380
- /[A-Z]/.test(preMarkerPrefix) ||
381
- /[A-Z]/.test(variableName[markerIndex]);
382
- const hasEndBoundary = markerIndex + normalizedMarker.length === normalizedVarName.length ||
383
- suffix === '_' ||
384
- /[A-Z]/.test(suffix);
385
- return hasStartBoundary && hasEndBoundary;
365
+ // Full type-word markers (non-abbreviations: String, Number, Function,
366
+ // Array, Object, Boolean, …) are Hungarian only when they occupy the
367
+ // first or last camelCase segment — a genuine prefix or suffix that
368
+ // tags the entity's runtime type. The prefix/suffix character checks
369
+ // above already return `true` for those positions, so reaching this
370
+ // point means the marker sits in a middle segment of the identifier
371
+ // (e.g. cloud·Function·Registry, user·String·Name). A middle
372
+ // full-type-word qualifies a domain concept ("a registry of cloud
373
+ // functions") rather than redundantly encoding the entity's type.
374
+ // Accepting the resulting false negatives is the deliberate trade-off:
375
+ // middle-segment full-type-words are overwhelmingly domain vocabulary,
376
+ // not type tags.
377
+ return false;
386
378
  });
387
379
  }
388
380
  // Check if the identifier is a built-in method or imported from an external module