@blumintinc/eslint-plugin-blumint 1.18.5 → 1.18.6
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
|
@@ -67,6 +67,52 @@ const ALLOWED_SUFFIXES = [
|
|
|
67
67
|
];
|
|
68
68
|
// Common compound nouns that should not be flagged as Hungarian notation
|
|
69
69
|
const ALLOWED_COMPOUND_NOUNS = ['PhoneNumber', 'EmailAddress', 'PostalCode'];
|
|
70
|
+
// Domain-entity head nouns that legitimately precede a "Number" suffix. In
|
|
71
|
+
// <entity>Number the trailing "Number" is the HEAD NOUN of the domain concept
|
|
72
|
+
// (the number OF an issue/line/round/version — GitHub's REST field is literally
|
|
73
|
+
// `issue_number`), not a type marker bolted onto the name. Removing it yields a
|
|
74
|
+
// wrong name (`issue` denotes the whole issue object, not its number), so these
|
|
75
|
+
// are domain compounds, not Hungarian notation — the same reasoning that
|
|
76
|
+
// motivated PhoneNumber/EmailAddress/PostalCode (#640), generalized to the whole
|
|
77
|
+
// <entity>Number category (#1277). Words that are themselves quantities (count,
|
|
78
|
+
// age, index, size, amount, ...) are intentionally ABSENT: for them "Number" is
|
|
79
|
+
// a redundant type tag, so <quantity>Number stays flagged as Hungarian.
|
|
80
|
+
const DOMAIN_NUMBER_HEAD_NOUNS = new Set([
|
|
81
|
+
'phone',
|
|
82
|
+
'issue',
|
|
83
|
+
'line',
|
|
84
|
+
'round',
|
|
85
|
+
'version',
|
|
86
|
+
'account',
|
|
87
|
+
'match',
|
|
88
|
+
'order',
|
|
89
|
+
'invoice',
|
|
90
|
+
'ticket',
|
|
91
|
+
'serial',
|
|
92
|
+
'model',
|
|
93
|
+
'page',
|
|
94
|
+
'reference',
|
|
95
|
+
'confirmation',
|
|
96
|
+
'tracking',
|
|
97
|
+
'license',
|
|
98
|
+
'part',
|
|
99
|
+
'revision',
|
|
100
|
+
'build',
|
|
101
|
+
'sequence',
|
|
102
|
+
'port',
|
|
103
|
+
'card',
|
|
104
|
+
'contract',
|
|
105
|
+
'document',
|
|
106
|
+
'receipt',
|
|
107
|
+
'registration',
|
|
108
|
+
'flight',
|
|
109
|
+
'room',
|
|
110
|
+
'seat',
|
|
111
|
+
'block',
|
|
112
|
+
'route',
|
|
113
|
+
'channel',
|
|
114
|
+
'badge',
|
|
115
|
+
]);
|
|
70
116
|
// Common built-in JavaScript prototype methods
|
|
71
117
|
const BUILT_IN_METHODS = new Set([
|
|
72
118
|
// String methods
|
|
@@ -234,6 +280,24 @@ function isSemanticTypeConcept(typeName) {
|
|
|
234
280
|
// concept (e.g. Extract+Number) rather than bare type tags glued together.
|
|
235
281
|
return segments.some((segment) => !FULL_TYPE_WORDS.has(segment.toLowerCase()));
|
|
236
282
|
}
|
|
283
|
+
// Is `name` a domain compound of the form <entity>Number, where the word directly
|
|
284
|
+
// before the trailing "Number" is a known domain-entity noun (issueNumber,
|
|
285
|
+
// lineNumber, roundNumber, versionNumber)? Only the LAST head segment is
|
|
286
|
+
// consulted, so prefixed variants generalize (githubIssueNumber, currentLineNumber
|
|
287
|
+
// pass) while numeric-head compounds still read as Hungarian (maxCountNumber ->
|
|
288
|
+
// head segment "Count", not a domain entity -> still flagged).
|
|
289
|
+
function isDomainNumberCompound(name) {
|
|
290
|
+
if (!name.endsWith('Number')) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const head = name.slice(0, -'Number'.length);
|
|
294
|
+
if (head.length === 0) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const segments = splitCamelSegments(head);
|
|
298
|
+
const lastSegment = segments[segments.length - 1];
|
|
299
|
+
return (!!lastSegment && DOMAIN_NUMBER_HEAD_NOUNS.has(lastSegment.toLowerCase()));
|
|
300
|
+
}
|
|
237
301
|
exports.noHungarian = (0, createRule_1.createRule)({
|
|
238
302
|
name: 'no-hungarian',
|
|
239
303
|
meta: {
|
|
@@ -374,6 +438,21 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
374
438
|
normalizedVarName.length > normalizedMarker.length &&
|
|
375
439
|
(/[A-Z0-9]/.test(variableName[variableName.length - normalizedMarker.length - 1]) ||
|
|
376
440
|
/[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
|
|
441
|
+
// A trailing "...Number" whose head noun is a domain entity
|
|
442
|
+
// (issueNumber, lineNumber, roundNumber, versionNumber) is a domain
|
|
443
|
+
// compound, not a Hungarian type tag: the suffix names WHAT the value
|
|
444
|
+
// is (the number OF an issue — GitHub's REST field is `issue_number`),
|
|
445
|
+
// and stripping it destroys the concept (`issue` = the whole object).
|
|
446
|
+
// Generalizes #640's PhoneNumber/EmailAddress/PostalCode carve-out to
|
|
447
|
+
// the whole <entity>Number category (#1277). Scoped to the full-word
|
|
448
|
+
// `Number` marker only: abbreviation tags (str/num/obj/arr/bool) are
|
|
449
|
+
// handled above and still fire, and numeric-quantity heads
|
|
450
|
+
// (countNumber, ageNumber, indexNumber) keep firing because such heads
|
|
451
|
+
// are deliberately absent from DOMAIN_NUMBER_HEAD_NOUNS.
|
|
452
|
+
if (normalizedMarker === 'number' &&
|
|
453
|
+
isDomainNumberCompound(variableName)) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
377
456
|
return true;
|
|
378
457
|
}
|
|
379
458
|
// Full type-word markers (non-abbreviations: String, Number, Function,
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noUnnecessaryVerbSuffix: TSESLint.RuleModule<"unnecessaryVerbSuffix", [], TSESLint.RuleListener>;
|
|
@@ -166,6 +166,46 @@ function isExported(node) {
|
|
|
166
166
|
}
|
|
167
167
|
return false;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Walks a scope chain upward from `scope` (inclusive) and reports whether
|
|
171
|
+
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
172
|
+
* Mirrors how the engine resolves an identifier at a use site: the first scope
|
|
173
|
+
* on the chain that declares the name wins. Used to detect whether a rewritten
|
|
174
|
+
* reference would be captured by a binding sitting between it and the
|
|
175
|
+
* declaration it currently resolves to.
|
|
176
|
+
*/
|
|
177
|
+
function isNameBoundInChain(scope, stopScope, targetName) {
|
|
178
|
+
let current = scope;
|
|
179
|
+
while (current) {
|
|
180
|
+
if (current.set.has(targetName)) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (current === stopScope) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
current = current.upper;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Walks a scope subtree rooted at `root` and reports whether `targetName` is
|
|
192
|
+
* declared anywhere within it — the renamed function's own parameters and body
|
|
193
|
+
* bindings. A `suggestion` binding here would shadow the function's new name
|
|
194
|
+
* (the self-shadowing trap in #1278).
|
|
195
|
+
*/
|
|
196
|
+
function isNameBoundInSubtree(root, targetName) {
|
|
197
|
+
const stack = [root];
|
|
198
|
+
while (stack.length > 0) {
|
|
199
|
+
const scope = stack.pop();
|
|
200
|
+
if (scope.set.has(targetName)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
for (const child of scope.childScopes) {
|
|
204
|
+
stack.push(child);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
169
209
|
exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
170
210
|
name: 'no-unnecessary-verb-suffix',
|
|
171
211
|
meta: {
|
|
@@ -182,6 +222,47 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
182
222
|
},
|
|
183
223
|
defaultOptions: [],
|
|
184
224
|
create(context) {
|
|
225
|
+
/**
|
|
226
|
+
* Returns true when renaming the symbol to `suggestion` would collide with
|
|
227
|
+
* an existing binding in any scope the rename touches, making the autofix
|
|
228
|
+
* semantics-changing (and thus unsafe). The strip-suffix fix rewrites the
|
|
229
|
+
* declaration plus every in-file reference to `suggestion`; if `suggestion`
|
|
230
|
+
* already resolves to a different binding the rewrite would:
|
|
231
|
+
* - redeclare a name already bound in the declaration scope,
|
|
232
|
+
* - capture a call site onto an intervening binding (e.g. turning
|
|
233
|
+
* `const line = lineAt(...)` into the TDZ self-reference
|
|
234
|
+
* `const line = line(...)`, #1278), or
|
|
235
|
+
* - shadow the function's own new name from inside its body.
|
|
236
|
+
* In every such case the fix is suppressed (report-only) so the developer
|
|
237
|
+
* picks a non-colliding name — the safety standard core rename fixers hold.
|
|
238
|
+
*/
|
|
239
|
+
function renameWouldCollide(functionNode, variable, suggestion) {
|
|
240
|
+
const scopeManager = context.sourceCode.scopeManager;
|
|
241
|
+
const functionScope = scopeManager?.acquire(functionNode) ?? null;
|
|
242
|
+
const declarationScope = variable?.scope ?? functionScope?.upper ?? null;
|
|
243
|
+
// (1) Declaration site: a `suggestion` already bound in the scope that
|
|
244
|
+
// holds the declaration would make the rename a redeclaration/shadow.
|
|
245
|
+
if (declarationScope?.set.has(suggestion)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
// (2) Reference sites: a binding sitting between a reference and the
|
|
249
|
+
// declaration scope would swallow the rewritten identifier — the
|
|
250
|
+
// reference would resolve to that binding instead of the function.
|
|
251
|
+
if (variable && declarationScope) {
|
|
252
|
+
for (const ref of variable.references) {
|
|
253
|
+
const referenceScope = ref.from ?? declarationScope;
|
|
254
|
+
if (isNameBoundInChain(referenceScope, declarationScope, suggestion)) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// (3) The function's own parameters/body: a `suggestion` binding there
|
|
260
|
+
// would shadow the function's new name.
|
|
261
|
+
if (functionScope && isNameBoundInSubtree(functionScope, suggestion)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
185
266
|
function checkFunctionName(node, name,
|
|
186
267
|
/**
|
|
187
268
|
* The AST node that holds the identifier being renamed.
|
|
@@ -251,6 +332,24 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
251
332
|
if (exported || !scopeNode || !declarationIdNode) {
|
|
252
333
|
return null;
|
|
253
334
|
}
|
|
335
|
+
// Note: context.getDeclaredVariables is the API available in the
|
|
336
|
+
// pinned @typescript-eslint version (the SourceCode-based
|
|
337
|
+
// replacement is not yet in these type definitions).
|
|
338
|
+
// getDeclaredVariables returns ALL variables the node declares
|
|
339
|
+
// (e.g. for a FunctionDeclaration it includes the function name
|
|
340
|
+
// variable AND its parameter variables). Pick the one whose name
|
|
341
|
+
// matches the symbol being renamed so we only follow references
|
|
342
|
+
// to the name, not parameters.
|
|
343
|
+
const declaredVars = context.getDeclaredVariables(scopeNode);
|
|
344
|
+
const targetVariable = declaredVars.find((variable) => variable.name === name) ??
|
|
345
|
+
null;
|
|
346
|
+
// Suppress the fix when the suggested name already binds
|
|
347
|
+
// something in a scope the rename would touch — a rename fixer
|
|
348
|
+
// must never change program semantics or break compilation
|
|
349
|
+
// (#1278).
|
|
350
|
+
if (renameWouldCollide(node, targetVariable, suggestion)) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
254
353
|
// Scope-tracked symbols (FunctionDeclaration, VariableDeclarator
|
|
255
354
|
// arrows/functions, named FunctionExpression): rename the
|
|
256
355
|
// declaration identifier and every in-file reference together so
|
|
@@ -258,19 +357,8 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
258
357
|
const fixes = [
|
|
259
358
|
fixer.replaceText(declarationIdNode, suggestion),
|
|
260
359
|
];
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// replacement is not yet in these type definitions).
|
|
264
|
-
const declaredVars = context.getDeclaredVariables(scopeNode);
|
|
265
|
-
// getDeclaredVariables returns ALL variables the node declares
|
|
266
|
-
// (e.g. for a FunctionDeclaration it includes the function name
|
|
267
|
-
// variable AND its parameter variables). Filter to the one whose
|
|
268
|
-
// name matches the symbol being renamed so we only follow
|
|
269
|
-
// references to the name, not parameters.
|
|
270
|
-
for (const variable of declaredVars) {
|
|
271
|
-
if (variable.name !== name)
|
|
272
|
-
continue;
|
|
273
|
-
for (const ref of variable.references) {
|
|
360
|
+
if (targetVariable) {
|
|
361
|
+
for (const ref of targetVariable.references) {
|
|
274
362
|
// Skip the declaration identifier itself — already handled.
|
|
275
363
|
if (ref.identifier === declarationIdNode)
|
|
276
364
|
continue;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.18.6",
|
|
4
|
+
"date": "2026-07-09T08:45:37.007Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-hungarian",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1277
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt domain-entity <noun>Number compounds (closes #1277)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-unnecessary-verb-suffix",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1278
|
|
19
|
+
],
|
|
20
|
+
"summary": "bail autofix on rename collision (closes #1278)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.18.5",
|
|
4
26
|
"date": "2026-07-09T03:26:45.486Z",
|