@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
@@ -218,7 +218,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
218
218
  module.exports = {
219
219
  meta: {
220
220
  name: '@blumintinc/eslint-plugin-blumint',
221
- version: '1.18.5',
221
+ version: '1.18.6',
222
222
  },
223
223
  parseOptions: {
224
224
  ecmaVersion: 2020,
@@ -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
- export declare const noUnnecessaryVerbSuffix: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"unnecessaryVerbSuffix", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
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
- // Note: context.getDeclaredVariables is the API available in the
262
- // pinned @typescript-eslint version (the SourceCode-based
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.18.5",
3
+ "version": "1.18.6",
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.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",