@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.
- 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/logical-top-to-bottom-grouping.js +87 -19
- 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-hungarian.js +11 -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 +212 -0
|
@@ -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 {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noSatisfiesInFrontendBundle = void 0;
|
|
4
|
+
const minimatch_1 = require("minimatch");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_EXCLUDE_PATHS = [
|
|
7
|
+
'functions/src/firestore/**',
|
|
8
|
+
'functions/src/callable/**',
|
|
9
|
+
'functions/src/pubsub/**',
|
|
10
|
+
'functions/src/webhooks/**',
|
|
11
|
+
'functions/src/queues/**',
|
|
12
|
+
'functions/src/realtime/**',
|
|
13
|
+
];
|
|
14
|
+
const DEFAULT_INCLUDE_PATHS = ['src/**', 'functions/src/**'];
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a filesystem path to forward slashes for consistent
|
|
17
|
+
* glob matching on all platforms.
|
|
18
|
+
*/
|
|
19
|
+
function normalizePath(filePath) {
|
|
20
|
+
return filePath.replace(/\\/g, '/');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the portion of the normalized path starting from a recognized
|
|
24
|
+
* root segment (`src/` or `functions/`), so that absolute paths from
|
|
25
|
+
* the RuleTester (e.g. `/project/src/components/Foo.tsx`) are matched
|
|
26
|
+
* against the configured include/exclude globs as if they were relative
|
|
27
|
+
* to the project root.
|
|
28
|
+
*
|
|
29
|
+
* Check `functions/` before `src/` because `functions/src/` paths contain
|
|
30
|
+
* `/src/` as a substring; matching `/src/` first would incorrectly strip
|
|
31
|
+
* the `functions/` prefix and break exclude-glob matching.
|
|
32
|
+
*/
|
|
33
|
+
function toRelativeLike(normalizedFilename) {
|
|
34
|
+
const functionsIdx = normalizedFilename.indexOf('/functions/');
|
|
35
|
+
if (functionsIdx !== -1) {
|
|
36
|
+
return normalizedFilename.slice(functionsIdx + 1); // "functions/..."
|
|
37
|
+
}
|
|
38
|
+
const srcIdx = normalizedFilename.indexOf('/src/');
|
|
39
|
+
if (srcIdx !== -1) {
|
|
40
|
+
return normalizedFilename.slice(srcIdx + 1); // "src/..."
|
|
41
|
+
}
|
|
42
|
+
return normalizedFilename;
|
|
43
|
+
}
|
|
44
|
+
exports.noSatisfiesInFrontendBundle = (0, createRule_1.createRule)({
|
|
45
|
+
name: 'no-satisfies-in-frontend-bundle',
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'problem',
|
|
48
|
+
docs: {
|
|
49
|
+
description: 'Disallow the `satisfies` operator in files that reach the frontend webpack bundle (Next.js 12 SWC cannot parse it)',
|
|
50
|
+
recommended: 'error',
|
|
51
|
+
},
|
|
52
|
+
fixable: undefined,
|
|
53
|
+
schema: [
|
|
54
|
+
{
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
excludePaths: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: { type: 'string' },
|
|
60
|
+
default: DEFAULT_EXCLUDE_PATHS,
|
|
61
|
+
},
|
|
62
|
+
includePaths: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
default: DEFAULT_INCLUDE_PATHS,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: {
|
|
72
|
+
noSatisfiesOperator: 'SWC in Next.js 12 cannot parse `satisfies`; this file is bundled by webpack and will fail the production build. Use an explicit type annotation, or a constrained identity helper when literal keys must be preserved.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultOptions: [
|
|
76
|
+
{
|
|
77
|
+
excludePaths: DEFAULT_EXCLUDE_PATHS,
|
|
78
|
+
includePaths: DEFAULT_INCLUDE_PATHS,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
create(context, [options]) {
|
|
82
|
+
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
83
|
+
const includePaths = options.includePaths ?? DEFAULT_INCLUDE_PATHS;
|
|
84
|
+
const filename = context.getFilename();
|
|
85
|
+
// Skip synthetic filenames used by RuleTester when no filename is provided.
|
|
86
|
+
if (filename === '<input>' || filename === '<text>') {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const normalizedFilename = normalizePath(filename);
|
|
90
|
+
// Exempt declaration files — they are never bundled.
|
|
91
|
+
if (normalizedFilename.endsWith('.d.ts')) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
// Exempt test and spec files — they are never bundled.
|
|
95
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(normalizedFilename)) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
// Derive a path relative to the project root for glob matching.
|
|
99
|
+
const relPath = toRelativeLike(normalizedFilename);
|
|
100
|
+
// Only enforce within the configured include paths.
|
|
101
|
+
const includeMatchers = includePaths.map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true }));
|
|
102
|
+
const isIncluded = includeMatchers.some((mm) => mm.match(normalizedFilename) || mm.match(relPath));
|
|
103
|
+
if (!isIncluded) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
// Skip files that match any exclude path.
|
|
107
|
+
const excludeMatchers = excludePaths.map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true }));
|
|
108
|
+
const isExcluded = excludeMatchers.some((mm) => mm.match(normalizedFilename) || mm.match(relPath));
|
|
109
|
+
if (isExcluded) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
// Every TSSatisfiesExpression is reported regardless of wrapping context.
|
|
114
|
+
// "x as const satisfies T" is a TSSatisfiesExpression (wrapping a
|
|
115
|
+
// TSAsExpression), so it is caught. Bare "as const" (TSAsExpression
|
|
116
|
+
// only) is NOT caught — correct.
|
|
117
|
+
TSSatisfiesExpression(node) {
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'noSatisfiesOperator',
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=no-satisfies-in-frontend-bundle.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
dismissLabels?: string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const noSingleDismissDialogButton: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noSingleDismissDialogButton", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noSingleDismissDialogButton = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_DISMISS_LABELS = [
|
|
7
|
+
'Cancel',
|
|
8
|
+
'Close',
|
|
9
|
+
'Dismiss',
|
|
10
|
+
'Not now',
|
|
11
|
+
'Never mind',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the string value from a node that is either a string Literal or a
|
|
15
|
+
* TemplateLiteral with no expressions (i.e. a plain template string). Returns
|
|
16
|
+
* null for anything more dynamic.
|
|
17
|
+
*/
|
|
18
|
+
function getStaticStringValue(node) {
|
|
19
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
20
|
+
return node.value;
|
|
21
|
+
}
|
|
22
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
23
|
+
node.expressions.length === 0 &&
|
|
24
|
+
node.quasis.length === 1) {
|
|
25
|
+
return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when the value (after trimming) matches one of the dismiss
|
|
31
|
+
* labels, case-insensitively.
|
|
32
|
+
*/
|
|
33
|
+
function isDismissLabel(value, dismissLabels) {
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
return dismissLabels.some((label) => label.toLowerCase() === normalized);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Searches the properties of an ObjectExpression for a property named
|
|
39
|
+
* `children` whose value is a static string matching a dismiss label.
|
|
40
|
+
* Returns the button ObjectExpression node if found, null otherwise.
|
|
41
|
+
*/
|
|
42
|
+
function findDismissChildrenProp(element, dismissLabels) {
|
|
43
|
+
for (const prop of element.properties) {
|
|
44
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
45
|
+
continue;
|
|
46
|
+
if (prop.computed)
|
|
47
|
+
continue;
|
|
48
|
+
const key = prop.key;
|
|
49
|
+
const isChildrenKey = (key.type === utils_1.AST_NODE_TYPES.Identifier && key.name === 'children') ||
|
|
50
|
+
(key.type === utils_1.AST_NODE_TYPES.Literal && key.value === 'children');
|
|
51
|
+
if (!isChildrenKey)
|
|
52
|
+
continue;
|
|
53
|
+
const staticVal = getStaticStringValue(prop.value);
|
|
54
|
+
if (staticVal !== null && isDismissLabel(staticVal, dismissLabels)) {
|
|
55
|
+
return prop;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
exports.noSingleDismissDialogButton = (0, createRule_1.createRule)({
|
|
61
|
+
name: 'no-single-dismiss-dialog-button',
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'suggestion',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Disallow a single dialog button whose label is a dismiss action (Cancel, Close, Dismiss, Not now, Never mind). Use navigation.onClose for dismissal; the buttons array should only contain affirmative actions.',
|
|
66
|
+
recommended: 'error',
|
|
67
|
+
},
|
|
68
|
+
fixable: undefined,
|
|
69
|
+
schema: [
|
|
70
|
+
{
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
dismissLabels: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: { type: 'string' },
|
|
76
|
+
default: DEFAULT_DISMISS_LABELS,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
messages: {
|
|
83
|
+
noSingleDismissDialogButton: 'A single dialog button with label "{{label}}" is a dismiss action. Use navigation.onClose to handle dismissal via the X close button instead; the buttons array should only contain affirmative actions.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
defaultOptions: [{ dismissLabels: DEFAULT_DISMISS_LABELS }],
|
|
87
|
+
create(context) {
|
|
88
|
+
const options = context.options[0] ?? {};
|
|
89
|
+
const dismissLabels = options.dismissLabels ?? DEFAULT_DISMISS_LABELS;
|
|
90
|
+
return {
|
|
91
|
+
Property(node) {
|
|
92
|
+
// Only care about properties named `buttons`
|
|
93
|
+
if (node.computed)
|
|
94
|
+
return;
|
|
95
|
+
const key = node.key;
|
|
96
|
+
const isButtonsKey = (key.type === utils_1.AST_NODE_TYPES.Identifier && key.name === 'buttons') ||
|
|
97
|
+
(key.type === utils_1.AST_NODE_TYPES.Literal && key.value === 'buttons');
|
|
98
|
+
if (!isButtonsKey)
|
|
99
|
+
return;
|
|
100
|
+
// The value must be an array literal
|
|
101
|
+
if (node.value.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
102
|
+
return;
|
|
103
|
+
const arrayExpr = node.value;
|
|
104
|
+
// Only flag exactly one element
|
|
105
|
+
if (arrayExpr.elements.length !== 1)
|
|
106
|
+
return;
|
|
107
|
+
const element = arrayExpr.elements[0];
|
|
108
|
+
if (!element || element.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
109
|
+
return;
|
|
110
|
+
const dismissProp = findDismissChildrenProp(element, dismissLabels);
|
|
111
|
+
if (!dismissProp)
|
|
112
|
+
return;
|
|
113
|
+
// Retrieve the label text for the error message
|
|
114
|
+
const labelNode = dismissProp.value;
|
|
115
|
+
const labelText = getStaticStringValue(labelNode) ?? '';
|
|
116
|
+
context.report({
|
|
117
|
+
node: element,
|
|
118
|
+
messageId: 'noSingleDismissDialogButton',
|
|
119
|
+
data: {
|
|
120
|
+
label: labelText.trim(),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=no-single-dismiss-dialog-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noStablehashReactNodes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noStableHashReactNode", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|