@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.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 (37) hide show
  1. package/README.md +28 -5
  2. package/lib/index.js +7 -1
  3. package/lib/rules/consistent-callback-naming.js +26 -0
  4. package/lib/rules/dynamic-https-errors.js +5 -26
  5. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  6. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
  7. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  8. package/lib/rules/enforce-dynamic-imports.js +42 -21
  9. package/lib/rules/enforce-memoize-async.js +1 -4
  10. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  11. package/lib/rules/enforce-verb-noun-naming.js +3 -0
  12. package/lib/rules/global-const-style.js +9 -0
  13. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  14. package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
  15. package/lib/rules/memo-nested-react-components.js +143 -8
  16. package/lib/rules/no-array-length-in-deps.js +74 -3
  17. package/lib/rules/no-circular-references.js +145 -482
  18. package/lib/rules/no-compositing-layer-props.js +31 -0
  19. package/lib/rules/no-entire-object-hook-deps.js +132 -97
  20. package/lib/rules/no-explicit-return-type.js +6 -0
  21. package/lib/rules/no-hungarian.js +119 -24
  22. package/lib/rules/no-margin-properties.js +7 -38
  23. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  24. package/lib/rules/no-unused-props.js +215 -37
  25. package/lib/rules/no-useless-fragment.js +10 -2
  26. package/lib/rules/parallelize-async-operations.js +1 -3
  27. package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
  28. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  29. package/lib/rules/react-memoize-literals.js +87 -1
  30. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  31. package/lib/rules/require-migration-script-metadata.js +206 -0
  32. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  33. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  34. package/lib/utils/ASTHelpers.d.ts +15 -0
  35. package/lib/utils/ASTHelpers.js +48 -0
  36. package/package.json +7 -6
  37. package/release-manifest.json +166 -0
@@ -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 current = memberExpr.object;
130
- while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
131
- current = current.object;
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 (current.type === utils_1.AST_NODE_TYPES.Identifier &&
134
- current.name === objectName) {
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 current = memberExpr.object;
186
+ let currentBase = unwrapExpression(memberExpr.object);
177
187
  let pathParts = [];
178
- let hasOptionalChaining = false;
188
+ let hasOptionalChainingInMethod = false;
179
189
  // Build the path to the array/string being accessed
180
- while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
181
- const currentMember = current;
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
- hasOptionalChaining = true;
196
+ hasOptionalChainingInMethod = true;
187
197
  }
188
- current = currentMember.object;
198
+ currentBase = unwrapExpression(currentMember.object);
189
199
  }
190
- if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
191
- current.name === objectName) {
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 + (hasOptionalChaining ? '?' : '');
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
- if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
214
- current.name === objectName) {
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
- const parent = node.parent;
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 = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
242
- parent.property === node &&
243
- !parent.computed;
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 = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
246
- parent.object === node;
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 = parent?.type === utils_1.AST_NODE_TYPES.Property &&
250
- parent.key === node &&
251
- !parent.computed &&
252
- !parent.shorthand;
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 = parent?.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
257
- parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
258
- parent?.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
259
- parent?.type === utils_1.AST_NODE_TYPES.LogicalExpression ||
260
- parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression ||
261
- parent?.type === utils_1.AST_NODE_TYPES.UnaryExpression ||
262
- (parent?.type === utils_1.AST_NODE_TYPES.Property &&
263
- (parent.value === node ||
264
- parent.shorthand ||
265
- (parent.key === node && parent.computed))) ||
266
- parent?.type === utils_1.AST_NODE_TYPES.TemplateLiteral ||
267
- parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
268
- parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
269
- parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer ||
270
- parent?.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute ||
271
- parent?.type === utils_1.AST_NODE_TYPES.SpreadElement ||
272
- parent?.type === utils_1.AST_NODE_TYPES.ForInStatement ||
273
- parent?.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
274
- parent?.type === utils_1.AST_NODE_TYPES.CallExpression;
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
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
283
- node.callee.name === objectName) {
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
- if (arg.type === utils_1.AST_NODE_TYPES.Identifier && arg.name === objectName) {
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
- attr.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
300
- attr.argument.name === objectName) {
301
- needsEntireObject = true;
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
- if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
309
- node.argument.name === objectName) {
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
- // Only process if this is the outermost member expression in a chain
318
- // (i.e., its parent is not also a member expression)
319
- const parent = memberExpr.parent;
320
- if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
321
- // This is an intermediate member expression, skip it
322
- return;
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
- // Check if this member expression involves our target object
325
- let current = memberExpr;
326
- let foundTargetObject = false;
327
- let hasDynamicComputed = false;
328
- // Walk up the member expression chain to see if it involves our target object
329
- while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
330
- const currentMember = current;
331
- // Check if this level uses dynamic computed property access
332
- if (currentMember.computed &&
333
- currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
334
- hasDynamicComputed = true;
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
- current = currentMember.object;
337
- }
338
- // Check if we reached our target object
339
- if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
340
- current.name === objectName) {
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
- else {
350
- // Static property access - add to usages
351
- const path = buildAccessPath(memberExpr);
352
- if (path) {
353
- usages.set(path, memberExpr.range?.[0] || 0);
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
- if (!element)
570
+ const unwrappedElement = element ? unwrapExpression(element) : null;
571
+ if (!unwrappedElement)
537
572
  return; // Skip null elements (holes in the array)
538
- if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
539
- const objectName = element.name;
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, element, nodeMap)) {
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
- // Combined type markers (former Hungarian prefixes and type suffixes)
21
- const TYPE_MARKERS = [
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
- // Check if a variable name contains a type marker with proper word boundaries
212
- function hasTypeMarker(variableName) {
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
- // Check if it's a prefix (PREFIX_REST)
250
- if (variableName.startsWith(markerUpper + '_') &&
251
- variableName.length > markerUpper.length + 1) {
252
- return true;
253
- }
254
- // Check if it's a suffix (REST_SUFFIX)
255
- if (variableName.endsWith('_' + markerUpper) &&
256
- variableName.length > markerUpper.length + 1) {
257
- return true;
258
- }
259
- // Check if it's in the middle (PART_MARKER_PART)
260
- const parts = variableName.split('_');
261
- // Only consider exact matches for parts, not substrings
262
- return parts.some((part) => part === markerUpper);
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
- // Check identifier for type markers (Hungarian notation)
332
- function checkIdentifier(node) {
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
  },