@blumintinc/eslint-plugin-blumint 1.17.1 → 1.17.3

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
@@ -217,7 +217,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
217
217
  module.exports = {
218
218
  meta: {
219
219
  name: '@blumintinc/eslint-plugin-blumint',
220
- version: '1.17.1',
220
+ version: '1.17.3',
221
221
  },
222
222
  parseOptions: {
223
223
  ecmaVersion: 2020,
@@ -241,6 +241,41 @@ function isTypeGuardFunction(node) {
241
241
  }
242
242
  return false;
243
243
  }
244
+ // The names below are the built-in TypeScript read-only wrapper types. When a
245
+ // function is annotated with one of these, the annotation is NOT redundant:
246
+ // TypeScript always infers the mutable concrete type (e.g. Set<T> not
247
+ // ReadonlySet<T>), so stripping the annotation silently changes the public
248
+ // return type and lets callers mutate internal state that the author intended
249
+ // to protect.
250
+ const READONLY_TYPE_NAMES = new Set([
251
+ 'ReadonlySet',
252
+ 'ReadonlyMap',
253
+ 'ReadonlyArray',
254
+ 'Readonly',
255
+ ]);
256
+ /**
257
+ * Returns true when `returnType` is a read-only widening annotation — i.e.
258
+ * one that TypeScript would NOT infer on its own and whose removal therefore
259
+ * changes the public API. Two forms are covered:
260
+ *
261
+ * TSTypeReference — ReadonlySet<T>, ReadonlyMap<K,V>, ReadonlyArray<T>,
262
+ * Readonly<T>
263
+ * TSTypeOperator — `readonly T[]` and `readonly [a, b]` tuples
264
+ */
265
+ function isReadonlyWideningReturnType(returnType) {
266
+ const typeAnnotation = returnType.typeAnnotation;
267
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
268
+ const typeName = typeAnnotation.typeName;
269
+ return (typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
270
+ READONLY_TYPE_NAMES.has(typeName.name));
271
+ }
272
+ // `readonly T[]` and `readonly [a, b]` are represented as TSTypeOperator
273
+ // nodes with operator === 'readonly'.
274
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeOperator) {
275
+ return typeAnnotation.operator === 'readonly';
276
+ }
277
+ return false;
278
+ }
244
279
  exports.noExplicitReturnType = (0, createRule_1.createRule)({
245
280
  name: 'no-explicit-return-type',
246
281
  meta: {
@@ -297,6 +332,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
297
332
  if (!returnType)
298
333
  return;
299
334
  if (isTypeGuardFunction(node) ||
335
+ isReadonlyWideningReturnType(returnType) ||
300
336
  (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node))) {
301
337
  return;
302
338
  }
@@ -320,6 +356,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
320
356
  return;
321
357
  }
322
358
  if (isTypeGuardFunction(node) ||
359
+ isReadonlyWideningReturnType(returnType) ||
323
360
  (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node))) {
324
361
  return;
325
362
  }
@@ -334,7 +371,8 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
334
371
  const returnType = node.returnType;
335
372
  if (!returnType)
336
373
  return;
337
- if (isTypeGuardFunction(node)) {
374
+ if (isTypeGuardFunction(node) ||
375
+ isReadonlyWideningReturnType(returnType)) {
338
376
  return;
339
377
  }
340
378
  context.report({
@@ -366,6 +404,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
366
404
  if (!returnType)
367
405
  return;
368
406
  if (isTypeGuardFunction(node.value) ||
407
+ isReadonlyWideningReturnType(returnType) ||
369
408
  (mergedOptions.allowAbstractMethodSignatures &&
370
409
  isInterfaceOrAbstractMethodSignature(node))) {
371
410
  return;
@@ -10,7 +10,12 @@ const COMMON_TYPES = [
10
10
  'Boolean',
11
11
  'Array',
12
12
  'Object',
13
- 'Function',
13
+ // 'Function' is intentionally excluded. Unlike an incidental, rot-prone data
14
+ // type (String/Number/...), a value named *Function is intrinsically and
15
+ // permanently callable, so the marker can never become misleading. Fn/Func/
16
+ // Function are function-ROLE designators (like callback/handler/predicate),
17
+ // not Hungarian type tags — compareFn/mapFn are the ECMAScript/MDN-canonical
18
+ // parameter names. See the ABBREVIATION_MARKERS note and issue #1255.
14
19
  // 'Date', too many false positives
15
20
  'RegExp',
16
21
  'Promise',
@@ -20,16 +25,11 @@ const COMMON_TYPES = [
20
25
  // Abbreviation type markers (e.g. str, arr, obj). No English word is spelled this
21
26
  // way, so their presence as a segment — even in the middle of a name — is
22
27
  // unambiguously a type tag (strName, USER_STR_NAME, ConfigArrSettings).
23
- const ABBREVIATION_MARKERS = [
24
- 'str',
25
- 'num',
26
- 'int',
27
- 'bool',
28
- 'arr',
29
- 'obj',
30
- 'fn',
31
- 'func',
32
- ];
28
+ // `fn`/`func` are deliberately excluded: they abbreviate a value's callable ROLE
29
+ // (like callback/handler/predicate), which is intrinsic and never rots, so
30
+ // checkFn/compareFn/mapFn/renderFunc are legitimate role names, not Hungarian
31
+ // type tags (#1255).
32
+ const ABBREVIATION_MARKERS = ['str', 'num', 'int', 'bool', 'arr', 'obj'];
33
33
  // Combined type markers (former Hungarian prefixes and type suffixes)
34
34
  const TYPE_MARKERS = [
35
35
  ...ABBREVIATION_MARKERS,
@@ -53,7 +53,7 @@ const SINGLE_LETTER_PREFIXES = new Set(['b', 'i']);
53
53
  // allowed compound noun PhoneNumber. Abbreviation markers (str/arr/obj/...) are
54
54
  // deliberately excluded: no English word is spelled that way, so their presence
55
55
  // as a segment is unambiguously a type tag even inside a type name.
56
- const FULL_TYPE_WORDS = new Set([...COMMON_TYPES, 'Func'].map((word) => word.toLowerCase()));
56
+ const FULL_TYPE_WORDS = new Set(COMMON_TYPES.map((word) => word.toLowerCase()));
57
57
  // Allowed descriptive suffixes that should not be flagged as Hungarian notation
58
58
  const ALLOWED_SUFFIXES = [
59
59
  'Formatted',
@@ -218,7 +218,7 @@ function splitCamelSegments(name) {
218
218
  // A TYPE name (alias/interface/class) is exempt from a full-type-word marker when
219
219
  // that marker is one clean PascalCase segment among OTHER descriptive segments —
220
220
  // i.e. the word denotes a type concept/relation, not a redundant type tag.
221
- // Examples: StringToNumber, CapitalizedString, PromiseOrValue, FuncKeys.
221
+ // Examples: StringToNumber, CapitalizedString, PromiseOrValue.
222
222
  // Abbreviation markers (str/arr/obj/...) never qualify, so genuine Hungarian type
223
223
  // names like UserStrName / ConfigArrSettings / UserObjData still fire.
224
224
  function isSemanticTypeConcept(typeName) {
@@ -177,9 +177,13 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
177
177
  if (node.parent?.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
178
178
  return true;
179
179
  }
180
- // Allow type assertions within method calls like array.includes()
181
- if (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
182
- node.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
180
+ // Allow type assertions that are direct arguments of any call or new expression.
181
+ // The asserted value is an argument — TypeScript structurally checks it against the
182
+ // parameter type so the returned value is the call's result, not the cast itself.
183
+ // This covers method calls (obj.method(x as T)), plain calls (fn(x as T)), and
184
+ // constructors (new Ctor(x as T)) uniformly.
185
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression ||
186
+ node.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
183
187
  return true;
184
188
  }
185
189
  // Allow type assertions within array includes checks
@@ -195,27 +199,6 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
195
199
  if (isPropertyAccess(node) && isInsideConditionalStatement(node)) {
196
200
  return true;
197
201
  }
198
- // Allow type assertions in object instantiations (as constructor arguments)
199
- if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
200
- node.parent.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
201
- return true;
202
- }
203
- // Allow type assertions as arguments to constructors
204
- if (node.parent?.type === utils_1.AST_NODE_TYPES.NewExpression ||
205
- (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
206
- node.parent.parent?.type === utils_1.AST_NODE_TYPES.NewExpression)) {
207
- return true;
208
- }
209
- // Allow type assertions as arguments to constructor calls
210
- if (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
211
- let current = node.parent;
212
- while (current?.parent) {
213
- if (current.parent.type === utils_1.AST_NODE_TYPES.NewExpression) {
214
- return true;
215
- }
216
- current = current.parent;
217
- }
218
- }
219
202
  // Allow type assertions in JSX attributes/props or object properties
220
203
  if (isInsideJSXAttributeOrObjectProperty(node)) {
221
204
  return true;
@@ -59,9 +59,6 @@ const COMMON_PREPOSITION_SUFFIXES = new Set([
59
59
  'Where',
60
60
  'When',
61
61
  'While',
62
- // Programming-specific suffixes
63
- 'Async',
64
- 'Sync',
65
62
  ]);
66
63
  /**
67
64
  * Phrasal-verb particles that fuse with a preceding past participle to form an
@@ -134,6 +131,41 @@ function isPhrasalVerbEnding(name, suffix) {
134
131
  const lastWord = beforeSuffix.match(/[A-Z]?[a-z]+$/)?.[0] ?? '';
135
132
  return phrasalVerbStem(lastWord) !== null;
136
133
  }
134
+ /**
135
+ * Returns true when `node` (or its relevant ancestor for the declaration kind)
136
+ * is directly wrapped in an export declaration. An exported symbol must not be
137
+ * auto-fixed because the fixer can only rename within the current file, leaving
138
+ * cross-file import references broken.
139
+ *
140
+ * Call this with:
141
+ * - the FunctionDeclaration node for `function foo() {}`
142
+ * - the ArrowFunctionExpression/FunctionExpression node for `const foo = () => {}`
143
+ * (its parent chain: arrow → VariableDeclarator → VariableDeclaration → export?)
144
+ */
145
+ function isExported(node) {
146
+ const parent = node.parent;
147
+ if (!parent)
148
+ return false;
149
+ // FunctionDeclaration directly inside `export function foo() {}`
150
+ if (parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
151
+ parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
152
+ return true;
153
+ }
154
+ // Arrow/FunctionExpression assigned to a VariableDeclarator:
155
+ // VariableDeclarator → VariableDeclaration → ExportNamedDeclaration
156
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
157
+ const varDecl = parent.parent; // VariableDeclaration
158
+ if (!varDecl)
159
+ return false;
160
+ const varDeclParent = varDecl.parent; // possible ExportNamedDeclaration
161
+ if (varDeclParent &&
162
+ (varDeclParent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
163
+ varDeclParent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration)) {
164
+ return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
137
169
  exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
138
170
  name: 'no-unnecessary-verb-suffix',
139
171
  meta: {
@@ -150,7 +182,27 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
150
182
  },
151
183
  defaultOptions: [],
152
184
  create(context) {
153
- function checkFunctionName(node, name) {
185
+ function checkFunctionName(node, name,
186
+ /**
187
+ * The AST node that holds the identifier being renamed.
188
+ * For FunctionDeclaration this is `node.id`; for VariableDeclarator
189
+ * assigned arrows it is `declarator.id`; for Property/MethodDefinition
190
+ * it is the key node. Passing it explicitly avoids re-deriving it inside
191
+ * the fixer and allows the reference-rename loop to skip it cleanly.
192
+ */
193
+ declarationIdNode,
194
+ /**
195
+ * The AST node whose declared variables the scope manager tracks.
196
+ * For FunctionDeclaration this is `node` itself; for a VariableDeclarator
197
+ * arrow it is the VariableDeclarator; for methods/properties it is null
198
+ * because member references are not in scope (resolved via `this.x`).
199
+ */
200
+ scopeNode,
201
+ /**
202
+ * Whether the symbol is exported. When true, the fix is suppressed so we
203
+ * don't produce broken cross-file renames.
204
+ */
205
+ exported) {
154
206
  if (!name)
155
207
  return;
156
208
  for (const suffix of COMMON_PREPOSITION_SUFFIXES) {
@@ -182,25 +234,50 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
182
234
  suggestion,
183
235
  },
184
236
  fix(fixer) {
185
- if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
186
- return fixer.replaceText(node.id, suggestion);
237
+ // An autofix here is only reference-safe when the fixer can
238
+ // rename EVERY use of the symbol, not just its declaration —
239
+ // otherwise call sites are orphaned and produce a ReferenceError
240
+ // (#1256). Two cases make that impossible, so the fix is
241
+ // suppressed (report only, no `--fix` change):
242
+ //
243
+ // 1. Exported symbols — a single-file fixer cannot reach
244
+ // cross-file import references.
245
+ // 2. Member-accessed symbols (class methods, object-literal
246
+ // properties, interface method signatures) — their call
247
+ // sites are member expressions (`this.x()`, `obj.x()`) that
248
+ // the scope manager does not track as variable references,
249
+ // so they cannot be found and renamed syntactically.
250
+ // `scopeNode` is null for exactly these declarations.
251
+ if (exported || !scopeNode || !declarationIdNode) {
252
+ return null;
187
253
  }
188
- else if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
189
- node.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
190
- const parent = node.parent;
191
- if (parent &&
192
- (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
193
- parent.type === utils_1.AST_NODE_TYPES.Property ||
194
- parent.type === utils_1.AST_NODE_TYPES.MethodDefinition)) {
195
- if ('key' in parent) {
196
- return fixer.replaceText(parent.key, suggestion);
197
- }
198
- else if ('id' in parent && parent.id) {
199
- return fixer.replaceText(parent.id, suggestion);
200
- }
254
+ // Scope-tracked symbols (FunctionDeclaration, VariableDeclarator
255
+ // arrows/functions, named FunctionExpression): rename the
256
+ // declaration identifier and every in-file reference together so
257
+ // no call site is left pointing at the old name.
258
+ const fixes = [
259
+ fixer.replaceText(declarationIdNode, suggestion),
260
+ ];
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) {
274
+ // Skip the declaration identifier itself — already handled.
275
+ if (ref.identifier === declarationIdNode)
276
+ continue;
277
+ fixes.push(fixer.replaceText(ref.identifier, suggestion));
201
278
  }
202
279
  }
203
- return null;
280
+ return fixes;
204
281
  },
205
282
  });
206
283
  }
@@ -210,38 +287,49 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
210
287
  return {
211
288
  FunctionDeclaration(node) {
212
289
  if (node.id) {
213
- checkFunctionName(node, node.id.name);
290
+ checkFunctionName(node, node.id.name, node.id, node, isExported(node));
214
291
  }
215
292
  },
216
293
  VariableDeclarator(node) {
217
294
  if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
218
295
  (node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
219
296
  node.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
220
- checkFunctionName(node.init, node.id.name);
297
+ checkFunctionName(node.init, node.id.name, node.id, node, isExported(node.init));
221
298
  }
222
299
  },
223
300
  MethodDefinition(node) {
224
301
  if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
225
- checkFunctionName(node.value, node.key.name);
302
+ // Class methods are called via member expressions (`this.method()`,
303
+ // `instance.method()`) that the scope manager does not track as
304
+ // references. A syntactic single-file fixer therefore cannot find and
305
+ // rename those call sites, so renaming the method would orphan them
306
+ // (#1256). Pass null for both the rename target and scopeNode so the
307
+ // violation is still reported but no unsafe fix is offered.
308
+ checkFunctionName(node.value, node.key.name, null, null, false);
226
309
  }
227
310
  },
228
311
  TSMethodSignature(node) {
229
- // Handle interface method signatures
312
+ // Interface method signatures have their implementations and call sites
313
+ // elsewhere (member accesses on implementers), unreachable from this
314
+ // declaration. Report only — never offer a rename fix.
230
315
  if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
231
- checkFunctionName(node, node.key.name);
316
+ checkFunctionName(node, node.key.name, null, null, false);
232
317
  }
233
318
  },
234
319
  Property(node) {
235
320
  if (node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
236
321
  (node.value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
237
322
  node.value.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
238
- checkFunctionName(node.value, node.key.name);
323
+ // Object-literal method properties are accessed via member expressions
324
+ // (`obj.method()`) the scope manager does not track. As with class
325
+ // methods, the fix is suppressed to avoid orphaning call sites.
326
+ checkFunctionName(node.value, node.key.name, null, null, false);
239
327
  }
240
328
  },
241
329
  FunctionExpression(node) {
242
330
  // Handle named function expressions
243
331
  if (node.id) {
244
- checkFunctionName(node, node.id.name);
332
+ checkFunctionName(node, node.id.name, node.id, node, isExported(node));
245
333
  }
246
334
  },
247
335
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,56 @@
1
1
  [
2
+ {
3
+ "version": "1.17.3",
4
+ "date": "2026-07-02T16:07:25.816Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-unnecessary-verb-suffix",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1256
11
+ ],
12
+ "summary": "make autofix reference-safe (closes #1256)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.17.2",
18
+ "date": "2026-07-02T12:29:19.590Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-explicit-return-type",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1253
25
+ ],
26
+ "summary": "exempt read-only widening return types from removal (closes #1253)"
27
+ },
28
+ {
29
+ "name": "no-hungarian",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1255
33
+ ],
34
+ "summary": "treat Fn/Func/Function as function-role designators, not type tags (closes #1255)"
35
+ },
36
+ {
37
+ "name": "no-type-assertion-returns",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1254
41
+ ],
42
+ "summary": "allow type assertions as call/new arguments in return position (closes #1254)"
43
+ },
44
+ {
45
+ "name": "no-unnecessary-verb-suffix",
46
+ "changeType": "fix",
47
+ "issues": [
48
+ 1252
49
+ ],
50
+ "summary": "stop flagging Async/Sync execution-model suffixes (closes #1252)"
51
+ }
52
+ ]
53
+ },
2
54
  {
3
55
  "version": "1.17.1",
4
56
  "date": "2026-07-01T22:31:34.840Z",