@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
|
@@ -32,6 +32,21 @@ const COMPOSITING_VALUES = new Set([
|
|
|
32
32
|
'translateZ',
|
|
33
33
|
'transparent',
|
|
34
34
|
]);
|
|
35
|
+
// CSS reset/identity values that explicitly DON'T promote a layer for a given
|
|
36
|
+
// property. These are the opt-out counterparts to COMPOSITING_VALUES: `none`
|
|
37
|
+
// removes the effect, `auto`/global keywords disable the hint, and the default
|
|
38
|
+
// keyword leaves the element un-promoted. Keyed by normalized property name so
|
|
39
|
+
// the allowlist stays property-specific (e.g. `none` clears `transform` but is
|
|
40
|
+
// not a valid no-op for `opacity`).
|
|
41
|
+
const NON_COMPOSITING_VALUES = {
|
|
42
|
+
filter: new Set(['none']),
|
|
43
|
+
'backdrop-filter': new Set(['none']),
|
|
44
|
+
transform: new Set(['none']),
|
|
45
|
+
contain: new Set(['none']),
|
|
46
|
+
perspective: new Set(['none']),
|
|
47
|
+
'will-change': new Set(['auto', 'unset', 'initial', 'inherit', 'revert']),
|
|
48
|
+
'backface-visibility': new Set(['visible']),
|
|
49
|
+
};
|
|
35
50
|
exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
36
51
|
name: 'no-compositing-layer-props',
|
|
37
52
|
meta: {
|
|
@@ -54,6 +69,14 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
54
69
|
value.includes('scale3d') ||
|
|
55
70
|
value.includes('translateZ'));
|
|
56
71
|
}
|
|
72
|
+
// Strip `!important` and normalize casing so reset/identity values written
|
|
73
|
+
// as e.g. `none !important` are still recognized as non-promoting.
|
|
74
|
+
function normalizePropertyValue(value) {
|
|
75
|
+
return value
|
|
76
|
+
.replace(/\s*!important\s*$/i, '')
|
|
77
|
+
.trim()
|
|
78
|
+
.toLowerCase();
|
|
79
|
+
}
|
|
57
80
|
function checkProperty(propertyName, propertyValue) {
|
|
58
81
|
const normalizedName = normalizePropertyName(propertyName);
|
|
59
82
|
if (COMPOSITING_PROPERTIES.has(normalizedName)) {
|
|
@@ -66,6 +89,14 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
66
89
|
return false;
|
|
67
90
|
return numValue > 0 && numValue < 1;
|
|
68
91
|
}
|
|
92
|
+
// CSS reset/identity values can't create a compositing layer, so don't
|
|
93
|
+
// flag them (mirrors the opacity value-awareness for the other props).
|
|
94
|
+
const allowedValues = NON_COMPOSITING_VALUES[normalizedName];
|
|
95
|
+
if (allowedValues &&
|
|
96
|
+
propertyValue &&
|
|
97
|
+
allowedValues.has(normalizePropertyValue(propertyValue))) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
69
100
|
return true;
|
|
70
101
|
}
|
|
71
102
|
if (propertyValue && checkPropertyValue(propertyValue)) {
|
|
@@ -55,6 +55,16 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function unwrapExpression(expr) {
|
|
59
|
+
let current = expr;
|
|
60
|
+
while (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
61
|
+
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
62
|
+
current.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
63
|
+
current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
64
|
+
current = current.expression;
|
|
65
|
+
}
|
|
66
|
+
return current;
|
|
67
|
+
}
|
|
58
68
|
function getObjectUsagesInHook(hookBody, objectName,
|
|
59
69
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
70
|
context) {
|
|
@@ -126,12 +136,12 @@ context) {
|
|
|
126
136
|
// For computed properties with variables (like user[key]), we need the entire object
|
|
127
137
|
if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
128
138
|
// Check if this is accessing our target object
|
|
129
|
-
let
|
|
130
|
-
while (
|
|
131
|
-
|
|
139
|
+
let currentBase = unwrapExpression(memberExpr.object);
|
|
140
|
+
while (currentBase.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
141
|
+
currentBase = unwrapExpression(currentBase.object);
|
|
132
142
|
}
|
|
133
|
-
if (
|
|
134
|
-
|
|
143
|
+
if (currentBase.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
144
|
+
currentBase.name === objectName) {
|
|
135
145
|
// This is a computed property access on our target object, so we need the entire object
|
|
136
146
|
needsEntireObject = true;
|
|
137
147
|
}
|
|
@@ -173,29 +183,29 @@ context) {
|
|
|
173
183
|
(ARRAY_METHODS.has(memberExpr.property.name) ||
|
|
174
184
|
STRING_METHODS.has(memberExpr.property.name))) {
|
|
175
185
|
// Check if this is accessing our target object or a property of it
|
|
176
|
-
let
|
|
186
|
+
let currentBase = unwrapExpression(memberExpr.object);
|
|
177
187
|
let pathParts = [];
|
|
178
|
-
let
|
|
188
|
+
let hasOptionalChainingInMethod = false;
|
|
179
189
|
// Build the path to the array/string being accessed
|
|
180
|
-
while (
|
|
181
|
-
const currentMember =
|
|
190
|
+
while (currentBase.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
191
|
+
const currentMember = currentBase;
|
|
182
192
|
if (currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
183
193
|
pathParts.unshift(currentMember.property.name);
|
|
184
194
|
}
|
|
185
195
|
if (currentMember.optional) {
|
|
186
|
-
|
|
196
|
+
hasOptionalChainingInMethod = true;
|
|
187
197
|
}
|
|
188
|
-
|
|
198
|
+
currentBase = unwrapExpression(currentMember.object);
|
|
189
199
|
}
|
|
190
|
-
if (
|
|
191
|
-
|
|
200
|
+
if (currentBase.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
201
|
+
currentBase.name === objectName) {
|
|
192
202
|
if (pathParts.length === 0) {
|
|
193
203
|
// Direct method call on the object (e.g., userData.map(...))
|
|
194
204
|
needsEntireObject = true;
|
|
195
205
|
}
|
|
196
206
|
else {
|
|
197
207
|
// Method call on a property (e.g., userData.items.map(...) or userData?.items?.map(...))
|
|
198
|
-
let path = objectName + (
|
|
208
|
+
let path = objectName + (hasOptionalChainingInMethod ? '?' : '');
|
|
199
209
|
path += '.' + pathParts.join('.');
|
|
200
210
|
usages.set(path, memberExpr.range?.[0] || 0);
|
|
201
211
|
}
|
|
@@ -207,11 +217,11 @@ context) {
|
|
|
207
217
|
if (memberExpr.optional) {
|
|
208
218
|
hasOptionalChaining = true;
|
|
209
219
|
}
|
|
210
|
-
current = memberExpr.object;
|
|
220
|
+
current = unwrapExpression(memberExpr.object);
|
|
211
221
|
}
|
|
212
222
|
// Check if we reached the target identifier
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
const base = unwrapExpression(current);
|
|
224
|
+
if (base.type === utils_1.AST_NODE_TYPES.Identifier && base.name === objectName) {
|
|
215
225
|
// Build the path with optional chaining
|
|
216
226
|
let path = objectName + (hasOptionalChaining ? '?' : '');
|
|
217
227
|
// Add each part with proper formatting (dot notation or bracket notation)
|
|
@@ -231,47 +241,57 @@ context) {
|
|
|
231
241
|
if (!node || visited.has(node))
|
|
232
242
|
return;
|
|
233
243
|
visited.add(node);
|
|
234
|
-
// Direct usage of the target identifier to distinguish between:
|
|
235
|
-
// 1. Never used (not even present) → suggest removal
|
|
236
|
-
// 2. Used in ways requiring entire object → no suggestion
|
|
237
|
-
// 3. Used only via specific fields → suggest replacing with fields
|
|
238
244
|
if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === objectName) {
|
|
239
|
-
|
|
245
|
+
// Skip TS type assertions, ChainExpression and TSNonNullExpression wrappers
|
|
246
|
+
// (used around the Identifier) so we can attribute the Identifier's usage
|
|
247
|
+
// to its actual parent context (e.g., call/member/assignment) and avoid
|
|
248
|
+
// misclassifying the dependency when determining if the whole object is referenced.
|
|
249
|
+
let wrapperNode = node;
|
|
250
|
+
let effectiveParent = node.parent;
|
|
251
|
+
while (effectiveParent &&
|
|
252
|
+
(effectiveParent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
253
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
254
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
255
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
|
|
256
|
+
wrapperNode = effectiveParent;
|
|
257
|
+
effectiveParent = effectiveParent.parent;
|
|
258
|
+
}
|
|
240
259
|
// Exclude: property name in `other.objectName` (not our target object)
|
|
241
|
-
const isMemberProperty =
|
|
242
|
-
|
|
243
|
-
!
|
|
260
|
+
const isMemberProperty = effectiveParent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
261
|
+
effectiveParent.property === wrapperNode &&
|
|
262
|
+
!effectiveParent.computed;
|
|
244
263
|
// Exclude: object in `objectName.prop` (handled by MemberExpression visitor for field tracking)
|
|
245
|
-
const isMemberObject =
|
|
246
|
-
|
|
264
|
+
const isMemberObject = effectiveParent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
265
|
+
effectiveParent.object === wrapperNode;
|
|
247
266
|
// Exclude: key in `{ objectName: value }` (not usage, just a label)
|
|
248
267
|
// Include: shorthand `{ objectName }` (actual usage)
|
|
249
|
-
const isPropertyKey =
|
|
250
|
-
|
|
251
|
-
!
|
|
252
|
-
!
|
|
268
|
+
const isPropertyKey = effectiveParent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
269
|
+
effectiveParent.key === wrapperNode &&
|
|
270
|
+
!effectiveParent.computed &&
|
|
271
|
+
!effectiveParent.shorthand;
|
|
253
272
|
if (!isMemberProperty && !isMemberObject && !isPropertyKey) {
|
|
254
273
|
isUsed = true;
|
|
255
274
|
// Patterns that require the entire object (cannot refactor to specific fields)
|
|
256
|
-
const isTypeAUsage =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
(
|
|
263
|
-
(
|
|
264
|
-
|
|
265
|
-
(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
const isTypeAUsage = effectiveParent?.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
276
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
277
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
278
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.LogicalExpression ||
|
|
279
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression ||
|
|
280
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.UnaryExpression ||
|
|
281
|
+
(effectiveParent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
282
|
+
(effectiveParent.value === wrapperNode ||
|
|
283
|
+
effectiveParent.shorthand ||
|
|
284
|
+
(effectiveParent.key === wrapperNode &&
|
|
285
|
+
effectiveParent.computed))) ||
|
|
286
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.TemplateLiteral ||
|
|
287
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
288
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
289
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer ||
|
|
290
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute ||
|
|
291
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
292
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
293
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
|
|
294
|
+
effectiveParent?.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
275
295
|
if (isTypeAUsage) {
|
|
276
296
|
needsEntireObject = true;
|
|
277
297
|
}
|
|
@@ -279,13 +299,16 @@ context) {
|
|
|
279
299
|
}
|
|
280
300
|
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
281
301
|
// Check if the object is being called as a function
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
const callee = unwrapExpression(node.callee);
|
|
303
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
304
|
+
callee.name === objectName) {
|
|
284
305
|
needsEntireObject = true;
|
|
285
306
|
}
|
|
286
307
|
// Check if the object is directly passed as an argument
|
|
287
308
|
node.arguments.forEach((arg) => {
|
|
288
|
-
|
|
309
|
+
const unwrappedArg = unwrapExpression(arg);
|
|
310
|
+
if (unwrappedArg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
311
|
+
unwrappedArg.name === objectName) {
|
|
289
312
|
needsEntireObject = true;
|
|
290
313
|
}
|
|
291
314
|
});
|
|
@@ -295,18 +318,21 @@ context) {
|
|
|
295
318
|
// If we find a JSX element, check its attributes for spread operator
|
|
296
319
|
if (node.type === utils_1.AST_NODE_TYPES.JSXElement) {
|
|
297
320
|
node.openingElement.attributes.forEach((attr) => {
|
|
298
|
-
if (attr.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
321
|
+
if (attr.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
322
|
+
const argument = unwrapExpression(attr.argument);
|
|
323
|
+
if (argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
324
|
+
argument.name === objectName) {
|
|
325
|
+
needsEntireObject = true;
|
|
326
|
+
}
|
|
302
327
|
}
|
|
303
328
|
});
|
|
304
329
|
}
|
|
305
330
|
}
|
|
306
331
|
else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
307
332
|
// If we find a spread operator with our target object, consider it as accessing all properties
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
const argument = unwrapExpression(node.argument);
|
|
334
|
+
if (argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
335
|
+
argument.name === objectName) {
|
|
310
336
|
needsEntireObject = true;
|
|
311
337
|
return;
|
|
312
338
|
}
|
|
@@ -314,43 +340,51 @@ context) {
|
|
|
314
340
|
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
315
341
|
// Check if this is accessing a property of our target object
|
|
316
342
|
const memberExpr = node;
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
if
|
|
321
|
-
|
|
322
|
-
|
|
343
|
+
// Skip TS type assertions, ChainExpression and TSNonNullExpression wrappers
|
|
344
|
+
// so we can attribute the MemberExpression's usage to its actual parent
|
|
345
|
+
// context and avoid misclassifying the dependency.
|
|
346
|
+
// We only process if this is the outermost member expression in a chain.
|
|
347
|
+
let effectiveParent = memberExpr.parent;
|
|
348
|
+
while (effectiveParent &&
|
|
349
|
+
(effectiveParent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
350
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
351
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
352
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
|
|
353
|
+
effectiveParent = effectiveParent.parent;
|
|
323
354
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
355
|
+
const isIntermediate = effectiveParent && effectiveParent.type === utils_1.AST_NODE_TYPES.MemberExpression;
|
|
356
|
+
if (!isIntermediate) {
|
|
357
|
+
// Check if this member expression involves our target object
|
|
358
|
+
let current = memberExpr;
|
|
359
|
+
let foundTargetObject = false;
|
|
360
|
+
let hasDynamicComputed = false;
|
|
361
|
+
// Walk up the member expression chain to see if it involves our target object
|
|
362
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
363
|
+
const currentMember = current;
|
|
364
|
+
// Check if this level uses dynamic computed property access
|
|
365
|
+
if (currentMember.computed &&
|
|
366
|
+
currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
367
|
+
hasDynamicComputed = true;
|
|
368
|
+
}
|
|
369
|
+
current = unwrapExpression(currentMember.object);
|
|
335
370
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
foundTargetObject = true;
|
|
342
|
-
}
|
|
343
|
-
if (foundTargetObject) {
|
|
344
|
-
if (hasDynamicComputed) {
|
|
345
|
-
// Dynamic computed property access means we need the entire object
|
|
346
|
-
needsEntireObject = true;
|
|
347
|
-
return;
|
|
371
|
+
// Check if we reached our target object
|
|
372
|
+
const base = unwrapExpression(current);
|
|
373
|
+
if (base.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
374
|
+
base.name === objectName) {
|
|
375
|
+
foundTargetObject = true;
|
|
348
376
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
377
|
+
if (foundTargetObject) {
|
|
378
|
+
if (hasDynamicComputed) {
|
|
379
|
+
// Dynamic computed property access means we need the entire object
|
|
380
|
+
needsEntireObject = true;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Static property access - add to usages
|
|
384
|
+
const path = buildAccessPath(memberExpr);
|
|
385
|
+
if (path) {
|
|
386
|
+
usages.set(path, memberExpr.range?.[0] || 0);
|
|
387
|
+
}
|
|
354
388
|
}
|
|
355
389
|
}
|
|
356
390
|
}
|
|
@@ -533,16 +567,17 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
533
567
|
}
|
|
534
568
|
// Check each dependency in the array
|
|
535
569
|
depsArg.elements.forEach((element) => {
|
|
536
|
-
|
|
570
|
+
const unwrappedElement = element ? unwrapExpression(element) : null;
|
|
571
|
+
if (!unwrappedElement)
|
|
537
572
|
return; // Skip null elements (holes in the array)
|
|
538
|
-
if (
|
|
539
|
-
const objectName =
|
|
573
|
+
if (unwrappedElement.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
574
|
+
const objectName = unwrappedElement.name;
|
|
540
575
|
// Skip type checking if we don't have TypeScript services
|
|
541
576
|
if (hasFullTypeChecking && parserServices) {
|
|
542
577
|
const checker = parserServices.program.getTypeChecker();
|
|
543
578
|
const nodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
544
579
|
// Skip if the dependency is an array or primitive type
|
|
545
|
-
if (isArrayOrPrimitive(checker,
|
|
580
|
+
if (isArrayOrPrimitive(checker, unwrappedElement, nodeMap)) {
|
|
546
581
|
return;
|
|
547
582
|
}
|
|
548
583
|
}
|
|
@@ -225,6 +225,12 @@ function isTypeGuardFunction(node) {
|
|
|
225
225
|
// Check for type predicates (is keyword)
|
|
226
226
|
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypePredicate)
|
|
227
227
|
return true;
|
|
228
|
+
// `never` is never inferred: TypeScript infers `void` for a function whose
|
|
229
|
+
// every path throws, so an explicit `: never` always carries more information
|
|
230
|
+
// than inference (callers rely on it for control-flow narrowing and
|
|
231
|
+
// exhaustiveness). Removing it would silently widen the type to `void`.
|
|
232
|
+
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSNeverKeyword)
|
|
233
|
+
return true;
|
|
228
234
|
// Check for assertion functions (asserts keyword)
|
|
229
235
|
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
230
236
|
const typeName = typeAnnotation.typeName;
|
|
@@ -17,8 +17,10 @@ const COMMON_TYPES = [
|
|
|
17
17
|
'Symbol',
|
|
18
18
|
'BigInt',
|
|
19
19
|
];
|
|
20
|
-
//
|
|
21
|
-
|
|
20
|
+
// Abbreviation type markers (e.g. str, arr, obj). No English word is spelled this
|
|
21
|
+
// way, so their presence as a segment — even in the middle of a name — is
|
|
22
|
+
// unambiguously a type tag (strName, USER_STR_NAME, ConfigArrSettings).
|
|
23
|
+
const ABBREVIATION_MARKERS = [
|
|
22
24
|
'str',
|
|
23
25
|
'num',
|
|
24
26
|
'int',
|
|
@@ -27,6 +29,10 @@ const TYPE_MARKERS = [
|
|
|
27
29
|
'obj',
|
|
28
30
|
'fn',
|
|
29
31
|
'func',
|
|
32
|
+
];
|
|
33
|
+
// Combined type markers (former Hungarian prefixes and type suffixes)
|
|
34
|
+
const TYPE_MARKERS = [
|
|
35
|
+
...ABBREVIATION_MARKERS,
|
|
30
36
|
'array',
|
|
31
37
|
...COMMON_TYPES,
|
|
32
38
|
'Class',
|
|
@@ -34,6 +40,20 @@ const TYPE_MARKERS = [
|
|
|
34
40
|
//'Type', people like to use 'type' as a general purpose noun
|
|
35
41
|
'Enum',
|
|
36
42
|
];
|
|
43
|
+
const ABBREVIATION_MARKER_SET = new Set(ABBREVIATION_MARKERS);
|
|
44
|
+
// Single-letter Hungarian type prefixes (b=boolean, i=integer/index).
|
|
45
|
+
// Only matched as a strict camelCase prefix (e.g. bIsActive, iCount); never as
|
|
46
|
+
// a suffix/middle/SCREAMING_SNAKE segment, where a lone letter is almost always
|
|
47
|
+
// a real word fragment (tab, lib, ui) rather than a type tag.
|
|
48
|
+
const SINGLE_LETTER_PREFIXES = new Set(['b', 'i']);
|
|
49
|
+
// Full type-concept words (spelled out). When one of these appears as a clean
|
|
50
|
+
// PascalCase segment inside a multi-word TYPE name (alias/interface/class), it
|
|
51
|
+
// denotes a type concept/relation (e.g. StringToNumber, CapitalizedString,
|
|
52
|
+
// PromiseOrValue) rather than a redundant Hungarian type tag — comparable to the
|
|
53
|
+
// allowed compound noun PhoneNumber. Abbreviation markers (str/arr/obj/...) are
|
|
54
|
+
// deliberately excluded: no English word is spelled that way, so their presence
|
|
55
|
+
// as a segment is unambiguously a type tag even inside a type name.
|
|
56
|
+
const FULL_TYPE_WORDS = new Set([...COMMON_TYPES, 'Func'].map((word) => word.toLowerCase()));
|
|
37
57
|
// Allowed descriptive suffixes that should not be flagged as Hungarian notation
|
|
38
58
|
const ALLOWED_SUFFIXES = [
|
|
39
59
|
'Formatted',
|
|
@@ -190,6 +210,30 @@ const BUILT_IN_METHODS = new Set([
|
|
|
190
210
|
'catch',
|
|
191
211
|
'finally',
|
|
192
212
|
]);
|
|
213
|
+
// Split a PascalCase/camelCase identifier into its word segments
|
|
214
|
+
// (e.g. "StringToNumber" -> ["String","To","Number"], "FuncKeys" -> ["Func","Keys"]).
|
|
215
|
+
function splitCamelSegments(name) {
|
|
216
|
+
return name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? [];
|
|
217
|
+
}
|
|
218
|
+
// A TYPE name (alias/interface/class) is exempt from a full-type-word marker when
|
|
219
|
+
// that marker is one clean PascalCase segment among OTHER descriptive segments —
|
|
220
|
+
// i.e. the word denotes a type concept/relation, not a redundant type tag.
|
|
221
|
+
// Examples: StringToNumber, CapitalizedString, PromiseOrValue, FuncKeys.
|
|
222
|
+
// Abbreviation markers (str/arr/obj/...) never qualify, so genuine Hungarian type
|
|
223
|
+
// names like UserStrName / ConfigArrSettings / UserObjData still fire.
|
|
224
|
+
function isSemanticTypeConcept(typeName) {
|
|
225
|
+
const segments = splitCamelSegments(typeName);
|
|
226
|
+
if (segments.length < 2) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const fullTypeWordSegments = segments.filter((segment) => FULL_TYPE_WORDS.has(segment.toLowerCase()));
|
|
230
|
+
if (fullTypeWordSegments.length === 0) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
// At least one segment must be a non-type-word descriptor so the name reads as a
|
|
234
|
+
// concept (e.g. Extract+Number) rather than bare type tags glued together.
|
|
235
|
+
return segments.some((segment) => !FULL_TYPE_WORDS.has(segment.toLowerCase()));
|
|
236
|
+
}
|
|
193
237
|
exports.noHungarian = (0, createRule_1.createRule)({
|
|
194
238
|
name: 'no-hungarian',
|
|
195
239
|
meta: {
|
|
@@ -208,8 +252,28 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
208
252
|
create(context) {
|
|
209
253
|
// Track identifiers that have already been checked to prevent double reporting
|
|
210
254
|
const checkedIdentifiers = new Set();
|
|
211
|
-
//
|
|
212
|
-
|
|
255
|
+
// Names declared as generic type parameters (e.g. TNumber, TKey). The leading
|
|
256
|
+
// `T` is the TypeScript convention for "Type parameter", not a Hungarian tag,
|
|
257
|
+
// so neither the declaration nor any reference to it is ever flagged.
|
|
258
|
+
const typeParameterNames = new Set();
|
|
259
|
+
// Check if a variable name contains a type marker with proper word boundaries.
|
|
260
|
+
// `isTypeName` is true for PascalCase type declarations (type aliases,
|
|
261
|
+
// interfaces, classes), enabling the semantic-type-concept exemption.
|
|
262
|
+
function hasTypeMarker(variableName, isTypeName = false) {
|
|
263
|
+
// Type names whose type-word denotes a concept/relation (StringToNumber,
|
|
264
|
+
// CapitalizedString, FuncKeys, PromiseOrValue) are not Hungarian — the word
|
|
265
|
+
// is part of the type's meaning, like the allowed compound noun PhoneNumber.
|
|
266
|
+
if (isTypeName && isSemanticTypeConcept(variableName)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
// Single-letter Hungarian prefixes (bIsActive, iCount): a lone b/i directly
|
|
270
|
+
// followed by an uppercase letter is a type tag. Restricted to camelCase
|
|
271
|
+
// (lowercase first letter) to avoid PascalCase type names like IButton.
|
|
272
|
+
if (variableName.length > 1 &&
|
|
273
|
+
SINGLE_LETTER_PREFIXES.has(variableName[0]) &&
|
|
274
|
+
/[A-Z]/.test(variableName[1])) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
213
277
|
// Check if the variable name is exactly one of the allowed compound nouns
|
|
214
278
|
// or if it contains one of the allowed compound nouns but is not a prefix like "strPhoneNumber"
|
|
215
279
|
for (const compoundNoun of ALLOWED_COMPOUND_NOUNS) {
|
|
@@ -244,22 +308,28 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
244
308
|
if (!variableName.includes('_')) {
|
|
245
309
|
return false;
|
|
246
310
|
}
|
|
311
|
+
const parts = variableName.split('_');
|
|
312
|
+
const lastIndex = parts.length - 1;
|
|
247
313
|
return TYPE_MARKERS.some((marker) => {
|
|
248
314
|
const markerUpper = marker.toUpperCase();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
315
|
+
const normalizedMarker = marker.toLowerCase();
|
|
316
|
+
const isAbbreviation = ABBREVIATION_MARKER_SET.has(normalizedMarker);
|
|
317
|
+
return parts.some((part, index) => {
|
|
318
|
+
if (part !== markerUpper) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
// Abbreviation markers (STR/ARR/OBJ/...) are type tags in any
|
|
322
|
+
// position — no English word is spelled that way.
|
|
323
|
+
if (isAbbreviation) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
// A FULL type word tags the entity's type only at the start (prefix),
|
|
327
|
+
// the end (suffix), or directly before the final noun
|
|
328
|
+
// (..._STRING_NAME). Buried deeper (EDITABLE_WRAPPER_NUMBER_PROPS_…)
|
|
329
|
+
// it qualifies an intermediate segment — a descriptive variant, not a
|
|
330
|
+
// type tag.
|
|
331
|
+
return (index === 0 || index === lastIndex || index === lastIndex - 1);
|
|
332
|
+
});
|
|
263
333
|
});
|
|
264
334
|
}
|
|
265
335
|
// For camelCase, PascalCase, etc.
|
|
@@ -328,9 +398,27 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
328
398
|
}
|
|
329
399
|
return false;
|
|
330
400
|
}
|
|
331
|
-
//
|
|
332
|
-
function
|
|
401
|
+
// Determine whether an identifier is (or references) a generic type parameter.
|
|
402
|
+
function isTypeParameter(node) {
|
|
403
|
+
// The type-parameter declaration itself: <TNumber>
|
|
404
|
+
if (node.parent &&
|
|
405
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeParameter &&
|
|
406
|
+
node.parent.name === node) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
// A reference to a declared type parameter (e.g. x: TNumber).
|
|
410
|
+
return typeParameterNames.has(node.name);
|
|
411
|
+
}
|
|
412
|
+
// Check identifier for type markers (Hungarian notation).
|
|
413
|
+
// `isTypeName` enables the semantic-type-concept exemption for type
|
|
414
|
+
// declarations (aliases, interfaces, classes).
|
|
415
|
+
function checkIdentifier(node, isTypeName = false) {
|
|
333
416
|
const name = node.name;
|
|
417
|
+
// Generic type parameters (TNumber, TKey, ...) are a TypeScript naming
|
|
418
|
+
// convention, never Hungarian — skip the declaration and all references.
|
|
419
|
+
if (isTypeParameter(node)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
334
422
|
// Create a unique ID for this node to avoid checking it twice
|
|
335
423
|
// Use the name along with source location for uniqueness
|
|
336
424
|
const nodeId = `${name}:${node.loc.start.line}:${node.loc.start.column}`;
|
|
@@ -344,7 +432,7 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
344
432
|
if (isExternalOrBuiltIn(node))
|
|
345
433
|
return;
|
|
346
434
|
// Check for type markers
|
|
347
|
-
if (hasTypeMarker(name)) {
|
|
435
|
+
if (hasTypeMarker(name, isTypeName)) {
|
|
348
436
|
context.report({
|
|
349
437
|
node,
|
|
350
438
|
messageId: 'noHungarian',
|
|
@@ -388,10 +476,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
388
476
|
}
|
|
389
477
|
}
|
|
390
478
|
},
|
|
479
|
+
// Record generic type-parameter names so neither the declaration nor any
|
|
480
|
+
// reference to them is flagged (the leading `T` is a TS convention).
|
|
481
|
+
TSTypeParameter(node) {
|
|
482
|
+
if (node.name.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
483
|
+
typeParameterNames.add(node.name.name);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
391
486
|
// Check class declarations
|
|
392
487
|
ClassDeclaration(node) {
|
|
393
488
|
if (node.id) {
|
|
394
|
-
checkIdentifier(node.id);
|
|
489
|
+
checkIdentifier(node.id, true);
|
|
395
490
|
}
|
|
396
491
|
// Check class methods and properties
|
|
397
492
|
for (const member of node.body.body) {
|
|
@@ -421,11 +516,11 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
421
516
|
},
|
|
422
517
|
// Check type aliases
|
|
423
518
|
TSTypeAliasDeclaration(node) {
|
|
424
|
-
checkIdentifier(node.id);
|
|
519
|
+
checkIdentifier(node.id, true);
|
|
425
520
|
},
|
|
426
521
|
// Check interface declarations
|
|
427
522
|
TSInterfaceDeclaration(node) {
|
|
428
|
-
checkIdentifier(node.id);
|
|
523
|
+
checkIdentifier(node.id, true);
|
|
429
524
|
},
|
|
430
525
|
};
|
|
431
526
|
},
|