@blumintinc/eslint-plugin-blumint 1.16.2 → 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/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-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 +190 -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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
tooltipComponents?: string[];
|
|
4
|
+
portalComponents?: string[];
|
|
5
|
+
detectTooltipSuffix?: boolean;
|
|
6
|
+
detectPortalSuffix?: boolean;
|
|
7
|
+
}?
|
|
8
|
+
];
|
|
9
|
+
export declare const noPortalInsideTooltip: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"portalInsideTooltip", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
10
|
+
export {};
|