@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.
- 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/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 +18 -26
- 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/react-memoize-literals.js +106 -1
- 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,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 {};
|