@blumintinc/eslint-plugin-blumint 1.20.175 → 1.20.176

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.175',
226
+ version: '1.20.176',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1302,11 +1302,18 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1302
1302
  * position the author wrote it, and hands assertSafe what it produces — the
1303
1303
  * rewrite adds a validation, it does not move a dereference.
1304
1304
  *
1305
- * A key written without a wrapper keeps the narrower argument the fix has
1306
- * always emitted: `String(id)` and `` `${id}` `` collapse to `id`, whose
1307
- * conversion assertSafe subsumes.
1305
+ * A coercion the author wrote is part of the key, so it is wrapped rather
1306
+ * than replaced: `assertSafe(String(id))` and ``assertSafe(`${id}`)``, not
1307
+ * `assertSafe(id)`. assertSafe VALIDATES a key, it does not coerce one —
1308
+ * it throws on any argument whose `typeof` is neither `string` nor
1309
+ * `number` — so dropping the `String()` call or the template hands it the
1310
+ * raw operand and changes what the key evaluates to. A boolean operand
1311
+ * turns a working `` record[`${flag}`] `` lookup into a throw (#2144).
1312
+ * Wrapping the key exactly as written is also what makes the sole
1313
+ * substitution behave like every other template: `` `${a}${b}` `` has
1314
+ * always been wrapped whole.
1308
1315
  */
1309
- const reportWrittenKey = (written, unwrapped, innerText) => reportUseAssertSafe(written, written === unwrapped ? innerText : context.sourceCode.getText(written));
1316
+ const reportWrittenKey = (written) => reportUseAssertSafe(written, context.sourceCode.getText(written));
1310
1317
  /**
1311
1318
  * Returns true when the identifier was initialized directly from an
1312
1319
  * assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)`.
@@ -1830,9 +1837,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1830
1837
  if (key.type === utils_1.AST_NODE_TYPES.CallExpression &&
1831
1838
  key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1832
1839
  key.callee.name === 'String') {
1833
- const arg = key.arguments[0];
1834
- const argText = context.sourceCode.getText(arg);
1835
- reportWrittenKey(written, key, argText);
1840
+ reportWrittenKey(written);
1836
1841
  }
1837
1842
  // Check for template literals like `${id}`
1838
1843
  if (key.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
@@ -1840,9 +1845,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1840
1845
  key.quasis.length === 2 &&
1841
1846
  key.quasis[0].value.raw === '' &&
1842
1847
  key.quasis[1].value.raw === '') {
1843
- const expr = key.expressions[0];
1844
- const exprText = context.sourceCode.getText(expr);
1845
- reportWrittenKey(written, key, exprText);
1848
+ reportWrittenKey(written);
1846
1849
  }
1847
1850
  }
1848
1851
  },
@@ -1855,9 +1858,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1855
1858
  if (left.type === utils_1.AST_NODE_TYPES.CallExpression &&
1856
1859
  left.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1857
1860
  left.callee.name === 'String') {
1858
- const arg = left.arguments[0];
1859
- const argText = context.sourceCode.getText(arg);
1860
- reportWrittenKey(written, left, argText);
1861
+ reportWrittenKey(written);
1861
1862
  }
1862
1863
  // Check for template literals like `${id}`
1863
1864
  if (left.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
@@ -1865,9 +1866,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1865
1866
  left.quasis.length === 2 &&
1866
1867
  left.quasis[0].value.raw === '' &&
1867
1868
  left.quasis[1].value.raw === '') {
1868
- const expr = left.expressions[0];
1869
- const exprText = context.sourceCode.getText(expr);
1870
- reportWrittenKey(written, left, exprText);
1869
+ reportWrittenKey(written);
1871
1870
  }
1872
1871
  }
1873
1872
  },
@@ -1909,9 +1908,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1909
1908
  if (property.type === utils_1.AST_NODE_TYPES.CallExpression &&
1910
1909
  property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1911
1910
  property.callee.name === 'String') {
1912
- const arg = property.arguments[0];
1913
- const argText = context.sourceCode.getText(arg);
1914
- reportWrittenKey(written, property, argText);
1911
+ reportWrittenKey(written);
1915
1912
  return;
1916
1913
  }
1917
1914
  // Check for template literals
@@ -1935,19 +1932,14 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1935
1932
  if (!reachesPrototype) {
1936
1933
  return;
1937
1934
  }
1938
- // `${id}` alone is the whole key, so the remedy names the inner
1939
- // expression and the fix wraps it directly. A template carrying
1940
- // fixed text has no such inner key — the string it builds is the
1941
- // key so that whole template is what gets wrapped, which is the
1942
- // shape the docs show for `assertSafe(`${id}_suffix`)`.
1943
- const isSimpleVarInterpolation = property.expressions.length === 1 &&
1944
- property.quasis.length === 2 &&
1945
- property.quasis[0].value.raw === '' &&
1946
- property.quasis[1].value.raw === '';
1947
- const unwrapped = isSimpleVarInterpolation
1948
- ? property.expressions[0]
1949
- : property;
1950
- reportWrittenKey(written, property, context.sourceCode.getText(unwrapped));
1935
+ // The string the template builds is the key, whether it carries
1936
+ // fixed text or is a lone substitution, so the whole template is
1937
+ // what gets wrapped — the shape the docs show for
1938
+ // `assertSafe(`${id}_suffix`)`. Reaching inside for `${id}` would
1939
+ // hand assertSafe the operand instead of the string the operand
1940
+ // widens to, and assertSafe throws on anything that is not already
1941
+ // a string or a number (#2144).
1942
+ reportWrittenKey(written);
1951
1943
  return;
1952
1944
  }
1953
1945
  // Check for direct variable usage (identifiers)
@@ -1971,8 +1963,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1971
1963
  if (isCompilerBoundedLookup(node, property)) {
1972
1964
  return;
1973
1965
  }
1974
- const propText = context.sourceCode.getText(property);
1975
- reportWrittenKey(written, property, propText);
1966
+ reportWrittenKey(written);
1976
1967
  return;
1977
1968
  }
1978
1969
  // Check for binary expressions (like index + 1)
@@ -1981,8 +1972,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1981
1972
  if (isLikelyArray) {
1982
1973
  return;
1983
1974
  }
1984
- const propText = context.sourceCode.getText(property);
1985
- reportWrittenKey(written, property, propText);
1975
+ reportWrittenKey(written);
1986
1976
  return;
1987
1977
  }
1988
1978
  // Check for boolean expressions and other literals
@@ -1993,8 +1983,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
1993
1983
  if (isLikelyArray) {
1994
1984
  return;
1995
1985
  }
1996
- const propText = context.sourceCode.getText(property);
1997
- reportWrittenKey(written, property, propText);
1986
+ reportWrittenKey(written);
1998
1987
  return;
1999
1988
  }
2000
1989
  // Check for function calls (anything that isn't handled above)
@@ -2006,8 +1995,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
2006
1995
  if (isLikelyArray) {
2007
1996
  return;
2008
1997
  }
2009
- const propText = context.sourceCode.getText(property);
2010
- reportWrittenKey(written, property, propText);
1998
+ reportWrittenKey(written);
2011
1999
  return;
2012
2000
  }
2013
2001
  }
@@ -41,6 +41,141 @@ const NON_PLURALIZABLE_SUFFIXES = [
41
41
  * the reference itself as a container.
42
42
  */
43
43
  const ARRAY_GENERIC_NAMES = new Set(['Array', 'ReadonlyArray']);
44
+ /**
45
+ * Word-final suffixes that are singular by shape. No English plural ends in
46
+ * `-sis` or `-ss`, so both classes are exempt wholesale rather than enumerated:
47
+ * every `-sis` noun (`Analysis`, `Basis`, `Thesis`, `Diagnosis`, `Synopsis`,
48
+ * `Chassis`) and every `-ss` noun (`Address`, `Progress`, `Class`, `Success`)
49
+ * is covered, including ones no list would anticipate.
50
+ *
51
+ * The neighbouring `-is`/`-us`/`-os` classes get no such blanket rule, because
52
+ * genuine plurals do end that way — `Emojis`, `Minis`, `Menus`, `Plateaus`,
53
+ * `Taxis`. Exempting those by shape would silence the rule on real collection
54
+ * names, so they are enumerated in SINGULAR_NOUNS_ENDING_IN_S instead.
55
+ */
56
+ const SINGULAR_WORD_SUFFIXES = ['sis', 'ss'];
57
+ /**
58
+ * Singular nouns that end in `s` and that `pluralize` mistakes for plurals.
59
+ * Stripping the trailing `s` yields a non-word — `Axis` → `Axi`, `Lens` →
60
+ * `Len`, `Chaos` → `Chao` — which is the tell that the identifier was singular
61
+ * all along. Enumeration is the only safe discriminator here: `Axis` and
62
+ * `Minis` share a shape, so nothing but the noun itself separates a Latin/Greek
63
+ * singular from an ordinary plural.
64
+ *
65
+ * Entries `pluralize` already classifies correctly (`Status`, `Corpus`) are
66
+ * listed too, so a change in that library's heuristics cannot quietly
67
+ * reintroduce the false positive.
68
+ */
69
+ const SINGULAR_NOUNS_ENDING_IN_S = new Set([
70
+ // Latin/Greek `-is` singulars outside the `-sis` family above.
71
+ 'axis',
72
+ 'praxis',
73
+ 'prophylaxis',
74
+ 'aegis',
75
+ 'cannabis',
76
+ 'chrysalis',
77
+ 'clematis',
78
+ 'dais',
79
+ 'dermis',
80
+ 'epidermis',
81
+ 'epiglottis',
82
+ 'glottis',
83
+ 'hubris',
84
+ 'ibis',
85
+ 'iris',
86
+ 'mantis',
87
+ 'marquis',
88
+ 'megalopolis',
89
+ 'metropolis',
90
+ 'pelvis',
91
+ 'portcullis',
92
+ 'proboscis',
93
+ 'tennis',
94
+ 'trellis',
95
+ // `-us` singulars.
96
+ 'alumnus',
97
+ 'apparatus',
98
+ 'bonus',
99
+ 'bus',
100
+ 'cactus',
101
+ 'campus',
102
+ 'census',
103
+ 'chorus',
104
+ 'consensus',
105
+ 'corpus',
106
+ 'exodus',
107
+ 'focus',
108
+ 'fungus',
109
+ 'genus',
110
+ 'hiatus',
111
+ 'impetus',
112
+ 'locus',
113
+ 'minus',
114
+ 'modulus',
115
+ 'nexus',
116
+ 'nucleus',
117
+ 'octopus',
118
+ 'opus',
119
+ 'plus',
120
+ 'prospectus',
121
+ 'radius',
122
+ 'sinus',
123
+ 'status',
124
+ 'stimulus',
125
+ 'surplus',
126
+ 'syllabus',
127
+ 'terminus',
128
+ 'thesaurus',
129
+ 'versus',
130
+ 'virus',
131
+ // Remaining singular nouns ending in `s`.
132
+ 'alias',
133
+ 'apropos',
134
+ 'asbestos',
135
+ 'atlas',
136
+ 'bias',
137
+ 'canvas',
138
+ 'chaos',
139
+ 'cosmos',
140
+ 'ethos',
141
+ 'fracas',
142
+ 'gas',
143
+ 'kudos',
144
+ 'lens',
145
+ 'news',
146
+ 'pancreas',
147
+ 'pathos',
148
+ 'rhinoceros',
149
+ 'series',
150
+ 'species',
151
+ 'thermos',
152
+ ]);
153
+ /**
154
+ * Splits a PascalCase/camelCase identifier into its words, keeping an
155
+ * all-caps run together: `DeferAxis` → `Defer`/`Axis`, `HTTPStatus` →
156
+ * `HTTP`/`Status`.
157
+ */
158
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+/g;
159
+ /**
160
+ * The exemption keys on the identifier's FINAL word rather than the whole
161
+ * identifier so it composes with any prefix: `Axis`, `DeferAxis` and
162
+ * `ChartRenderAxis` all resolve to `axis`. Whole-identifier matching would
163
+ * exempt only the bare noun and keep reporting every compound built on it.
164
+ */
165
+ function trailingWordOf(name) {
166
+ const words = name.match(IDENTIFIER_WORD_PATTERN);
167
+ return (words?.[words.length - 1] ?? name).toLowerCase();
168
+ }
169
+ /**
170
+ * True when the identifier's final word is a singular noun that merely ends in
171
+ * `s`. Checked before `pluralize`, whose naive trailing-`s` strip is what
172
+ * misreads these nouns in the first place.
173
+ */
174
+ function endsWithSingularNoun(name) {
175
+ const trailingWord = trailingWordOf(name);
176
+ return (SINGULAR_NOUNS_ENDING_IN_S.has(trailingWord) ||
177
+ SINGULAR_WORD_SUFFIXES.some((suffix) => trailingWord.endsWith(suffix)));
178
+ }
44
179
  /**
45
180
  * Union members that only express absence. Stripping them keeps a nullable
46
181
  * container recognisable as a container: `T[]` and `T[] | null` describe the
@@ -137,6 +272,11 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
137
272
  // Skip checking if name ends with 'Props', 'Params', 'Data', etc.
138
273
  if (NON_PLURALIZABLE_SUFFIXES.some((suffix) => name.toLowerCase().endsWith(suffix.toLowerCase())))
139
274
  return false;
275
+ // Singular nouns that end in `s` must be settled BEFORE pluralize sees
276
+ // them: its trailing-`s` strip turns `DeferAxis` into the non-word
277
+ // `DeferAxi` and reports a rename to it.
278
+ if (endsWithSingularNoun(name))
279
+ return false;
140
280
  // Skip checking if name is already singular according to pluralize
141
281
  if (pluralize.isSingular(name))
142
282
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.175",
3
+ "version": "1.20.176",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.176",
4
+ "date": "2026-08-26T11:56:32.903Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-assert-safe-object-key",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2144
11
+ ],
12
+ "summary": "wrap a coerced key instead of replacing it (closes #2144)"
13
+ },
14
+ {
15
+ "name": "enforce-singular-type-names",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 2143
19
+ ],
20
+ "summary": "exempt singular nouns that merely end in s (closes #2143)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.175",
4
26
  "date": "2026-08-26T06:19:14.998Z",