@blumintinc/eslint-plugin-blumint 1.20.161 → 1.20.162

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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.161',
226
+ version: '1.20.162',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -155,6 +155,53 @@ const DOMAIN_SYMBOL_HEAD_NOUNS = new Set([
155
155
  'unicode',
156
156
  'ascii',
157
157
  ]);
158
+ // Domain head nouns that legitimately precede a "Class" suffix. In
159
+ // <taxonomy>Class the trailing "Class" is the HEAD NOUN of the domain concept —
160
+ // the BUCKET a value falls into — not the JavaScript `class` construct bolted
161
+ // onto the name. "Window size class" is the Material Design 3 / UIKit term for
162
+ // a breakpoint bucket (compact/medium/expanded), a regex *character class* is
163
+ // `[a-z]`, an S3 *storage class* is "STANDARD" — in each case the value is a
164
+ // string or number map, so there is no type marker to strip: removing the
165
+ // suffix yields a wrong name (WINDOW_SIZE names a width, not a bucket). Same
166
+ // reasoning as the <entity>Number (#1277) and <domain>Symbol (#1835)
167
+ // carve-outs, applied to the taxonomy sense of "class" (#2030).
168
+ //
169
+ // Nouns whose <noun>Class reads as a tag on a JS class value — user, config,
170
+ // helper, base, model, controller, wrapper — are intentionally ABSENT, so
171
+ // UserClass / userClass / HELPER_CLASS stay flagged, as do all PREFIX uses
172
+ // (classRegistry, CLASS_MAP). The carve-out is additionally vetoed whenever the
173
+ // declaration syntactically proves a real class (see isClassValuedDeclaration).
174
+ const DOMAIN_CLASS_HEAD_NOUNS = new Set([
175
+ // Layout / responsive design: M3 and UIKit bucket a window's width into a
176
+ // "size class".
177
+ 'size',
178
+ // Regex and linguistics: character class ([a-z]), word class (noun/verb).
179
+ 'character',
180
+ 'word',
181
+ // Mathematics / CS taxonomy: equivalence class, complexity class (P, NP).
182
+ 'equivalence',
183
+ 'complexity',
184
+ // Systems / infrastructure: C and S3 storage class, QoS traffic/service
185
+ // class, USB device class.
186
+ 'storage',
187
+ 'traffic',
188
+ 'service',
189
+ 'device',
190
+ // Finance: asset class, share class.
191
+ 'asset',
192
+ 'share',
193
+ // Travel: fare/cabin/booking class (airline RBD codes).
194
+ 'fare',
195
+ 'cabin',
196
+ 'booking',
197
+ // Categorization by attribute: weight class (boxing), age class, hazard
198
+ // class (DOT), drug class (pharmacology), vehicle class (DMV).
199
+ 'weight',
200
+ 'age',
201
+ 'hazard',
202
+ 'drug',
203
+ 'vehicle',
204
+ ]);
158
205
  // Common built-in JavaScript prototype methods
159
206
  const BUILT_IN_METHODS = new Set([
160
207
  // String methods
@@ -404,6 +451,24 @@ function isDomainSymbolCompound(name) {
404
451
  const lastSegment = segments[segments.length - 1];
405
452
  return (!!lastSegment && DOMAIN_SYMBOL_HEAD_NOUNS.has(lastSegment.toLowerCase()));
406
453
  }
454
+ // Is `name` a domain compound of the form <taxonomy>Class, where the word
455
+ // directly before the trailing "Class" names a bucketing taxonomy
456
+ // (windowSizeClass, characterClass, storageClass)? Only the LAST head segment
457
+ // is consulted, so prefixed variants generalize (currentWindowSizeClass passes)
458
+ // while names whose value is a real JS class keep firing (userClass -> head
459
+ // segment "user", not a taxonomy).
460
+ function isDomainClassCompound(name) {
461
+ if (!name.endsWith('Class')) {
462
+ return false;
463
+ }
464
+ const head = name.slice(0, -'Class'.length);
465
+ if (head.length === 0) {
466
+ return false;
467
+ }
468
+ const segments = splitCamelSegments(head);
469
+ const lastSegment = segments[segments.length - 1];
470
+ return (!!lastSegment && DOMAIN_CLASS_HEAD_NOUNS.has(lastSegment.toLowerCase()));
471
+ }
407
472
  // Does a type annotation denote the JS `symbol` primitive (`symbol` or the
408
473
  // declaration-site form `unique symbol`)?
409
474
  function isSymbolTypeAnnotation(node) {
@@ -476,6 +541,43 @@ function isSymbolTypedDeclaration(node) {
476
541
  return false;
477
542
  }
478
543
  }
544
+ // Does a type annotation denote a class constructor (`new (...) => T`)? Such an
545
+ // annotation is the one syntactic spelling that proves the annotated value is a
546
+ // class without type information.
547
+ function isConstructorTypeAnnotation(node) {
548
+ return !!node && node.type === utils_1.AST_NODE_TYPES.TSConstructorType;
549
+ }
550
+ // Does the declaration site PROVE, syntactically, that the named value is a JS
551
+ // class? Only a `class` expression initializer, a class declaration's own name,
552
+ // or an explicit constructor-type annotation are conclusive without type
553
+ // information — an aliased constructor (`const sizeClass = User`) is invisible
554
+ // here, which is precisely why the taxonomy carve-out is keyed on the head noun
555
+ // rather than on the type. When this holds, the trailing "Class" genuinely
556
+ // encodes the value's type and the DOMAIN_CLASS_HEAD_NOUNS carve-out is vetoed,
557
+ // so `const SizeClass = class {}` still reports. Mirrors
558
+ // isSymbolTypedDeclaration (#1835).
559
+ function isClassValuedDeclaration(node) {
560
+ if (isConstructorTypeAnnotation(node.typeAnnotation?.typeAnnotation)) {
561
+ return true;
562
+ }
563
+ const parent = node.parent;
564
+ if (!parent) {
565
+ return false;
566
+ }
567
+ switch (parent.type) {
568
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
569
+ case utils_1.AST_NODE_TYPES.ClassExpression:
570
+ return parent.id === node;
571
+ case utils_1.AST_NODE_TYPES.VariableDeclarator:
572
+ return (parent.id === node &&
573
+ parent.init?.type === utils_1.AST_NODE_TYPES.ClassExpression);
574
+ case utils_1.AST_NODE_TYPES.PropertyDefinition:
575
+ return (parent.key === node &&
576
+ parent.value?.type === utils_1.AST_NODE_TYPES.ClassExpression);
577
+ default:
578
+ return false;
579
+ }
580
+ }
479
581
  // Rebuild a SCREAMING_SNAKE_CASE identifier's segments into a PascalCase compound
480
582
  // (["MATCH","NUMBER"] -> "MatchNumber") so the snake-case branch can reuse the
481
583
  // camelCase isDomainNumberCompound / DOMAIN_NUMBER_HEAD_NOUNS exemption verbatim,
@@ -512,7 +614,9 @@ exports.noHungarian = (0, createRule_1.createRule)({
512
614
  // interfaces, classes), enabling the semantic-type-concept exemption.
513
615
  // `isSymbolTyped` is true when the declaration syntactically proves a JS
514
616
  // `symbol` value, which vetoes the <domain>Symbol glyph exemption.
515
- function hasTypeMarker(variableName, isTypeName = false, isSymbolTyped = false) {
617
+ // `isClassValued` is true when the declaration syntactically proves a JS
618
+ // class value, which vetoes the <taxonomy>Class exemption.
619
+ function hasTypeMarker(variableName, isTypeName = false, isSymbolTyped = false, isClassValued = false) {
516
620
  // Type names whose type-word denotes a concept/relation (StringToNumber,
517
621
  // CapitalizedString, FuncKeys, PromiseOrValue) are not Hungarian — the word
518
622
  // is part of the type's meaning, like the allowed compound noun PhoneNumber.
@@ -621,6 +725,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
621
725
  isDomainSymbolCompound(screamingSnakePartsToPascalCase(parts))) {
622
726
  return false;
623
727
  }
728
+ // A trailing "..._CLASS" whose preceding head noun names a
729
+ // bucketing taxonomy (WINDOW_SIZE_CLASS, STORAGE_CLASS) is a
730
+ // domain compound, not a type tag — same carve-out as camelCase
731
+ // windowSizeClass (#2030), routed through the shared PascalCase
732
+ // helper so the two casings cannot diverge (the #1294 asymmetry).
733
+ if (normalizedMarker === 'class' &&
734
+ index === lastIndex &&
735
+ !isClassValued &&
736
+ isDomainClassCompound(screamingSnakePartsToPascalCase(parts))) {
737
+ return false;
738
+ }
624
739
  return true;
625
740
  });
626
741
  });
@@ -695,6 +810,20 @@ exports.noHungarian = (0, createRule_1.createRule)({
695
810
  isDomainSymbolCompound(variableName)) {
696
811
  return false;
697
812
  }
813
+ // A trailing "...Class" whose head noun names a bucketing taxonomy
814
+ // (windowSizeClass, characterClass, storageClass) is a domain
815
+ // compound: the suffix names WHAT the value is (the BUCKET a window
816
+ // width falls into — Material Design 3 / UIKit vocabulary), and
817
+ // stripping it yields a wrong name (windowSize is a width, not a
818
+ // bucket) (#2030). Scoped to the full-word `Class` marker in SUFFIX
819
+ // position only, and vetoed when the declaration proves a real
820
+ // class, so userClass / classRegistry / `const SizeClass = class {}`
821
+ // keep firing.
822
+ if (normalizedMarker === 'class' &&
823
+ !isClassValued &&
824
+ isDomainClassCompound(variableName)) {
825
+ return false;
826
+ }
698
827
  return true;
699
828
  }
700
829
  // Full type-word markers (non-abbreviations: String, Number, Function,
@@ -770,7 +899,7 @@ exports.noHungarian = (0, createRule_1.createRule)({
770
899
  if (isExternalOrBuiltIn(node))
771
900
  return;
772
901
  // Check for type markers
773
- if (hasTypeMarker(name, isTypeName, isSymbolTypedDeclaration(node))) {
902
+ if (hasTypeMarker(name, isTypeName, isSymbolTypedDeclaration(node), isClassValuedDeclaration(node))) {
774
903
  context.report({
775
904
  node,
776
905
  messageId: 'noHungarian',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.161",
3
+ "version": "1.20.162",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.162",
4
+ "date": "2026-08-17T15:38:50.817Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-hungarian",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2030
11
+ ],
12
+ "summary": "treat Class as a taxonomy head noun in suffix position (closes #2030)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.161",
4
18
  "date": "2026-08-17T13:22:13.676Z",