@blumintinc/eslint-plugin-blumint 1.20.148 → 1.20.149

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.148',
226
+ version: '1.20.149',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const ASTHelpers_1 = require("../utils/ASTHelpers");
7
7
  const disableDirectives_1 = require("../utils/disableDirectives");
8
+ const hungarianNaming_1 = require("../utils/hungarianNaming");
8
9
  const importInsertion_1 = require("../utils/importInsertion");
9
10
  // React hooks to check
10
11
  const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
@@ -62,10 +63,45 @@ function getLastPropertyName(expr) {
62
63
  }
63
64
  return null;
64
65
  }
66
+ /**
67
+ * The name used when the base contributes nothing spellable. Free of any type
68
+ * marker by construction, and the concept the memo actually stands for — the
69
+ * hash of the dependency's CONTENT, which is what depending on `.length`
70
+ * failed to track.
71
+ */
72
+ const BASE_FREE_HASH_NAME = 'contentHash';
73
+ function capitalize(name) {
74
+ return name.charAt(0).toUpperCase() + name.slice(1);
75
+ }
76
+ /**
77
+ * Names for the memo binding, in descending order of how much of the base they
78
+ * keep. `no-hungarian` ships as an error and is NOT fixable, so a name it
79
+ * rejects turns this fixer's work into a manual rename in a file that was clean
80
+ * beforehand (#1997) — every candidate is therefore checked against the same
81
+ * predicate that rule applies, and the first acceptable one wins.
82
+ *
83
+ * Which candidate that is follows from what `no-hungarian` rejects:
84
+ *
85
+ * - `<base>Hash` reads as the domain concept for almost every base and is the
86
+ * preferred spelling.
87
+ * - A single-letter `b`/`i` base makes `bHash`/`iHash` look like a Hungarian
88
+ * type prefix (b=boolean, i=integer) glued to a capital. `hashOf<Base>` says
89
+ * the same thing with the base out of the leading position.
90
+ * - A base that IS a type word or its abbreviation (`obj`, `arr`, `str`,
91
+ * `array`, `number`, ...) taints every name that carries it as a segment, so
92
+ * no base-preserving candidate can succeed. Dropping the type-coded base is
93
+ * exactly the rename `no-hungarian` asks for.
94
+ */
95
+ function baseDerivedNames(base) {
96
+ return [`${base}Hash`, `hashOf${capitalize(base)}`];
97
+ }
65
98
  function generateUniqueName(base, taken) {
66
- const candidate = `${base}Hash`;
99
+ const candidate = baseDerivedNames(base).find((name) => !(0, hungarianNaming_1.encodesTypeMarker)(name)) ??
100
+ BASE_FREE_HASH_NAME;
67
101
  if (!taken.has(candidate))
68
102
  return candidate;
103
+ // A numeric suffix disambiguates without reopening the naming question: it
104
+ // adds no word boundary, so it cannot turn an accepted name into a marker.
69
105
  let i = 2;
70
106
  while (taken.has(`${candidate}${i}`)) {
71
107
  i++;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mirrors the type-marker predicate the shipped `no-hungarian` rule applies to
3
+ * a declaration name, so a fixer can refuse to SYNTHESISE a name that rule
4
+ * rejects. `no-hungarian` is not fixable, so a generated identifier it flags
5
+ * converts machine-resolvable work into a manual rename inside a file that was
6
+ * clean before `--fix` ran (#1997).
7
+ *
8
+ * Scope is the shape a fixer emits: an alphanumeric camelCase/PascalCase
9
+ * identifier. `no-hungarian`'s SCREAMING_SNAKE_CASE branch and its
10
+ * compound-noun / descriptive-suffix / <entity>Number / <domain>Symbol
11
+ * carve-outs are deliberately absent. Every one of them can only turn a
12
+ * rejection into an acceptance, and each is keyed on a trailing word a
13
+ * generated name does not carry, so their absence is unreachable for such a
14
+ * name and, if ever reached, costs the caller its next candidate rather than
15
+ * reintroducing the defect. `no-array-length-in-deps.test.ts` pins agreement
16
+ * with the real rule over the corpus of names its fixer can emit, so a change
17
+ * to either predicate surfaces there.
18
+ */
19
+ /**
20
+ * Whether `no-hungarian` reads `name` as encoding its own type.
21
+ */
22
+ export declare function encodesTypeMarker(name: string): boolean;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * Mirrors the type-marker predicate the shipped `no-hungarian` rule applies to
4
+ * a declaration name, so a fixer can refuse to SYNTHESISE a name that rule
5
+ * rejects. `no-hungarian` is not fixable, so a generated identifier it flags
6
+ * converts machine-resolvable work into a manual rename inside a file that was
7
+ * clean before `--fix` ran (#1997).
8
+ *
9
+ * Scope is the shape a fixer emits: an alphanumeric camelCase/PascalCase
10
+ * identifier. `no-hungarian`'s SCREAMING_SNAKE_CASE branch and its
11
+ * compound-noun / descriptive-suffix / <entity>Number / <domain>Symbol
12
+ * carve-outs are deliberately absent. Every one of them can only turn a
13
+ * rejection into an acceptance, and each is keyed on a trailing word a
14
+ * generated name does not carry, so their absence is unreachable for such a
15
+ * name and, if ever reached, costs the caller its next candidate rather than
16
+ * reintroducing the defect. `no-array-length-in-deps.test.ts` pins agreement
17
+ * with the real rule over the corpus of names its fixer can emit, so a change
18
+ * to either predicate surfaces there.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.encodesTypeMarker = void 0;
22
+ // Abbreviation type markers. No English word is spelled this way, so their
23
+ // presence as a whole camelCase segment is unambiguously a type tag.
24
+ const ABBREVIATION_MARKERS = ['str', 'num', 'int', 'bool', 'arr', 'obj'];
25
+ const COMMON_TYPES = [
26
+ 'String',
27
+ 'Number',
28
+ 'Boolean',
29
+ 'Array',
30
+ 'Object',
31
+ 'RegExp',
32
+ 'Promise',
33
+ 'Symbol',
34
+ 'BigInt',
35
+ ];
36
+ const TYPE_MARKERS = [
37
+ ...ABBREVIATION_MARKERS,
38
+ 'array',
39
+ ...COMMON_TYPES,
40
+ 'Class',
41
+ 'Interface',
42
+ 'Enum',
43
+ ];
44
+ const ABBREVIATION_MARKER_SET = new Set(ABBREVIATION_MARKERS);
45
+ const FULL_TYPE_WORDS = new Set(COMMON_TYPES.map((word) => word.toLowerCase()));
46
+ // Single-letter Hungarian type prefixes (b=boolean, i=integer/index), matched
47
+ // only as a strict camelCase prefix. This is the arm that rejects `bHash`.
48
+ const SINGLE_LETTER_PREFIXES = new Set(['b', 'i']);
49
+ const CAMEL_SEGMENT_REGEX = /[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g;
50
+ function splitCamelSegmentsRaw(name) {
51
+ return name.match(CAMEL_SEGMENT_REGEX) ?? [];
52
+ }
53
+ // A built-in type word carrying an internal capital (BigInt) splits into parts
54
+ // whose tail collides with an abbreviation marker (Int/int), so the parts are
55
+ // re-merged into one atomic segment before segment matching.
56
+ const MULTI_CAPITAL_TYPE_WORD_PARTS = COMMON_TYPES.map(splitCamelSegmentsRaw).filter((parts) => parts.length > 1);
57
+ function mergeMultiCapitalTypeWords(segments) {
58
+ const merged = [];
59
+ let index = 0;
60
+ while (index < segments.length) {
61
+ const match = MULTI_CAPITAL_TYPE_WORD_PARTS.find((parts) => parts.every((part, offset) => segments[index + offset]?.toLowerCase() === part.toLowerCase()));
62
+ if (match) {
63
+ merged.push(segments.slice(index, index + match.length).join(''));
64
+ index += match.length;
65
+ }
66
+ else {
67
+ merged.push(segments[index]);
68
+ index += 1;
69
+ }
70
+ }
71
+ return merged;
72
+ }
73
+ function splitCamelSegments(name) {
74
+ return mergeMultiCapitalTypeWords(splitCamelSegmentsRaw(name));
75
+ }
76
+ function isPascalCaseName(name) {
77
+ return /^[A-Z]/.test(name) && name !== name.toUpperCase();
78
+ }
79
+ // A PascalCase name whose type word qualifies a DIFFERENT head noun names a
80
+ // concept or relation (StringToNumber, NumberAmountEditor), not the value's own
81
+ // runtime type.
82
+ function isSemanticTypeConcept(name) {
83
+ const segments = splitCamelSegments(name);
84
+ if (segments.length < 2) {
85
+ return false;
86
+ }
87
+ const hasTypeWord = segments.some((segment) => FULL_TYPE_WORDS.has(segment.toLowerCase()));
88
+ if (!hasTypeWord) {
89
+ return false;
90
+ }
91
+ return segments.some((segment) => !FULL_TYPE_WORDS.has(segment.toLowerCase()));
92
+ }
93
+ /**
94
+ * Whether `no-hungarian` reads `name` as encoding its own type.
95
+ */
96
+ function encodesTypeMarker(name) {
97
+ if (isPascalCaseName(name) && isSemanticTypeConcept(name)) {
98
+ return false;
99
+ }
100
+ if (name.length > 1 &&
101
+ SINGLE_LETTER_PREFIXES.has(name[0]) &&
102
+ /[A-Z]/.test(name[1])) {
103
+ return true;
104
+ }
105
+ const normalizedName = name.toLowerCase();
106
+ return TYPE_MARKERS.some((marker) => {
107
+ const normalizedMarker = marker.toLowerCase();
108
+ if (normalizedName === normalizedMarker) {
109
+ return false;
110
+ }
111
+ // An abbreviation marker is a type tag only as a whole segment: "int"
112
+ // inside "Mint" or "point" is a word fragment, not a tag.
113
+ if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
114
+ return splitCamelSegments(name).some((segment) => segment.toLowerCase() === normalizedMarker);
115
+ }
116
+ // A full type word tags the value's type only at a word boundary in
117
+ // leading or trailing position; a middle segment qualifies a domain concept.
118
+ if (normalizedName.startsWith(normalizedMarker) &&
119
+ normalizedName.length > normalizedMarker.length &&
120
+ /[A-Z0-9]/.test(name[normalizedMarker.length])) {
121
+ return true;
122
+ }
123
+ return (normalizedName.endsWith(normalizedMarker) &&
124
+ normalizedName.length > normalizedMarker.length &&
125
+ (/[A-Z0-9]/.test(name[name.length - normalizedMarker.length - 1]) ||
126
+ /[A-Z]/.test(name[name.length - normalizedMarker.length])));
127
+ });
128
+ }
129
+ exports.encodesTypeMarker = encodesTypeMarker;
130
+ //# sourceMappingURL=hungarianNaming.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.148",
3
+ "version": "1.20.149",
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.149",
4
+ "date": "2026-08-13T16:22:27.196Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-array-length-in-deps",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1997
11
+ ],
12
+ "summary": "emit a hash name no-hungarian accepts (closes #1997)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.148",
4
18
  "date": "2026-08-13T13:06:17.038Z",