@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,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceIsPrefixValidators = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const minimatch_1 = require("minimatch");
7
+ const DEFAULT_TARGET_PATHS = ['**/validators/**/*.ts'];
8
+ const DEFAULT_REQUIRED_PREFIX = 'is';
9
+ // Additional prefixes that are semantically valid predicates in the codebase:
10
+ // - 'not' for negative predicates (notContainsUrl, notContainsEmail)
11
+ // - 'are' for plural predicates (areBothPositiveIntegers, areBothFiniteNumbers)
12
+ const DEFAULT_ALLOWED_PREFIXES = ['is', 'not', 'are'];
13
+ const DEFAULT_DISALLOWED_PREFIXES = ['validate', 'check', 'verify', 'ensure'];
14
+ // Infrastructure exports that live in validators/ directories but are not
15
+ // validators themselves. These are builder utilities, type exports, and
16
+ // factory patterns that follow their own naming conventions.
17
+ const DEFAULT_EXCLUDE_NAMES = [
18
+ 'ValidatorPipeline',
19
+ 'ValidatorFactory',
20
+ 'ValidatorFactorySync',
21
+ 'Validate',
22
+ 'ValidateSync',
23
+ ];
24
+ const DEFAULT_EXCLUDE_PATTERNS = ['**/*.test.ts', '**/*.spec.ts'];
25
+ /**
26
+ * Checks whether `name` starts with `prefix` at a proper camelCase boundary.
27
+ * The prefix must be followed by an uppercase letter, digit, or end-of-string
28
+ * so that e.g. 'check' matches 'checkTokenCount' but not 'checkbox'.
29
+ */
30
+ function startsWithPrefix(name, prefix) {
31
+ if (!name.toLowerCase().startsWith(prefix.toLowerCase())) {
32
+ return false;
33
+ }
34
+ if (name.length === prefix.length) {
35
+ return true;
36
+ }
37
+ const nextChar = name.charAt(prefix.length);
38
+ return (nextChar === nextChar.toUpperCase() && nextChar !== nextChar.toLowerCase());
39
+ }
40
+ /**
41
+ * Derives the suggested rename by stripping the disallowed prefix and
42
+ * prepending the requiredPrefix. Preserves PascalCase of the remainder.
43
+ *
44
+ * Example: 'validateDecreaseOnly' → 'isDecreaseOnly'
45
+ */
46
+ function suggestRename(name, strippedPrefix, requiredPrefix) {
47
+ const remainder = name.slice(strippedPrefix.length);
48
+ // Ensure the first character of the remainder is uppercase after stripping.
49
+ const capitalizedRemainder = remainder.charAt(0).toUpperCase() + remainder.slice(1);
50
+ return `${requiredPrefix}${capitalizedRemainder}`;
51
+ }
52
+ /**
53
+ * A sentinel value that signals to `checkName` that the exported binding is
54
+ * definitely callable (e.g. a FunctionDeclaration or a specifier that can only
55
+ * be a local function binding). This is distinct from `undefined` which means
56
+ * "no initializer, unknown callability."
57
+ */
58
+ const DEFINITELY_CALLABLE = Symbol('DEFINITELY_CALLABLE');
59
+ /**
60
+ * Returns true when the export declaration is for a type-only construct:
61
+ * - `export type Foo = ...`
62
+ * - `export interface Foo { ... }`
63
+ * - `export class Foo { ... }` (classes are infrastructure, not validators)
64
+ * - `export enum Foo { ... }` (enums are not validators)
65
+ */
66
+ function isTypeOrClassExport(node) {
67
+ if (!node.declaration) {
68
+ return false;
69
+ }
70
+ const declType = node.declaration.type;
71
+ return (declType === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
72
+ declType === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
73
+ declType === utils_1.AST_NODE_TYPES.TSEnumDeclaration ||
74
+ declType === utils_1.AST_NODE_TYPES.ClassDeclaration);
75
+ }
76
+ /**
77
+ * Returns true when the variable initializer is clearly a non-function
78
+ * constant (primitive literal, regex, array literal, object literal, template
79
+ * literal, tagged template) rather than a callable validator. This avoids
80
+ * false positives on constants like `MAX_DESCRIPTION_LENGTH`, `EMAIL_REGEX`,
81
+ * and `ERROR_NOT_INTEGER`.
82
+ *
83
+ * Per the spec (Edge Case 1), we flag:
84
+ * - function expressions / arrow functions
85
+ * - call expressions (derived validators, ValidatorPipeline chains)
86
+ * - member expressions that may resolve to validators
87
+ *
88
+ * We skip:
89
+ * - Literal primitives (string/number/boolean)
90
+ * - RegExp literals
91
+ * - Array literals
92
+ * - Object literals
93
+ * - Template literals
94
+ * - Tagged template expressions
95
+ * - TS `as const` assertions wrapping the above (TSAsExpression)
96
+ * - Await expressions
97
+ *
98
+ * Any initializer not in the skip list is treated as potentially callable so
99
+ * we don't accidentally hide a renamed validator.
100
+ */
101
+ function isNonFunctionInitializer(init) {
102
+ if (!init) {
103
+ return true;
104
+ }
105
+ // Unwrap `as const` / type assertions — look at the underlying expression.
106
+ let expr = init;
107
+ while (expr.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
108
+ expr.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
109
+ expr.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
110
+ expr.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
111
+ expr = expr.expression;
112
+ }
113
+ switch (expr.type) {
114
+ case utils_1.AST_NODE_TYPES.Literal:
115
+ // string, number, boolean, null, bigint, regex
116
+ return true;
117
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
118
+ return true;
119
+ case utils_1.AST_NODE_TYPES.TaggedTemplateExpression:
120
+ return true;
121
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
122
+ return true;
123
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
124
+ return true;
125
+ default:
126
+ // Arrow functions, function expressions, call expressions, identifiers,
127
+ // member expressions, new expressions, etc. — all potentially callable.
128
+ return false;
129
+ }
130
+ }
131
+ exports.enforceIsPrefixValidators = (0, createRule_1.createRule)({
132
+ name: 'enforce-is-prefix-validators',
133
+ meta: {
134
+ type: 'suggestion',
135
+ docs: {
136
+ description: 'Enforce the `is` (or other allowed predicate) prefix for exported validators in **/validators/**/*.ts files',
137
+ recommended: 'error',
138
+ },
139
+ schema: [
140
+ {
141
+ type: 'object',
142
+ properties: {
143
+ targetPaths: {
144
+ type: 'array',
145
+ items: { type: 'string' },
146
+ },
147
+ requiredPrefix: {
148
+ type: 'string',
149
+ },
150
+ allowedPrefixes: {
151
+ type: 'array',
152
+ items: { type: 'string' },
153
+ },
154
+ disallowedPrefixes: {
155
+ type: 'array',
156
+ items: { type: 'string' },
157
+ },
158
+ excludeNames: {
159
+ type: 'array',
160
+ items: { type: 'string' },
161
+ },
162
+ excludePatterns: {
163
+ type: 'array',
164
+ items: { type: 'string' },
165
+ },
166
+ },
167
+ additionalProperties: false,
168
+ },
169
+ ],
170
+ messages: {
171
+ disallowedPrefix: 'Exported validator "{{name}}" uses the disallowed "{{prefix}}" prefix. Validators use the "{{requiredPrefix}}" prefix (e.g., "{{suggestion}}"). See .claude/skills/validation/SKILL.md.',
172
+ missingRequiredPrefix: 'Exported validator "{{name}}" must use the "{{requiredPrefix}}" prefix. Rename to "{{suggestion}}".',
173
+ },
174
+ },
175
+ defaultOptions: [{}],
176
+ create(context, [options]) {
177
+ const targetPaths = options.targetPaths ?? DEFAULT_TARGET_PATHS;
178
+ const requiredPrefix = options.requiredPrefix ?? DEFAULT_REQUIRED_PREFIX;
179
+ const allowedPrefixes = options.allowedPrefixes ?? DEFAULT_ALLOWED_PREFIXES;
180
+ const disallowedPrefixes = options.disallowedPrefixes ?? DEFAULT_DISALLOWED_PREFIXES;
181
+ const excludeNames = options.excludeNames ?? DEFAULT_EXCLUDE_NAMES;
182
+ const excludePatterns = options.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
183
+ const filename = context.getFilename();
184
+ // Build matchers once per file to avoid repeated Minimatch construction.
185
+ const targetMatchers = targetPaths.map((p) => new minimatch_1.Minimatch(p));
186
+ const excludeMatchers = excludePatterns.map((p) => new minimatch_1.Minimatch(p));
187
+ const isTargetFile = targetMatchers.some((mm) => mm.match(filename));
188
+ const isExcludedFile = excludeMatchers.some((mm) => mm.match(filename));
189
+ if (!isTargetFile || isExcludedFile) {
190
+ return {};
191
+ }
192
+ const excludeNamesSet = new Set(excludeNames);
193
+ /**
194
+ * Check a single exported name and report a violation if it does not meet
195
+ * the prefix requirements.
196
+ *
197
+ * @param init - The initializer expression for `export const` declarations,
198
+ * `DEFINITELY_CALLABLE` for function declarations / specifier exports
199
+ * that are definitely callable, or `undefined` when the callability is
200
+ * unknown (treated as non-callable to avoid false positives).
201
+ */
202
+ function checkName(name, reportNode, init) {
203
+ // Infrastructure or explicitly excluded names are always exempt.
204
+ if (excludeNamesSet.has(name)) {
205
+ return;
206
+ }
207
+ // Skip PascalCase names: these are typically class instances, type
208
+ // utilities, or infrastructure exports — not validator functions.
209
+ if (name.charAt(0) === name.charAt(0).toUpperCase() &&
210
+ /^[A-Z]/.test(name)) {
211
+ return;
212
+ }
213
+ // Non-function constants (primitives, regex, arrays, objects) are exempt
214
+ // from the naming requirement per Edge Case 1 in the spec.
215
+ // When init is DEFINITELY_CALLABLE (function declarations, specifier
216
+ // exports), skip the check and always enforce the prefix requirement.
217
+ if (init !== DEFINITELY_CALLABLE &&
218
+ isNonFunctionInitializer(init)) {
219
+ return;
220
+ }
221
+ // Check if the name uses an explicitly disallowed prefix.
222
+ const matchedDisallowed = disallowedPrefixes.find((prefix) => startsWithPrefix(name, prefix));
223
+ if (matchedDisallowed) {
224
+ const suggestion = suggestRename(name, matchedDisallowed, requiredPrefix);
225
+ context.report({
226
+ node: reportNode,
227
+ messageId: 'disallowedPrefix',
228
+ data: {
229
+ name,
230
+ prefix: matchedDisallowed,
231
+ requiredPrefix,
232
+ suggestion,
233
+ },
234
+ });
235
+ return;
236
+ }
237
+ // Check if the name starts with one of the allowed prefixes.
238
+ const hasAllowedPrefix = allowedPrefixes.some((prefix) => startsWithPrefix(name, prefix));
239
+ if (!hasAllowedPrefix) {
240
+ // Generate a best-effort suggestion using the required prefix.
241
+ const suggestion = `${requiredPrefix}${name
242
+ .charAt(0)
243
+ .toUpperCase()}${name.slice(1)}`;
244
+ context.report({
245
+ node: reportNode,
246
+ messageId: 'missingRequiredPrefix',
247
+ data: {
248
+ name,
249
+ requiredPrefix,
250
+ suggestion,
251
+ },
252
+ });
253
+ }
254
+ }
255
+ return {
256
+ ExportNamedDeclaration(node) {
257
+ // Skip type/interface/class/enum — these are not validator functions.
258
+ if (isTypeOrClassExport(node)) {
259
+ return;
260
+ }
261
+ // `export const foo = ...` or `export let foo = ...`
262
+ if (node.declaration?.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
263
+ for (const declarator of node.declaration.declarations) {
264
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier) {
265
+ checkName(declarator.id.name, declarator.id, declarator.init);
266
+ }
267
+ }
268
+ return;
269
+ }
270
+ // `export function foo(...) { ... }`
271
+ if (node.declaration?.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
272
+ node.declaration.id) {
273
+ // Function declarations are always callable.
274
+ checkName(node.declaration.id.name, node.declaration.id, DEFINITELY_CALLABLE);
275
+ return;
276
+ }
277
+ // `export { foo }` — re-exports of locally-defined names.
278
+ // We only flag names that are exported from the current file's own
279
+ // bindings. Specifiers with `source` (e.g., `export { x } from '...'`)
280
+ // re-export external names the author does not control, so we skip them
281
+ // to avoid false positives.
282
+ if (node.specifiers.length > 0 && node.source === null) {
283
+ for (const specifier of node.specifiers) {
284
+ if (specifier.type === utils_1.AST_NODE_TYPES.ExportSpecifier &&
285
+ specifier.exported.type === utils_1.AST_NODE_TYPES.Identifier) {
286
+ // For specifier-style exports, we cannot inspect the initializer
287
+ // here without scope analysis. We treat them as definitely callable
288
+ // (DEFINITELY_CALLABLE) so the prefix check always runs.
289
+ const exportedName = specifier.exported.name;
290
+ const localName = specifier.local.type === utils_1.AST_NODE_TYPES.Identifier
291
+ ? specifier.local.name
292
+ : exportedName;
293
+ // Skip re-exports of known infrastructure names.
294
+ if (!excludeNamesSet.has(localName)) {
295
+ checkName(exportedName, specifier.exported, DEFINITELY_CALLABLE);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ },
301
+ };
302
+ },
303
+ });
304
+ //# sourceMappingURL=enforce-is-prefix-validators.js.map
@@ -0,0 +1,12 @@
1
+ type Options = [
2
+ {
3
+ propsToCheck?: string[];
4
+ ignoredWords?: string[];
5
+ ignorePatterns?: string[];
6
+ allowList?: string[];
7
+ checkJsxText?: boolean;
8
+ }
9
+ ];
10
+ type MessageIds = 'titleCase' | 'allCaps';
11
+ export declare const enforceM3SentenceCase: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
12
+ export {};