@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
@@ -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
@@ -352,6 +352,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
352
352
  /[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
353
353
  return true;
354
354
  }
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.
362
+ if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
363
+ const segments = splitCamelSegments(variableName);
364
+ return segments.some((s) => s.toLowerCase() === normalizedMarker);
365
+ }
355
366
  // Check for word boundaries to avoid matching substrings
356
367
  // For example, avoid matching "int" in "points" or "str" in "stream"
357
368
  const markerIndex = normalizedVarName.indexOf(normalizedMarker);
@@ -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 {};
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noPortalInsideTooltip = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ const DEFAULT_TOOLTIP_COMPONENTS = [
6
+ 'Tooltip',
7
+ 'WYSIWYGTooltip',
8
+ 'MatchPayoutTooltip',
9
+ 'TeamDisplayTooltip',
10
+ 'OptionalTooltip',
11
+ 'ComingSoonTooltip',
12
+ 'TooltipDynamicDelay',
13
+ 'ValidationTooltip',
14
+ ];
15
+ const DEFAULT_PORTAL_COMPONENTS = [
16
+ 'Dialog',
17
+ 'Drawer',
18
+ 'Menu',
19
+ 'Popover',
20
+ 'Portal',
21
+ 'Modal',
22
+ 'WizardPortal',
23
+ 'DialogCentered',
24
+ 'AlertDialog',
25
+ ];
26
+ const PORTAL_SUFFIXES = ['Portal', 'Dialog', 'Drawer', 'Menu', 'Popover'];
27
+ /**
28
+ * Extracts the string name from a JSX element's opening tag name expression.
29
+ * Returns null for member expressions (e.g. Foo.Bar) since they're out of scope.
30
+ */
31
+ function getJsxElementName(nameExpr) {
32
+ if (nameExpr.type === 'JSXIdentifier') {
33
+ return nameExpr.name;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Recursively searches a node's JSX descendant tree for portal components,
39
+ * including those nested inside expression containers (logical/conditional
40
+ * expressions), fragments, and regular elements.
41
+ *
42
+ * Returns the first portal node found, or null if none.
43
+ */
44
+ function findPortalInDescendants(node, isPortalComponent) {
45
+ if (node.type === 'JSXElement') {
46
+ const name = getJsxElementName(node.openingElement.name);
47
+ if (name !== null && isPortalComponent(name)) {
48
+ return node;
49
+ }
50
+ // Recurse into JSXElement children
51
+ for (const child of node.children) {
52
+ const found = findPortalInDescendants(child, isPortalComponent);
53
+ if (found)
54
+ return found;
55
+ }
56
+ return null;
57
+ }
58
+ if (node.type === 'JSXFragment') {
59
+ // Treat fragments as transparent — recurse into their children
60
+ for (const child of node.children) {
61
+ const found = findPortalInDescendants(child, isPortalComponent);
62
+ if (found)
63
+ return found;
64
+ }
65
+ return null;
66
+ }
67
+ if (node.type === 'JSXExpressionContainer') {
68
+ return findPortalInExpression(node.expression, isPortalComponent);
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Descends into expression nodes (logical, conditional) looking for portal
74
+ * JSX elements. Call expressions and other dynamic shapes are out of scope
75
+ * and silently produce no match (avoiding false positives).
76
+ */
77
+ function findPortalInExpression(expr, isPortalComponent) {
78
+ if (expr.type === 'JSXElement') {
79
+ return findPortalInDescendants(expr, isPortalComponent);
80
+ }
81
+ if (expr.type === 'JSXFragment') {
82
+ return findPortalInDescendants(expr, isPortalComponent);
83
+ }
84
+ if (expr.type === 'LogicalExpression') {
85
+ // Both sides could be JSX — check the right operand (the render branch)
86
+ // and also the left in case of || chaining
87
+ const rightResult = findPortalInExpression(expr.right, isPortalComponent);
88
+ if (rightResult)
89
+ return rightResult;
90
+ return findPortalInExpression(expr.left, isPortalComponent);
91
+ }
92
+ if (expr.type === 'ConditionalExpression') {
93
+ const consequentResult = findPortalInExpression(expr.consequent, isPortalComponent);
94
+ if (consequentResult)
95
+ return consequentResult;
96
+ return findPortalInExpression(expr.alternate, isPortalComponent);
97
+ }
98
+ if (expr.type === 'Identifier') {
99
+ // Handle {Portal} — an Identifier whose name matches a portal component name
100
+ if (isPortalComponent(expr.name)) {
101
+ return expr;
102
+ }
103
+ }
104
+ // CallExpression, TemplateLiteral, etc. are out of scope — return null
105
+ return null;
106
+ }
107
+ exports.noPortalInsideTooltip = (0, createRule_1.createRule)({
108
+ name: 'no-portal-inside-tooltip',
109
+ meta: {
110
+ type: 'problem',
111
+ docs: {
112
+ description: 'Disallow portal-rendering components (Dialog, Drawer, Menu, Popover, Portal, Modal) inside Tooltip wrapper components. React Portals preserve React-tree event bubbling, so a portal nested under a Tooltip leaves the tooltip orphaned over the modal.',
113
+ recommended: 'error',
114
+ },
115
+ schema: [
116
+ {
117
+ type: 'object',
118
+ properties: {
119
+ tooltipComponents: {
120
+ type: 'array',
121
+ items: { type: 'string' },
122
+ description: 'Additional tooltip wrapper component names to detect.',
123
+ },
124
+ portalComponents: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description: 'Additional portal-rendering component names to detect.',
128
+ },
129
+ detectTooltipSuffix: {
130
+ type: 'boolean',
131
+ default: true,
132
+ description: 'When true, any component whose name ends in "Tooltip" is treated as a tooltip wrapper.',
133
+ },
134
+ detectPortalSuffix: {
135
+ type: 'boolean',
136
+ default: true,
137
+ description: 'When true, any component whose name ends in Portal, Dialog, Drawer, Menu, or Popover is treated as a portal.',
138
+ },
139
+ },
140
+ additionalProperties: false,
141
+ },
142
+ ],
143
+ messages: {
144
+ portalInsideTooltip: "Portal-rendering component '{{portalName}}' must not be nested inside Tooltip wrapper '{{tooltipName}}'. React Portals preserve React-tree event bubbling, so the portal's modal Backdrop becomes a logical descendant of the tooltip wrapper — preventing onMouseLeave from firing and leaving the tooltip orphaned over the modal. Hoist the portal out of the tooltip (use TooltipChipTrigger or render the portal as a sibling).",
145
+ },
146
+ fixable: undefined,
147
+ },
148
+ defaultOptions: [{}],
149
+ create(context) {
150
+ const options = context.options[0] ?? {};
151
+ const detectTooltipSuffix = options.detectTooltipSuffix !== false;
152
+ const detectPortalSuffix = options.detectPortalSuffix !== false;
153
+ // Build the tooltip component name set
154
+ const tooltipSet = new Set(DEFAULT_TOOLTIP_COMPONENTS);
155
+ if (options.tooltipComponents) {
156
+ for (const name of options.tooltipComponents) {
157
+ tooltipSet.add(name);
158
+ }
159
+ }
160
+ // Build the portal component name set
161
+ const portalSet = new Set(DEFAULT_PORTAL_COMPONENTS);
162
+ if (options.portalComponents) {
163
+ for (const name of options.portalComponents) {
164
+ portalSet.add(name);
165
+ }
166
+ }
167
+ const isTooltipComponent = (name) => {
168
+ if (tooltipSet.has(name))
169
+ return true;
170
+ if (detectTooltipSuffix && name.endsWith('Tooltip'))
171
+ return true;
172
+ return false;
173
+ };
174
+ const isPortalComponent = (name) => {
175
+ if (portalSet.has(name))
176
+ return true;
177
+ if (detectPortalSuffix) {
178
+ for (const suffix of PORTAL_SUFFIXES) {
179
+ if (name.endsWith(suffix) && name !== suffix)
180
+ return true;
181
+ }
182
+ }
183
+ return false;
184
+ };
185
+ return {
186
+ JSXElement(node) {
187
+ const tooltipName = getJsxElementName(node.openingElement.name);
188
+ if (tooltipName === null || !isTooltipComponent(tooltipName)) {
189
+ return;
190
+ }
191
+ // Only walk the tooltip's children (not its attributes like `title`)
192
+ for (const child of node.children) {
193
+ const portalNode = findPortalInDescendants(child, isPortalComponent);
194
+ if (portalNode) {
195
+ let portalName = 'Portal';
196
+ if (portalNode.type === 'JSXElement') {
197
+ portalName =
198
+ getJsxElementName(portalNode.openingElement.name) ?? 'Portal';
199
+ }
200
+ else if (portalNode.type === 'Identifier') {
201
+ portalName = portalNode.name;
202
+ }
203
+ context.report({
204
+ node: portalNode,
205
+ messageId: 'portalInsideTooltip',
206
+ data: {
207
+ portalName,
208
+ tooltipName,
209
+ },
210
+ });
211
+ // Report only the first portal found per tooltip to avoid flooding
212
+ break;
213
+ }
214
+ }
215
+ },
216
+ };
217
+ },
218
+ });
219
+ //# sourceMappingURL=no-portal-inside-tooltip.js.map
@@ -0,0 +1,11 @@
1
+ type Options = [
2
+ {
3
+ booleanPrefixes?: string[];
4
+ callbackPrefixes?: string[];
5
+ booleanSuffixesToStrip?: string[];
6
+ exemptQualifiers?: string[];
7
+ minNounLengthForSuffixMatch?: number;
8
+ }?
9
+ ];
10
+ export declare const noRedundantBooleanCallbackProps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"redundantBooleanProp", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
11
+ export {};