@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,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 {};
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noRedundantBooleanCallbackProps = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * Default boolean prop prefixes that signal visibility/enablement control —
8
+ * the patterns most likely to be redundant when a matching callback exists.
9
+ * Prefixes like `is`/`has` represent state rather than control, so they are
10
+ * excluded by default to avoid false positives on state props.
11
+ */
12
+ const DEFAULT_BOOLEAN_PREFIXES = [
13
+ 'shouldShow',
14
+ 'shouldEnable',
15
+ 'shouldAllow',
16
+ 'enable',
17
+ 'allow',
18
+ 'show',
19
+ 'hide',
20
+ 'display',
21
+ ];
22
+ /**
23
+ * Default callback prefixes to match against the boolean's core noun.
24
+ */
25
+ const DEFAULT_CALLBACK_PREFIXES = ['on', 'handle'];
26
+ /**
27
+ * Trailing decorative suffixes that do not change the noun being controlled.
28
+ * E.g. `shouldShowMinimizeIcon` — `Icon` is purely decorative; the feature
29
+ * being toggled is still `Minimize`.
30
+ *
31
+ * When suffix stripping IS used to produce a noun match, the rule requires the
32
+ * resulting noun to be at least `minNounLengthForSuffixMatch` characters long
33
+ * to avoid false positives on short generic action words (e.g. `Close`) that
34
+ * appear in many general-purpose callbacks.
35
+ */
36
+ const DEFAULT_BOOLEAN_SUFFIXES_TO_STRIP = [
37
+ 'Icon',
38
+ 'Button',
39
+ 'Action',
40
+ 'Control',
41
+ 'Badge',
42
+ ];
43
+ /**
44
+ * Minimum noun length when suffix stripping is involved in the match. Short
45
+ * nouns like `Close` (5) are often generic action names used in callbacks that
46
+ * span more than one trigger (e.g. `onClose` handles backdrop, escape, and
47
+ * icon clicks). Requiring at least 6 chars lets specific nouns like `Delete`,
48
+ * `Submit`, `Dismiss` match while excluding single-word generic actions.
49
+ */
50
+ const DEFAULT_MIN_NOUN_LENGTH_FOR_SUFFIX_MATCH = 6;
51
+ /**
52
+ * Qualifying word-parts whose presence in the boolean name changes its
53
+ * semantics enough that the boolean is NOT a simple feature-presence toggle.
54
+ * E.g. `shouldExpandInitially` is an initial-state control, not feature-
55
+ * presence control, so we do not flag it even if `onExpand` is present.
56
+ */
57
+ const DEFAULT_EXEMPT_QUALIFIERS = [
58
+ 'Initially',
59
+ 'OnMount',
60
+ 'ByDefault',
61
+ 'WhenHovered',
62
+ 'WhenFocused',
63
+ 'WhenActive',
64
+ 'Always',
65
+ 'Never',
66
+ ];
67
+ /**
68
+ * Determine whether a boolean prop name (after stripping its visibility prefix)
69
+ * "matches" a callback noun with enough confidence to flag a redundancy.
70
+ *
71
+ * Two tiers of match:
72
+ * 1. **Exact match** (no suffix stripping): the boolean remainder equals the
73
+ * callback noun exactly — high confidence, no length restriction.
74
+ * 2. **Suffix-strip match**: the boolean remainder starts with the callback
75
+ * noun and the leftover is a recognized decorative suffix. Requires the
76
+ * callback noun to be at least `minNounLength` characters to avoid matching
77
+ * short generic action words.
78
+ */
79
+ function booleanCoreMatchesCallbackNoun(booleanRemainder, callbackNoun, suffixesToStrip, minNounLengthForSuffixMatch) {
80
+ const lowerRemainder = booleanRemainder.toLowerCase();
81
+ const lowerNoun = callbackNoun.toLowerCase();
82
+ // Tier 1: exact match (case-insensitive) — highest confidence
83
+ if (lowerRemainder === lowerNoun)
84
+ return true;
85
+ // Tier 2: remainder starts with noun + recognized decorative suffix
86
+ if (!lowerRemainder.startsWith(lowerNoun))
87
+ return false;
88
+ if (callbackNoun.length < minNounLengthForSuffixMatch)
89
+ return false;
90
+ const leftover = booleanRemainder.slice(callbackNoun.length);
91
+ for (const suffix of suffixesToStrip) {
92
+ if (leftover === suffix)
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+ /**
98
+ * Check whether a boolean prop name contains an exempt qualifier word,
99
+ * indicating that the boolean controls something beyond simple feature
100
+ * presence (e.g. initial state, conditional display).
101
+ */
102
+ function hasExemptQualifier(name, exemptQualifiers) {
103
+ for (const qualifier of exemptQualifiers) {
104
+ if (name.includes(qualifier))
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * Extract the remainder of a boolean prop name after stripping its leading
111
+ * visibility/enablement prefix. Returns null if no matching prefix is found
112
+ * or if the name is entirely the prefix.
113
+ *
114
+ * Longest prefix wins (so `shouldShow` beats `should`).
115
+ */
116
+ function stripBooleanPrefix(name, prefixes) {
117
+ const sortedPrefixes = [...prefixes].sort((a, b) => b.length - a.length);
118
+ for (const prefix of sortedPrefixes) {
119
+ if (name.toLowerCase().startsWith(prefix.toLowerCase())) {
120
+ const remainder = name.slice(prefix.length);
121
+ if (remainder.length === 0)
122
+ return null;
123
+ // Require a camelCase word boundary after the prefix
124
+ if (remainder[0] >= 'A' && remainder[0] <= 'Z')
125
+ return remainder;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Extract the core noun from a callback prop name after stripping its leading
132
+ * callback prefix (e.g. `on` from `onMinimize` → `Minimize`). Returns null
133
+ * if no matching prefix is found or if the name is entirely the prefix.
134
+ */
135
+ function extractCallbackNoun(name, callbackPrefixes) {
136
+ const sortedPrefixes = [...callbackPrefixes].sort((a, b) => b.length - a.length);
137
+ for (const prefix of sortedPrefixes) {
138
+ if (name.toLowerCase().startsWith(prefix.toLowerCase())) {
139
+ const remainder = name.slice(prefix.length);
140
+ if (remainder.length === 0)
141
+ return null;
142
+ if (remainder[0] >= 'A' && remainder[0] <= 'Z')
143
+ return remainder;
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * Determine if a type annotation node represents a boolean (or a
150
+ * boolean-containing union like `boolean | undefined`).
151
+ */
152
+ function isBooleanTypeNode(typeAnnotation) {
153
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword)
154
+ return true;
155
+ // Handle `boolean | undefined` / `boolean | null` explicit union forms
156
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSUnionType) {
157
+ return typeAnnotation.types.every((t) => t.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword ||
158
+ t.type === utils_1.AST_NODE_TYPES.TSUndefinedKeyword ||
159
+ t.type === utils_1.AST_NODE_TYPES.TSNullKeyword);
160
+ }
161
+ return false;
162
+ }
163
+ /**
164
+ * Determine if a type annotation node represents a callable (function type,
165
+ * named callback type reference, or union involving a function type —
166
+ * including the `callback | 'disabled'` pattern).
167
+ */
168
+ function isCallableTypeNode(typeAnnotation) {
169
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSFunctionType)
170
+ return true;
171
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference)
172
+ return true;
173
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSUnionType) {
174
+ return typeAnnotation.types.some((t) => t.type === utils_1.AST_NODE_TYPES.TSFunctionType ||
175
+ t.type === utils_1.AST_NODE_TYPES.TSTypeReference);
176
+ }
177
+ return false;
178
+ }
179
+ /**
180
+ * Get the string name of a TSPropertySignature key, or null for computed keys.
181
+ */
182
+ function getPropName(prop) {
183
+ const key = prop.key;
184
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier)
185
+ return key.name;
186
+ if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
187
+ return key.value;
188
+ }
189
+ return null;
190
+ }
191
+ /**
192
+ * Collect all TSPropertySignature members from a type-level node, recursively
193
+ * unwrapping `Readonly<{...}>` wrappers and flattening inline `&` intersection
194
+ * constituent type literals. External type references in intersections are
195
+ * intentionally skipped to avoid flagging inherited library props.
196
+ */
197
+ function collectMembers(node) {
198
+ const results = [];
199
+ if (node.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
200
+ results.push(node);
201
+ return results;
202
+ }
203
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
204
+ for (const member of node.members) {
205
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
206
+ results.push(member);
207
+ }
208
+ }
209
+ return results;
210
+ }
211
+ if (node.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
212
+ for (const constituent of node.types) {
213
+ if (constituent.type === utils_1.AST_NODE_TYPES.TSTypeLiteral ||
214
+ constituent.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
215
+ results.push(...collectMembers(constituent));
216
+ }
217
+ else if (constituent.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
218
+ // Unwrap `Readonly<{...}>` — recognized safe wrapper
219
+ const ref = constituent;
220
+ if (ref.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
221
+ ref.typeName.name === 'Readonly' &&
222
+ ref.typeParameters?.params?.length === 1 &&
223
+ ref.typeParameters.params[0].type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
224
+ results.push(...collectMembers(ref.typeParameters.params[0]));
225
+ }
226
+ // Other TSTypeReference in an intersection = external type, skip
227
+ }
228
+ }
229
+ return results;
230
+ }
231
+ // Top-level `Readonly<{...}>` wrapper
232
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
233
+ const ref = node;
234
+ if (ref.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
235
+ ref.typeName.name === 'Readonly' &&
236
+ ref.typeParameters?.params?.length === 1 &&
237
+ ref.typeParameters.params[0].type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
238
+ return collectMembers(ref.typeParameters.params[0]);
239
+ }
240
+ }
241
+ return results;
242
+ }
243
+ exports.noRedundantBooleanCallbackProps = (0, createRule_1.createRule)({
244
+ name: 'no-redundant-boolean-callback-props',
245
+ meta: {
246
+ type: 'suggestion',
247
+ docs: {
248
+ description: 'Disallow redundant boolean props that duplicate the semantic meaning of optional callback props, violating the Interface Segregation Principle.',
249
+ recommended: 'error',
250
+ },
251
+ fixable: undefined,
252
+ schema: [
253
+ {
254
+ type: 'object',
255
+ properties: {
256
+ booleanPrefixes: {
257
+ type: 'array',
258
+ items: { type: 'string' },
259
+ },
260
+ callbackPrefixes: {
261
+ type: 'array',
262
+ items: { type: 'string' },
263
+ },
264
+ booleanSuffixesToStrip: {
265
+ type: 'array',
266
+ items: { type: 'string' },
267
+ },
268
+ exemptQualifiers: {
269
+ type: 'array',
270
+ items: { type: 'string' },
271
+ },
272
+ minNounLengthForSuffixMatch: {
273
+ type: 'number',
274
+ },
275
+ },
276
+ additionalProperties: false,
277
+ },
278
+ ],
279
+ messages: {
280
+ redundantBooleanProp: 'Boolean prop "{{booleanProp}}" is redundant alongside callback prop "{{callbackProp}}" — the callback\'s presence already communicates whether the feature is enabled. Remove "{{booleanProp}}" and key off the callback\'s presence instead, or use the `callback | \'disabled\'` union pattern.',
281
+ },
282
+ },
283
+ defaultOptions: [{}],
284
+ create(context, [options]) {
285
+ const booleanPrefixes = options?.booleanPrefixes ?? DEFAULT_BOOLEAN_PREFIXES;
286
+ const callbackPrefixes = options?.callbackPrefixes ?? DEFAULT_CALLBACK_PREFIXES;
287
+ const booleanSuffixesToStrip = options?.booleanSuffixesToStrip ?? DEFAULT_BOOLEAN_SUFFIXES_TO_STRIP;
288
+ const exemptQualifiers = options?.exemptQualifiers ?? DEFAULT_EXEMPT_QUALIFIERS;
289
+ const minNounLengthForSuffixMatch = options?.minNounLengthForSuffixMatch ??
290
+ DEFAULT_MIN_NOUN_LENGTH_FOR_SUFFIX_MATCH;
291
+ function checkMembers(members) {
292
+ // Collect boolean candidates: props whose type is boolean and whose name
293
+ // matches a visibility/enablement prefix and has no exempt qualifier.
294
+ const booleanCandidates = [];
295
+ // Map from callback noun (lower-case) → original prop name for reporting
296
+ const callbackNouns = new Map();
297
+ for (const member of members) {
298
+ const name = getPropName(member);
299
+ if (!name || !member.typeAnnotation)
300
+ continue;
301
+ const typeNode = member.typeAnnotation.typeAnnotation;
302
+ if (isBooleanTypeNode(typeNode)) {
303
+ const remainder = stripBooleanPrefix(name, booleanPrefixes);
304
+ if (remainder && !hasExemptQualifier(name, exemptQualifiers)) {
305
+ booleanCandidates.push({ prop: member, name, remainder });
306
+ }
307
+ }
308
+ else if (isCallableTypeNode(typeNode)) {
309
+ const noun = extractCallbackNoun(name, callbackPrefixes);
310
+ if (noun) {
311
+ callbackNouns.set(noun.toLowerCase(), name);
312
+ }
313
+ }
314
+ }
315
+ // For each boolean candidate, look for a matching callback
316
+ for (const { prop, name, remainder } of booleanCandidates) {
317
+ for (const [callbackNounLower, callbackName] of callbackNouns) {
318
+ // Reconstruct the callback noun with its original casing (first char
319
+ // uppercase) to use for the suffix-match length check.
320
+ const callbackNoun = callbackNounLower.charAt(0).toUpperCase() +
321
+ callbackNounLower.slice(1);
322
+ if (booleanCoreMatchesCallbackNoun(remainder, callbackNoun, booleanSuffixesToStrip, minNounLengthForSuffixMatch)) {
323
+ context.report({
324
+ node: prop,
325
+ messageId: 'redundantBooleanProp',
326
+ data: {
327
+ booleanProp: name,
328
+ callbackProp: callbackName,
329
+ },
330
+ });
331
+ break; // report at most one error per boolean prop
332
+ }
333
+ }
334
+ }
335
+ }
336
+ function analyzeTypeNode(typeNode) {
337
+ const members = collectMembers(typeNode);
338
+ if (members.length > 0) {
339
+ checkMembers(members);
340
+ }
341
+ }
342
+ return {
343
+ TSTypeAliasDeclaration(node) {
344
+ analyzeTypeNode(node.typeAnnotation);
345
+ },
346
+ TSInterfaceDeclaration(node) {
347
+ // Analyze the interface body's own members; ignore `extends` clauses
348
+ // (inherited external types are not analyzed to avoid false positives).
349
+ const members = node.body.body.filter((m) => m.type === utils_1.AST_NODE_TYPES.TSPropertySignature);
350
+ checkMembers(members);
351
+ },
352
+ };
353
+ },
354
+ });
355
+ //# sourceMappingURL=no-redundant-boolean-callback-props.js.map
@@ -0,0 +1,8 @@
1
+ type Options = [
2
+ {
3
+ excludePaths?: string[];
4
+ includePaths?: string[];
5
+ }
6
+ ];
7
+ export declare const noSatisfiesInFrontendBundle: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noSatisfiesOperator", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export {};