@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.
Files changed (49) 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-portal-inside-tooltip.d.ts +10 -0
  19. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  20. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  22. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  24. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  26. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  27. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  28. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  29. package/lib/rules/parallelize-loop-awaits.js +582 -0
  30. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  32. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  34. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  35. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  36. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  38. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  39. package/lib/rules/prefer-use-base62-id.js +483 -0
  40. package/lib/rules/prefer-use-theme.d.ts +1 -0
  41. package/lib/rules/prefer-use-theme.js +206 -0
  42. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  43. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  44. package/lib/rules/require-props-composition.d.ts +10 -0
  45. package/lib/rules/require-props-composition.js +433 -0
  46. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  47. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  48. package/package.json +1 -1
  49. package/release-manifest.json +190 -0
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requirePropsComposition = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const minimatch_1 = require("minimatch");
6
+ const createRule_1 = require("../utils/createRule");
7
+ const DEFAULT_EXCLUDED_COMPONENTS = new Set([
8
+ 'Box',
9
+ 'Stack',
10
+ 'Typography',
11
+ 'Fragment',
12
+ 'Divider',
13
+ 'Container',
14
+ 'Grid',
15
+ 'Paper',
16
+ 'Card',
17
+ 'CardContent',
18
+ 'CardHeader',
19
+ 'CardActions',
20
+ 'List',
21
+ 'ListItem',
22
+ 'Table',
23
+ 'TableBody',
24
+ 'TableCell',
25
+ 'TableHead',
26
+ 'TableRow',
27
+ 'Toolbar',
28
+ 'AppBar',
29
+ 'Drawer',
30
+ 'Modal',
31
+ 'Backdrop',
32
+ 'Collapse',
33
+ 'Fade',
34
+ 'Grow',
35
+ 'Slide',
36
+ 'Zoom',
37
+ 'CircularProgress',
38
+ 'LinearProgress',
39
+ 'Skeleton',
40
+ 'Suspense',
41
+ 'StrictMode',
42
+ 'Profiler',
43
+ 'ErrorBoundary',
44
+ 'React.Fragment',
45
+ 'React.Suspense',
46
+ 'React.StrictMode',
47
+ ]);
48
+ const DEFAULT_TARGET_PATHS = ['src/components/**/*.tsx'];
49
+ /**
50
+ * Derives the expected Props type name for a JSX element name.
51
+ * e.g. "LoadingButton" → "LoadingButtonProps"
52
+ */
53
+ function toPropsTypeName(componentName) {
54
+ return `${componentName}Props`;
55
+ }
56
+ /**
57
+ * Returns true if the given TSTypeReference node references XProps via Pick or
58
+ * Omit (including nested inside Readonly<...>).
59
+ */
60
+ function typeReferenceContainsPickOrOmit(node, propsTypeName) {
61
+ const name = getTypeReferenceName(node);
62
+ if (name === 'Pick' || name === 'Omit') {
63
+ // First type argument should be the target props type
64
+ const params = node.typeParameters?.params;
65
+ if (params && params.length >= 1) {
66
+ const firstParam = params[0];
67
+ if (firstParam.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
68
+ const refName = getTypeReferenceName(firstParam);
69
+ if (refName === propsTypeName) {
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ if (name === 'Readonly') {
76
+ // Unwrap Readonly<...> and recurse
77
+ const params = node.typeParameters?.params;
78
+ if (params && params.length === 1) {
79
+ const inner = params[0];
80
+ if (inner.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
81
+ return typeReferenceContainsPickOrOmit(inner, propsTypeName);
82
+ }
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ /**
88
+ * Returns the identifier name of a TSTypeReference node.
89
+ */
90
+ function getTypeReferenceName(node) {
91
+ const typeName = node.typeName;
92
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
93
+ return typeName.name;
94
+ }
95
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
96
+ // e.g. React.Fragment
97
+ return `${typeName.left.type === utils_1.AST_NODE_TYPES.Identifier ? typeName.left.name : ''}.${typeName.right.name}`;
98
+ }
99
+ return '';
100
+ }
101
+ /**
102
+ * Recursively check if a TS type node composes with the given propsTypeName
103
+ * via Pick/Omit (at any level of intersection / Readonly wrapping, or nested
104
+ * in a TSTypeLiteral property's type annotation).
105
+ */
106
+ function typeNodeComposesWithProps(typeNode, propsTypeName) {
107
+ switch (typeNode.type) {
108
+ case utils_1.AST_NODE_TYPES.TSTypeReference: {
109
+ if (typeReferenceContainsPickOrOmit(typeNode, propsTypeName)) {
110
+ return true;
111
+ }
112
+ // Also recurse into type params (e.g. Readonly<Pick<XProps, ...>>)
113
+ if (typeNode.typeParameters) {
114
+ for (const param of typeNode.typeParameters.params) {
115
+ if (typeNodeComposesWithProps(param, propsTypeName)) {
116
+ return true;
117
+ }
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ case utils_1.AST_NODE_TYPES.TSIntersectionType: {
123
+ // Check each member of an intersection (A & B & C)
124
+ return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName));
125
+ }
126
+ case utils_1.AST_NODE_TYPES.TSUnionType: {
127
+ // Check each member of a union — for union types, at least one member composes
128
+ return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName));
129
+ }
130
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral: {
131
+ // Check property signatures for nested composition
132
+ // e.g. { iconProps?: Omit<GradientIconButtonProps, 'IconComponent'> }
133
+ return typeNode.members.some((member) => {
134
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
135
+ member.typeAnnotation) {
136
+ return typeNodeComposesWithProps(member.typeAnnotation.typeAnnotation, propsTypeName);
137
+ }
138
+ return false;
139
+ });
140
+ }
141
+ default:
142
+ return false;
143
+ }
144
+ }
145
+ /**
146
+ * Collect all capitalized JSX element names used in a component's function
147
+ * body (handles all JSX regardless of nesting depth / conditional branches).
148
+ */
149
+ function collectJsxElementNames(node) {
150
+ const names = new Set();
151
+ function visit(n) {
152
+ if (!n || typeof n !== 'object')
153
+ return;
154
+ if (n.type === utils_1.AST_NODE_TYPES.JSXOpeningElement &&
155
+ n.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
156
+ const name = n.name.name;
157
+ // Only custom components — starts with uppercase
158
+ if (/^[A-Z]/.test(name)) {
159
+ names.add(name);
160
+ }
161
+ }
162
+ // Traverse all child nodes
163
+ for (const key of Object.keys(n)) {
164
+ if (key === 'parent')
165
+ continue;
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ const child = n[key];
168
+ if (Array.isArray(child)) {
169
+ for (const item of child) {
170
+ if (item && typeof item === 'object' && 'type' in item) {
171
+ visit(item);
172
+ }
173
+ }
174
+ }
175
+ else if (child && typeof child === 'object' && 'type' in child) {
176
+ visit(child);
177
+ }
178
+ }
179
+ }
180
+ visit(node);
181
+ return names;
182
+ }
183
+ /**
184
+ * Find the Props type alias node that corresponds to a component by name.
185
+ * Looks for `type <ComponentName>Props = ...` in the program body.
186
+ */
187
+ function findPropsTypeAlias(program, componentName) {
188
+ const expectedTypeName = toPropsTypeName(componentName);
189
+ for (const stmt of program.body) {
190
+ // type XProps = ...
191
+ if (stmt.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
192
+ stmt.id.name === expectedTypeName) {
193
+ return stmt;
194
+ }
195
+ // export type XProps = ...
196
+ if (stmt.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
197
+ stmt.declaration?.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
198
+ stmt.declaration.id.name === expectedTypeName) {
199
+ return stmt.declaration;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * Given a component function node, find the props parameter type annotation
206
+ * and return the type alias name if it points to one. This handles patterns
207
+ * like `const Foo = ({ a }: FooProps) => ...`.
208
+ */
209
+ function getPropsTypeNameFromParam(funcNode) {
210
+ const firstParam = funcNode.params[0];
211
+ if (!firstParam)
212
+ return null;
213
+ let typeAnnotation = null;
214
+ if (firstParam.type === utils_1.AST_NODE_TYPES.Identifier &&
215
+ firstParam.typeAnnotation) {
216
+ typeAnnotation = firstParam.typeAnnotation.typeAnnotation;
217
+ }
218
+ else if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
219
+ firstParam.typeAnnotation) {
220
+ typeAnnotation = firstParam.typeAnnotation.typeAnnotation;
221
+ }
222
+ else if (firstParam.type === utils_1.AST_NODE_TYPES.RestElement) {
223
+ return null;
224
+ }
225
+ if (typeAnnotation &&
226
+ typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
227
+ const name = getTypeReferenceName(typeAnnotation);
228
+ // Strip Readonly wrapper if present
229
+ if (name === 'Readonly' && typeAnnotation.typeParameters?.params[0]) {
230
+ const inner = typeAnnotation.typeParameters.params[0];
231
+ if (inner.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
232
+ return getTypeReferenceName(inner);
233
+ }
234
+ }
235
+ return name;
236
+ }
237
+ return null;
238
+ }
239
+ exports.requirePropsComposition = (0, createRule_1.createRule)({
240
+ name: 'require-props-composition',
241
+ meta: {
242
+ type: 'suggestion',
243
+ docs: {
244
+ description: 'Require React component Props types to compose (via Pick/Omit) with the props types of non-leaf child components rendered in JSX',
245
+ recommended: 'warn',
246
+ },
247
+ fixable: undefined,
248
+ schema: [
249
+ {
250
+ type: 'object',
251
+ properties: {
252
+ targetPaths: {
253
+ type: 'array',
254
+ items: { type: 'string' },
255
+ },
256
+ excludeComponents: {
257
+ type: 'array',
258
+ items: { type: 'string' },
259
+ },
260
+ minDependencyCount: {
261
+ type: 'number',
262
+ minimum: 1,
263
+ },
264
+ requireAllDependencies: {
265
+ type: 'boolean',
266
+ },
267
+ },
268
+ additionalProperties: false,
269
+ },
270
+ ],
271
+ messages: {
272
+ missingPropsComposition: "Component '{{componentName}}' renders {{dependencyList}} but '{{propsTypeName}}' does not compose with {{missingList}} via Pick<...> or Omit<...>. Consider: type {{propsTypeName}} = Omit<{{primaryDep}}, 'overriddenProp'> & { /* your props */ };",
273
+ },
274
+ },
275
+ defaultOptions: [{}],
276
+ create(context, [options]) {
277
+ const targetPaths = options?.targetPaths ?? DEFAULT_TARGET_PATHS;
278
+ const excludeComponents = new Set([
279
+ ...DEFAULT_EXCLUDED_COMPONENTS,
280
+ ...(options?.excludeComponents ?? []),
281
+ ]);
282
+ const minDependencyCount = options?.minDependencyCount ?? 1;
283
+ const requireAllDependencies = options?.requireAllDependencies ?? false;
284
+ // Check whether the current file matches any of the targetPaths globs.
285
+ const filename = context.getFilename();
286
+ const matchesTargetPath = targetPaths.some((pattern) => (0, minimatch_1.minimatch)(filename, pattern, { matchBase: false }));
287
+ if (!matchesTargetPath) {
288
+ return {};
289
+ }
290
+ return {
291
+ // Arrow function component: const MyComponent = (...) => ...
292
+ VariableDeclaration(node) {
293
+ for (const declarator of node.declarations) {
294
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
295
+ !declarator.init) {
296
+ continue;
297
+ }
298
+ const componentName = declarator.id.name;
299
+ // Must start with uppercase to be a component
300
+ if (!/^[A-Z]/.test(componentName))
301
+ continue;
302
+ let funcNode = null;
303
+ if (declarator.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
304
+ declarator.init.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
305
+ funcNode = declarator.init;
306
+ }
307
+ else if (
308
+ // memo((...) => ...)
309
+ declarator.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
310
+ const call = declarator.init;
311
+ const arg0 = call.arguments[0];
312
+ if (arg0 &&
313
+ (arg0.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
314
+ arg0.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
315
+ funcNode = arg0;
316
+ }
317
+ }
318
+ if (!funcNode)
319
+ continue;
320
+ // Get the program node from the ancestors
321
+ const ancestors = context.getAncestors();
322
+ const prog = ancestors[0];
323
+ checkComponentWithProgram(componentName, funcNode, declarator.id, prog);
324
+ }
325
+ },
326
+ // Function declaration component: function MyComponent(...) { ... }
327
+ FunctionDeclaration(node) {
328
+ if (!node.id || !/^[A-Z]/.test(node.id.name))
329
+ return;
330
+ const componentName = node.id.name;
331
+ const ancestors = context.getAncestors();
332
+ const prog = ancestors[0];
333
+ checkComponentWithProgram(componentName, node, node.id, prog);
334
+ },
335
+ };
336
+ function checkComponentWithProgram(componentName, funcNode, reportNode, prog) {
337
+ // Collect all JSX element names used in the component body
338
+ const body = funcNode.body ?? funcNode;
339
+ const allJsxNames = collectJsxElementNames(body);
340
+ // Filter to non-excluded custom components
341
+ const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) && name !== componentName);
342
+ if (depComponents.length < minDependencyCount) {
343
+ return;
344
+ }
345
+ // Resolve the props type for this component
346
+ const propsTypeAlias = findPropsTypeAlias(prog, componentName);
347
+ let propsTypeName = null;
348
+ let propsTypeNode = null;
349
+ if (propsTypeAlias) {
350
+ propsTypeName = propsTypeAlias.id.name;
351
+ propsTypeNode = propsTypeAlias.typeAnnotation;
352
+ }
353
+ else {
354
+ // Fall back to inline parameter annotation
355
+ const paramTypeName = getPropsTypeNameFromParam(funcNode);
356
+ if (!paramTypeName) {
357
+ // No props type at all — skip per spec
358
+ return;
359
+ }
360
+ propsTypeName = paramTypeName;
361
+ // Try to find this type alias in the program too
362
+ const resolved = findPropsTypeAliasByName(prog, paramTypeName);
363
+ if (resolved) {
364
+ propsTypeNode = resolved.typeAnnotation;
365
+ }
366
+ }
367
+ // No props type resolvable — skip
368
+ if (!propsTypeNode) {
369
+ return;
370
+ }
371
+ const composedWith = new Set();
372
+ const missingComposition = [];
373
+ for (const dep of depComponents) {
374
+ const expectedPropsType = toPropsTypeName(dep);
375
+ const composes = typeNodeComposesWithProps(propsTypeNode, expectedPropsType);
376
+ if (composes) {
377
+ composedWith.add(dep);
378
+ }
379
+ else {
380
+ missingComposition.push(dep);
381
+ }
382
+ }
383
+ if (!requireAllDependencies) {
384
+ // Only flag when NO dependency has composition
385
+ if (composedWith.size > 0) {
386
+ return;
387
+ }
388
+ }
389
+ else {
390
+ // Flag when ANY dependency is missing composition
391
+ if (missingComposition.length === 0) {
392
+ return;
393
+ }
394
+ }
395
+ const flaggedDeps = requireAllDependencies
396
+ ? missingComposition
397
+ : depComponents;
398
+ if (flaggedDeps.length === 0)
399
+ return;
400
+ context.report({
401
+ node: reportNode,
402
+ messageId: 'missingPropsComposition',
403
+ data: {
404
+ componentName,
405
+ propsTypeName: propsTypeName ?? `${componentName}Props`,
406
+ dependencyList: depComponents.map((d) => `'${d}'`).join(', '),
407
+ missingList: flaggedDeps
408
+ .map((d) => `'${toPropsTypeName(d)}'`)
409
+ .join(', '),
410
+ primaryDep: toPropsTypeName(flaggedDeps[0]),
411
+ },
412
+ });
413
+ }
414
+ },
415
+ });
416
+ /**
417
+ * Find a type alias by name anywhere in the program body (exported or not).
418
+ */
419
+ function findPropsTypeAliasByName(program, typeName) {
420
+ for (const stmt of program.body) {
421
+ if (stmt.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
422
+ stmt.id.name === typeName) {
423
+ return stmt;
424
+ }
425
+ if (stmt.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
426
+ stmt.declaration?.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
427
+ stmt.declaration.id.name === typeName) {
428
+ return stmt.declaration;
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+ //# sourceMappingURL=require-props-composition.js.map
@@ -0,0 +1,9 @@
1
+ type Options = [
2
+ {
3
+ firestoreTypePaths?: string[];
4
+ targetPaths?: string[];
5
+ ignoreTestFiles?: boolean;
6
+ }
7
+ ];
8
+ export declare const requireServerTimestampForFirestoreDates: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useServerTimestamp", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
9
+ export {};