@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.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 +28 -5
- package/lib/index.js +7 -1
- package/lib/rules/consistent-callback-naming.js +26 -0
- package/lib/rules/dynamic-https-errors.js +5 -26
- package/lib/rules/enforce-assert-safe-object-key.js +29 -0
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
- package/lib/rules/enforce-dynamic-imports.d.ts +5 -1
- package/lib/rules/enforce-dynamic-imports.js +90 -19
- package/lib/rules/enforce-memoize-async.js +1 -4
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-verb-noun-naming.js +3 -0
- package/lib/rules/global-const-style.js +9 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
- package/lib/rules/memo-nested-react-components.js +143 -8
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-circular-references.js +145 -482
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-entire-object-hook-deps.js +132 -97
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +1 -3
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/react-memoize-literals.js +87 -1
- package/lib/rules/require-memo.js +8 -0
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +15 -0
- package/lib/utils/ASTHelpers.js +48 -0
- package/package.json +7 -6
- package/release-manifest.json +196 -0
- package/lib/rules/prefer-memoized-props.d.ts +0 -3
- package/lib/rules/prefer-memoized-props.js +0 -445
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +0 -7
- package/lib/rules/prefer-nullish-coalescing-override.js +0 -188
- package/lib/rules/require-usememo-object-literals.d.ts +0 -4
- package/lib/rules/require-usememo-object-literals.js +0 -64
|
@@ -33,6 +33,14 @@ const DEFAULT_BOOLEAN_PREFIXES = [
|
|
|
33
33
|
const DEFAULT_OPTIONS = {
|
|
34
34
|
prefixes: DEFAULT_BOOLEAN_PREFIXES,
|
|
35
35
|
ignoreOverriddenGetters: false,
|
|
36
|
+
// Property names declared in interfaces and type aliases are frequently
|
|
37
|
+
// dictated by contracts the author cannot rename: external API request/response
|
|
38
|
+
// shapes, third-party library interfaces, and persisted data-model schemas
|
|
39
|
+
// (e.g. Firestore document fields). Flagging those produces unavoidable false
|
|
40
|
+
// positives, so property-signature enforcement is opt-in. Names the author does
|
|
41
|
+
// choose freshly — variables, function returns, class fields, parameters — stay
|
|
42
|
+
// enforced by default.
|
|
43
|
+
enforceForPropertySignatures: false,
|
|
36
44
|
};
|
|
37
45
|
const BOOLEANISH_BINARY_OPERATORS = new Set(['===', '!==', '==', '!=', '>', '<', '>=', '<=', 'in', 'instanceof']);
|
|
38
46
|
exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
@@ -58,6 +66,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
58
66
|
type: 'boolean',
|
|
59
67
|
default: false,
|
|
60
68
|
},
|
|
69
|
+
enforceForPropertySignatures: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
61
73
|
},
|
|
62
74
|
additionalProperties: false,
|
|
63
75
|
},
|
|
@@ -71,8 +83,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
71
83
|
defaultOptions: [DEFAULT_OPTIONS],
|
|
72
84
|
create(context, [options]) {
|
|
73
85
|
const approvedPrefixes = options.prefixes || DEFAULT_OPTIONS.prefixes;
|
|
86
|
+
const approvedPrefixesWithoutAsserts = approvedPrefixes.filter((p) => p !== 'asserts');
|
|
74
87
|
const ignoreOverriddenGetters = options.ignoreOverriddenGetters ??
|
|
75
88
|
DEFAULT_OPTIONS.ignoreOverriddenGetters;
|
|
89
|
+
const enforceForPropertySignatures = options.enforceForPropertySignatures ??
|
|
90
|
+
DEFAULT_OPTIONS.enforceForPropertySignatures;
|
|
76
91
|
function findVariableInScopes(name) {
|
|
77
92
|
let currentScope = context.getScope();
|
|
78
93
|
while (currentScope) {
|
|
@@ -83,32 +98,58 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
83
98
|
}
|
|
84
99
|
return undefined;
|
|
85
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if a name is prefixed by a boolean keyword with proper boundaries.
|
|
103
|
+
* Supports camelCase (isSomething), snake_case (is_something, IS_SOMETHING),
|
|
104
|
+
* and exact matches.
|
|
105
|
+
*/
|
|
106
|
+
function isPrefixedByBooleanKeyword(name, prefixes) {
|
|
107
|
+
const normalizedName = name.startsWith('_') ? name.slice(1) : name;
|
|
108
|
+
const checkPrefix = (p) => {
|
|
109
|
+
if (!normalizedName.toLowerCase().startsWith(p.toLowerCase())) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (normalizedName.length === p.length) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const nextChar = normalizedName.charAt(p.length);
|
|
116
|
+
// For SCREAMING_SNAKE_CASE or similar all-uppercase names,
|
|
117
|
+
// we require an underscore boundary.
|
|
118
|
+
const isAllUppercase = normalizedName === normalizedName.toUpperCase() &&
|
|
119
|
+
/[a-z]/i.test(normalizedName);
|
|
120
|
+
if (isAllUppercase) {
|
|
121
|
+
return nextChar === '_';
|
|
122
|
+
}
|
|
123
|
+
// For camelCase, the next char must be uppercase, a digit, or $
|
|
124
|
+
return (nextChar === '_' ||
|
|
125
|
+
nextChar === '$' ||
|
|
126
|
+
(nextChar >= '0' && nextChar <= '9') ||
|
|
127
|
+
(nextChar === nextChar.toUpperCase() &&
|
|
128
|
+
nextChar !== nextChar.toLowerCase()));
|
|
129
|
+
};
|
|
130
|
+
const checkPrefixWithPlural = (p) => {
|
|
131
|
+
if (checkPrefix(p))
|
|
132
|
+
return true;
|
|
133
|
+
if (['is', 'has', 'does', 'was', 'had', 'did'].includes(p)) {
|
|
134
|
+
const pluralPrefix = pluralize_1.default.plural(p);
|
|
135
|
+
if (checkPrefix(pluralPrefix))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
};
|
|
140
|
+
return prefixes.some((prefix) => checkPrefixWithPlural(prefix));
|
|
141
|
+
}
|
|
86
142
|
/**
|
|
87
143
|
* Check if a name starts with any of the approved prefixes, their plural forms,
|
|
88
144
|
* or if it starts with an underscore (which indicates a private/internal property)
|
|
89
145
|
*/
|
|
90
146
|
function hasApprovedPrefix(name, options) {
|
|
91
147
|
const treatLeadingUnderscoreAsApproved = options?.treatLeadingUnderscoreAsApproved ?? true;
|
|
92
|
-
const normalizedName = name.startsWith('_') ? name.slice(1) : name;
|
|
93
148
|
// Skip checking properties that start with an underscore (private/internal properties)
|
|
94
149
|
if (treatLeadingUnderscoreAsApproved && name.startsWith('_')) {
|
|
95
150
|
return true;
|
|
96
151
|
}
|
|
97
|
-
return approvedPrefixes
|
|
98
|
-
// Check for exact prefix match
|
|
99
|
-
if (normalizedName.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
// Check for plural form of the prefix
|
|
103
|
-
// Only apply pluralization to certain prefixes that have meaningful plural forms
|
|
104
|
-
if (['is', 'has', 'does', 'was', 'had', 'did'].includes(prefix)) {
|
|
105
|
-
const pluralPrefix = pluralize_1.default.plural(prefix);
|
|
106
|
-
return normalizedName
|
|
107
|
-
.toLowerCase()
|
|
108
|
-
.startsWith(pluralPrefix.toLowerCase());
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
});
|
|
152
|
+
return isPrefixedByBooleanKeyword(name, approvedPrefixes);
|
|
112
153
|
}
|
|
113
154
|
function nameSuggestsBoolean(name) {
|
|
114
155
|
const normalizedName = name.startsWith('_') ? name.slice(1) : name;
|
|
@@ -125,9 +166,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
125
166
|
'authenticated',
|
|
126
167
|
'authorized',
|
|
127
168
|
];
|
|
128
|
-
return (
|
|
129
|
-
|
|
130
|
-
}) || suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
|
|
169
|
+
return (isPrefixedByBooleanKeyword(normalizedName, approvedPrefixes) ||
|
|
170
|
+
suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
|
|
131
171
|
}
|
|
132
172
|
/**
|
|
133
173
|
* Capitalize the first letter of a name for use in suggested alternatives
|
|
@@ -205,94 +245,20 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
205
245
|
// Check for logical expressions (&&)
|
|
206
246
|
if (node.init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
207
247
|
node.init.operator === '&&') {
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const calleeName = rightSide.callee.name;
|
|
214
|
-
const lowerCallee = calleeName.toLowerCase();
|
|
215
|
-
// For assert*-style utilities, only treat as boolean if we can confirm boolean return type
|
|
216
|
-
if (lowerCallee.startsWith('assert')) {
|
|
217
|
-
return identifierReturnsBoolean(calleeName);
|
|
218
|
-
}
|
|
219
|
-
// Otherwise, infer based on naming heuristics
|
|
220
|
-
const isBooleanCall = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
|
|
221
|
-
lowerCallee.startsWith(prefix.toLowerCase()));
|
|
222
|
-
if (isBooleanCall ||
|
|
223
|
-
lowerCallee.includes('boolean') ||
|
|
224
|
-
lowerCallee.includes('enabled') ||
|
|
225
|
-
lowerCallee.includes('auth') ||
|
|
226
|
-
lowerCallee.includes('valid') ||
|
|
227
|
-
lowerCallee.includes('check')) {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
if (lowerCallee.startsWith('get') ||
|
|
231
|
-
lowerCallee.startsWith('fetch') ||
|
|
232
|
-
lowerCallee.startsWith('retrieve') ||
|
|
233
|
-
lowerCallee.startsWith('load') ||
|
|
234
|
-
lowerCallee.startsWith('read')) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// If the method name doesn't suggest it returns a boolean, don't flag it
|
|
239
|
-
if (rightSide.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
240
|
-
rightSide.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
241
|
-
const methodName = rightSide.callee.property.name;
|
|
242
|
-
const lowerMethodName = methodName.toLowerCase();
|
|
243
|
-
// Ignore assert*-style methods which often return the input value
|
|
244
|
-
if (lowerMethodName.startsWith('assert')) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
// Check if the method name suggests it returns a boolean
|
|
248
|
-
const isBooleanMethod = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
|
|
249
|
-
lowerMethodName.startsWith(prefix.toLowerCase()));
|
|
250
|
-
// If the method name suggests it returns a boolean (starts with a boolean prefix or contains 'boolean' or 'enabled'),
|
|
251
|
-
// then the variable should be treated as a boolean
|
|
252
|
-
if (isBooleanMethod ||
|
|
253
|
-
lowerMethodName.includes('boolean') ||
|
|
254
|
-
lowerMethodName.includes('enabled') ||
|
|
255
|
-
lowerMethodName.includes('auth') ||
|
|
256
|
-
lowerMethodName.includes('valid') ||
|
|
257
|
-
lowerMethodName.includes('check')) {
|
|
258
|
-
return true;
|
|
259
|
-
}
|
|
260
|
-
// For methods like getVolume(), getData(), etc., assume they return non-boolean values
|
|
261
|
-
if (lowerMethodName.startsWith('get') ||
|
|
262
|
-
lowerMethodName.startsWith('fetch') ||
|
|
263
|
-
lowerMethodName.startsWith('retrieve') ||
|
|
264
|
-
lowerMethodName.startsWith('load') ||
|
|
265
|
-
lowerMethodName.startsWith('read')) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
248
|
+
const left = evaluateBooleanishExpression(node.init.left);
|
|
249
|
+
const right = evaluateBooleanishExpression(node.init.right);
|
|
250
|
+
// If both sides are boolean, the result is boolean.
|
|
251
|
+
if (left === 'boolean' && right === 'boolean') {
|
|
252
|
+
return true;
|
|
269
253
|
}
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (lowerPropertyName.includes('parent') ||
|
|
277
|
-
lowerPropertyName.includes('element') ||
|
|
278
|
-
lowerPropertyName.includes('node') ||
|
|
279
|
-
lowerPropertyName.includes('child') ||
|
|
280
|
-
lowerPropertyName.includes('sibling')) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
// Ignore assert*-style properties which often return the input value
|
|
284
|
-
if (lowerPropertyName.startsWith('assert')) {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
// For property access like user.isAuthenticated, treat as boolean
|
|
288
|
-
const isBooleanProperty = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
|
|
289
|
-
lowerPropertyName.startsWith(prefix.toLowerCase()));
|
|
290
|
-
if (isBooleanProperty) {
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
254
|
+
// If the right side is boolean, and the left side is unknown (but not non-boolean),
|
|
255
|
+
// we treat it as boolean to avoid false negatives for common patterns like `user && user.isActive`.
|
|
256
|
+
// This is a trade-off: it might cause some false positives for non-boolean variables
|
|
257
|
+
// used as guards, but those are less common than the `user && user.isActive` pattern.
|
|
258
|
+
if (right === 'boolean' && left === 'unknown') {
|
|
259
|
+
return true;
|
|
293
260
|
}
|
|
294
|
-
|
|
295
|
-
return true;
|
|
261
|
+
return false;
|
|
296
262
|
}
|
|
297
263
|
// Special case for logical OR (||) - only consider it boolean if:
|
|
298
264
|
// 1. It's used with boolean literals or
|
|
@@ -330,8 +296,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
330
296
|
if (lowerCallee.startsWith('assert')) {
|
|
331
297
|
return identifierReturnsBoolean(calleeName);
|
|
332
298
|
}
|
|
333
|
-
return
|
|
334
|
-
lowerCallee.startsWith(prefix.toLowerCase()));
|
|
299
|
+
return isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
|
|
335
300
|
}
|
|
336
301
|
// Default to false for other cases with || to avoid false positives
|
|
337
302
|
return false;
|
|
@@ -351,8 +316,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
351
316
|
return identifierReturnsBoolean(calleeName);
|
|
352
317
|
}
|
|
353
318
|
// Check if the function name suggests it returns a boolean
|
|
354
|
-
return approvedPrefixes.
|
|
355
|
-
lowerCallee.startsWith(prefix.toLowerCase()));
|
|
319
|
+
return isPrefixedByBooleanKeyword(calleeName, approvedPrefixes.filter((p) => p !== 'asserts'));
|
|
356
320
|
}
|
|
357
321
|
}
|
|
358
322
|
return false;
|
|
@@ -435,8 +399,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
435
399
|
if (lowerCallee.startsWith('assert')) {
|
|
436
400
|
return identifierReturnsBoolean(calleeName) ? 'boolean' : 'unknown';
|
|
437
401
|
}
|
|
438
|
-
const matchesPrefix =
|
|
439
|
-
lowerCallee.startsWith(prefix.toLowerCase()));
|
|
402
|
+
const matchesPrefix = isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
|
|
440
403
|
if (matchesPrefix ||
|
|
441
404
|
lowerCallee.includes('boolean') ||
|
|
442
405
|
lowerCallee.includes('enabled') ||
|
|
@@ -460,8 +423,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
460
423
|
if (lowerMethodName.startsWith('assert')) {
|
|
461
424
|
return 'unknown';
|
|
462
425
|
}
|
|
463
|
-
const matchesPrefix =
|
|
464
|
-
lowerMethodName.startsWith(prefix.toLowerCase()));
|
|
426
|
+
const matchesPrefix = isPrefixedByBooleanKeyword(methodName, approvedPrefixesWithoutAsserts);
|
|
465
427
|
if (matchesPrefix ||
|
|
466
428
|
lowerMethodName.includes('boolean') ||
|
|
467
429
|
lowerMethodName.includes('enabled') ||
|
|
@@ -475,7 +437,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
475
437
|
}
|
|
476
438
|
function evaluateBooleanishExpression(expression) {
|
|
477
439
|
if (!expression)
|
|
478
|
-
return '
|
|
440
|
+
return 'unknown';
|
|
479
441
|
let currentExpression = expression;
|
|
480
442
|
if (currentExpression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
481
443
|
currentExpression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
@@ -825,10 +787,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
825
787
|
});
|
|
826
788
|
}
|
|
827
789
|
/**
|
|
828
|
-
* Check if a variable is
|
|
790
|
+
* Check if a variable is initialized with a boolean-suggesting member expression
|
|
829
791
|
* This helps identify variables that should be flagged as needing a boolean prefix
|
|
830
792
|
*/
|
|
831
|
-
function
|
|
793
|
+
function isLikelyBooleanByMemberExpression(node) {
|
|
832
794
|
// Check if the variable is initialized with a boolean-related value
|
|
833
795
|
const variableDeclarator = node.parent;
|
|
834
796
|
if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
@@ -844,7 +806,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
844
806
|
init.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
845
807
|
const propertyName = init.property.name;
|
|
846
808
|
// If the property name suggests it's a boolean (starts with a boolean prefix)
|
|
847
|
-
const isBooleanProperty =
|
|
809
|
+
const isBooleanProperty = isPrefixedByBooleanKeyword(propertyName, approvedPrefixes);
|
|
848
810
|
if (isBooleanProperty) {
|
|
849
811
|
return true;
|
|
850
812
|
}
|
|
@@ -895,8 +857,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
895
857
|
return;
|
|
896
858
|
// Check if it's a boolean variable
|
|
897
859
|
let isBooleanVar = hasBooleanTypeAnnotation(node.id) || hasInitialBooleanValue(node);
|
|
898
|
-
// Check if it's a boolean variable used in a while loop
|
|
899
|
-
if (!isBooleanVar &&
|
|
860
|
+
// Check if it's a boolean variable used in a while loop or initialized with a boolean property
|
|
861
|
+
if (!isBooleanVar && isLikelyBooleanByMemberExpression(node.id)) {
|
|
900
862
|
isBooleanVar = true;
|
|
901
863
|
}
|
|
902
864
|
// Check if it's an arrow function with boolean return type
|
|
@@ -1074,6 +1036,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1074
1036
|
* Check property signatures in interfaces/types for boolean types
|
|
1075
1037
|
*/
|
|
1076
1038
|
function checkPropertySignature(node) {
|
|
1039
|
+
// Interface/type-alias property names are commonly imposed by contracts the
|
|
1040
|
+
// author cannot rename (external APIs, data-model schemas), so they are only
|
|
1041
|
+
// enforced when explicitly opted in.
|
|
1042
|
+
if (!enforceForPropertySignatures)
|
|
1043
|
+
return;
|
|
1077
1044
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1078
1045
|
return;
|
|
1079
1046
|
const propertyName = node.key.name;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export declare const RULE_NAME = "enforce-dynamic-imports";
|
|
2
2
|
type Options = [
|
|
3
3
|
{
|
|
4
|
-
libraries
|
|
4
|
+
libraries?: string[];
|
|
5
|
+
ignoredLibraries?: string[];
|
|
6
|
+
internalPrefixes?: string[];
|
|
5
7
|
allowImportType?: boolean;
|
|
6
8
|
}
|
|
7
9
|
];
|
|
10
|
+
export declare const DEFAULT_IGNORED_LIBRARIES: string[];
|
|
11
|
+
export declare const DEFAULT_INTERNAL_PREFIXES: string[];
|
|
8
12
|
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
9
13
|
export default _default;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RULE_NAME = void 0;
|
|
3
|
+
exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const module_1 = require("module");
|
|
5
7
|
exports.RULE_NAME = 'enforce-dynamic-imports';
|
|
8
|
+
exports.DEFAULT_IGNORED_LIBRARIES = [
|
|
9
|
+
'react',
|
|
10
|
+
'react/**',
|
|
11
|
+
'react-dom',
|
|
12
|
+
'react-dom/**',
|
|
13
|
+
'next',
|
|
14
|
+
'next/**',
|
|
15
|
+
'@mui/material',
|
|
16
|
+
'@mui/material/**',
|
|
17
|
+
'@mui/icons-material',
|
|
18
|
+
'@mui/icons-material/**',
|
|
19
|
+
'@emotion/**',
|
|
20
|
+
'clsx',
|
|
21
|
+
'tailwind-merge',
|
|
22
|
+
];
|
|
23
|
+
exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
|
|
24
|
+
// Pre-built set of Node.js core module names for O(1) lookup.
|
|
25
|
+
const NODE_BUILTINS = new Set(module_1.builtinModules);
|
|
26
|
+
// Returns true for any source that resolves to a Node builtin: bare name
|
|
27
|
+
// ('crypto'), node:-prefixed ('node:fs'), or path form ('fs/promises').
|
|
28
|
+
const isNodeBuiltin = (source) => source.startsWith('node:') ||
|
|
29
|
+
NODE_BUILTINS.has(source) ||
|
|
30
|
+
NODE_BUILTINS.has(source.split('/')[0]);
|
|
6
31
|
exports.default = (0, createRule_1.createRule)({
|
|
7
32
|
name: exports.RULE_NAME,
|
|
8
33
|
meta: {
|
|
9
34
|
type: 'suggestion',
|
|
10
35
|
docs: {
|
|
11
|
-
description: 'Enforce dynamic imports for
|
|
36
|
+
description: 'Enforce dynamic imports for external libraries by default to optimize bundle size, unless explicitly ignored',
|
|
12
37
|
recommended: 'error',
|
|
13
38
|
},
|
|
14
39
|
schema: [
|
|
@@ -21,6 +46,18 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
21
46
|
type: 'string',
|
|
22
47
|
},
|
|
23
48
|
},
|
|
49
|
+
ignoredLibraries: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
internalPrefixes: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
24
61
|
allowImportType: {
|
|
25
62
|
type: 'boolean',
|
|
26
63
|
},
|
|
@@ -29,46 +66,80 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
29
66
|
},
|
|
30
67
|
],
|
|
31
68
|
messages: {
|
|
32
|
-
dynamicImportRequired: 'Static import from "{{source}}"
|
|
69
|
+
dynamicImportRequired: 'Static import from "{{source}}" loads the full package into the initial bundle. → This increases download size and delays first render, undermining our lazy‑loading pattern for external dependencies. → Use a dynamic import (e.g., useDynamic(() => import("{{source}}"))), add "{{source}}" to ignoredLibraries for intentional static usage, or use a type‑only import when you only need types.',
|
|
33
70
|
},
|
|
34
71
|
},
|
|
35
72
|
defaultOptions: [
|
|
36
73
|
{
|
|
37
|
-
|
|
74
|
+
ignoredLibraries: exports.DEFAULT_IGNORED_LIBRARIES,
|
|
75
|
+
internalPrefixes: exports.DEFAULT_INTERNAL_PREFIXES,
|
|
38
76
|
allowImportType: true,
|
|
39
77
|
},
|
|
40
78
|
],
|
|
41
79
|
create(context, [options]) {
|
|
42
|
-
const { libraries, allowImportType = true } = options;
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
const { libraries, ignoredLibraries = exports.DEFAULT_IGNORED_LIBRARIES, internalPrefixes = exports.DEFAULT_INTERNAL_PREFIXES, allowImportType = true, } = options;
|
|
81
|
+
// When `libraries` is provided, the rule operates in whitelist mode:
|
|
82
|
+
// only the explicitly listed libraries are enforced. This preserves
|
|
83
|
+
// backwards-compatibility with pre-1.16.0 consumer configurations.
|
|
84
|
+
// When `libraries` is absent, enforce-by-default mode applies:
|
|
85
|
+
// all external imports are flagged unless in `ignoredLibraries`.
|
|
86
|
+
const isWhitelistMode = libraries !== undefined;
|
|
87
|
+
// Build an O(1) + glob matcher from a list of library patterns.
|
|
88
|
+
const buildMatcher = (list) => {
|
|
89
|
+
const exactSet = new Set();
|
|
90
|
+
const globs = [];
|
|
91
|
+
for (const lib of list) {
|
|
92
|
+
const mm = new minimatch_1.Minimatch(lib);
|
|
93
|
+
if (mm.hasMagic()) {
|
|
94
|
+
globs.push(mm);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
exactSet.add(lib);
|
|
51
98
|
}
|
|
52
|
-
|
|
53
|
-
|
|
99
|
+
}
|
|
100
|
+
return (source) => exactSet.has(source) || globs.some((mm) => mm.match(source));
|
|
101
|
+
};
|
|
102
|
+
// In whitelist mode, `libraries` is defined (checked above). In
|
|
103
|
+
// enforce-by-default mode, `ignoredLibraries` is used instead.
|
|
104
|
+
const isListedInWhitelist = buildMatcher(libraries ?? []);
|
|
105
|
+
const isIgnoredLibrary = buildMatcher(ignoredLibraries);
|
|
106
|
+
// A source is external only if it looks like an npm package specifier AND
|
|
107
|
+
// is not a known-internal path. Node builtins and configured internal
|
|
108
|
+
// prefixes (e.g. src/, functions/) are excluded to avoid false positives
|
|
109
|
+
// on TypeScript baseUrl imports and Node core modules.
|
|
110
|
+
const isExternal = (source) => {
|
|
111
|
+
return (/^[a-z0-9@]/i.test(source) &&
|
|
112
|
+
!source.startsWith('@/') &&
|
|
113
|
+
!isNodeBuiltin(source) &&
|
|
114
|
+
!internalPrefixes.some((prefix) => source.startsWith(prefix)));
|
|
54
115
|
};
|
|
55
116
|
return {
|
|
56
117
|
ImportDeclaration(node) {
|
|
57
118
|
const importSource = node.source.value;
|
|
58
119
|
// Skip type-only imports if allowed
|
|
59
120
|
if (allowImportType) {
|
|
60
|
-
// Check if it's a type-only import declaration
|
|
61
121
|
if (node.importKind === 'type') {
|
|
62
122
|
return;
|
|
63
123
|
}
|
|
64
|
-
// Check if all specifiers are type imports
|
|
65
124
|
if (node.specifiers.length > 0 &&
|
|
66
125
|
node.specifiers.every((spec) => spec.type === 'ImportSpecifier' && spec.importKind === 'type')) {
|
|
67
126
|
return;
|
|
68
127
|
}
|
|
69
128
|
}
|
|
70
|
-
|
|
71
|
-
if (
|
|
129
|
+
let shouldReport;
|
|
130
|
+
if (isWhitelistMode) {
|
|
131
|
+
// Whitelist mode: report only if the source matches an explicitly
|
|
132
|
+
// listed library. External detection and ignoredLibraries are not
|
|
133
|
+
// consulted, so unlisted npm packages are silently allowed.
|
|
134
|
+
shouldReport = isListedInWhitelist(importSource);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Enforce-by-default mode: report if the source is an external
|
|
138
|
+
// package and is not in the ignored list.
|
|
139
|
+
shouldReport =
|
|
140
|
+
isExternal(importSource) && !isIgnoredLibrary(importSource);
|
|
141
|
+
}
|
|
142
|
+
if (shouldReport) {
|
|
72
143
|
context.report({
|
|
73
144
|
node,
|
|
74
145
|
messageId: 'dynamicImportRequired',
|
|
@@ -4,10 +4,7 @@ exports.enforceMemoizeAsync = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const MEMOIZE_MODULE = '@blumintinc/typescript-memoize';
|
|
7
|
-
const ALLOWED_MEMOIZE_MODULES = new Set([
|
|
8
|
-
MEMOIZE_MODULE,
|
|
9
|
-
'typescript-memoize',
|
|
10
|
-
]);
|
|
7
|
+
const ALLOWED_MEMOIZE_MODULES = new Set([MEMOIZE_MODULE, 'typescript-memoize']);
|
|
11
8
|
/**
|
|
12
9
|
* Matches a memoize decorator in supported syntaxes:
|
|
13
10
|
* - @Alias()
|
|
@@ -3,6 +3,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMuiRoundedIcons = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
/**
|
|
7
|
+
* Non-Rounded MUI variant suffixes. An icon imported as one of these variants
|
|
8
|
+
* (e.g. `AddReactionOutlined`) maps to the Rounded variant of its BASE name
|
|
9
|
+
* (`AddReactionRounded`), not `AddReactionOutlinedRounded` (which doesn't exist).
|
|
10
|
+
* Matched exactly, so a distinct icon like `MailOutline` (ends in "Outline", not
|
|
11
|
+
* "Outlined") is left intact → `MailOutlineRounded`, which does exist.
|
|
12
|
+
*/
|
|
13
|
+
const VARIANT_SUFFIXES = ['Outlined', 'Sharp', 'TwoTone'];
|
|
14
|
+
/**
|
|
15
|
+
* @mui/icons-material brand icons that ship in a single (Filled) variant only —
|
|
16
|
+
* they have no Rounded counterpart, so the rule must not demand one. Computed
|
|
17
|
+
* from @mui/icons-material (the only base icons lacking a `*Rounded` sibling).
|
|
18
|
+
*/
|
|
19
|
+
const ICONS_WITHOUT_ROUNDED = new Set([
|
|
20
|
+
'Apple',
|
|
21
|
+
'GitHub',
|
|
22
|
+
'Google',
|
|
23
|
+
'Instagram',
|
|
24
|
+
'LinkedIn',
|
|
25
|
+
'Microsoft',
|
|
26
|
+
'Pinterest',
|
|
27
|
+
'Reddit',
|
|
28
|
+
'Telegram',
|
|
29
|
+
'Twitter',
|
|
30
|
+
'WhatsApp',
|
|
31
|
+
'X',
|
|
32
|
+
'YouTube',
|
|
33
|
+
]);
|
|
34
|
+
/** Strips a trailing non-Rounded variant suffix to recover the base icon name. */
|
|
35
|
+
const toBaseIconName = (iconName) => {
|
|
36
|
+
const suffix = VARIANT_SUFFIXES.find((variant) => iconName.endsWith(variant));
|
|
37
|
+
return suffix ? iconName.slice(0, -suffix.length) : iconName;
|
|
38
|
+
};
|
|
6
39
|
exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
7
40
|
name: 'enforce-mui-rounded-icons',
|
|
8
41
|
meta: {
|
|
@@ -36,8 +69,16 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
|
36
69
|
if (!iconName) {
|
|
37
70
|
return;
|
|
38
71
|
}
|
|
72
|
+
// Map a non-Rounded variant import to its base icon so the Rounded
|
|
73
|
+
// suggestion/fix targets a name that actually exists.
|
|
74
|
+
const baseIconName = toBaseIconName(iconName);
|
|
75
|
+
// Brand icons have no Rounded variant — demanding one is a false
|
|
76
|
+
// positive and the fix would point at a non-existent module.
|
|
77
|
+
if (ICONS_WITHOUT_ROUNDED.has(baseIconName)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
39
80
|
// Create the rounded variant name
|
|
40
|
-
const roundedVariant = `${
|
|
81
|
+
const roundedVariant = `${baseIconName}Rounded`;
|
|
41
82
|
context.report({
|
|
42
83
|
node,
|
|
43
84
|
messageId: 'enforceRoundedVariant',
|
|
@@ -20,6 +20,7 @@ const ALLOWLIST = {
|
|
|
20
20
|
domain: new Set([
|
|
21
21
|
'ai',
|
|
22
22
|
'blumint',
|
|
23
|
+
'main',
|
|
23
24
|
'middleware',
|
|
24
25
|
'noop',
|
|
25
26
|
'nth',
|
|
@@ -406,6 +407,8 @@ const ALLOWLIST = {
|
|
|
406
407
|
'brush',
|
|
407
408
|
'bubble',
|
|
408
409
|
'buck',
|
|
410
|
+
'bucket',
|
|
411
|
+
'bucketize',
|
|
409
412
|
'buckle',
|
|
410
413
|
'bud',
|
|
411
414
|
'budge',
|
|
@@ -158,6 +158,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
158
158
|
if (target.type === utils_1.AST_NODE_TYPES.Literal && 'regex' in target) {
|
|
159
159
|
return false;
|
|
160
160
|
}
|
|
161
|
+
// Skip null and boolean literals. `null as const` is invalid
|
|
162
|
+
// TypeScript (TS1355), so the autofix would produce uncompilable
|
|
163
|
+
// code; `true`/`false` already have literal types, so `as const`
|
|
164
|
+
// is redundant. (`undefined` is an Identifier, not a Literal, so
|
|
165
|
+
// it never reaches the literal branch below.)
|
|
166
|
+
if (target.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
167
|
+
(target.value === null || typeof target.value === 'boolean')) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
161
170
|
return (target.type === utils_1.AST_NODE_TYPES.Literal ||
|
|
162
171
|
target.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
163
172
|
target.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|