@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
|
@@ -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
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|