@blumintinc/eslint-plugin-blumint 1.20.99 → 1.20.101

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.99',
226
+ version: '1.20.101',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -4,7 +4,60 @@ exports.enforceCentralizedMockFirestore = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const importInsertion_1 = require("../utils/importInsertion");
7
- const MOCK_FIRESTORE_PATH = '../../../../../__test-utils__/mockFirestore';
7
+ /**
8
+ * The centralized module's identity: the path segments that name it, without
9
+ * the relative prefix a specifier needs to reach it. Keeping the identity
10
+ * separate from the specifier is what makes the self-file guard below correct
11
+ * no matter what prefix the fixer emits — the emitted depth is a hardcoded
12
+ * guess (Issue #1387) and a guard keyed on it would exempt the wrong files.
13
+ */
14
+ const MOCK_FIRESTORE_MODULE = '__test-utils__/mockFirestore';
15
+ const MOCK_FIRESTORE_PATH = `../../../../../${MOCK_FIRESTORE_MODULE}`;
16
+ const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
17
+ /**
18
+ * The module every other file is told to import from is the one module that
19
+ * must define `mockFirestore` locally: rewriting it produces an import of
20
+ * itself, so the canonical implementation is deleted and the module imports a
21
+ * name it no longer defines.
22
+ *
23
+ * The linted path is matched by suffix because it reaches the rule in whatever
24
+ * form the caller used — absolute (`/repo/src/__test-utils__/mockFirestore.ts`)
25
+ * or project-relative (`__test-utils__/mockFirestore.ts`). The suffix has to
26
+ * land on a path-segment boundary, otherwise `not__test-utils__/mockFirestore`
27
+ * — an unrelated module — would be exempted too.
28
+ */
29
+ const isCentralizedMockModule = (filename) => {
30
+ const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
31
+ if (!normalized.endsWith(MOCK_FIRESTORE_MODULE)) {
32
+ return false;
33
+ }
34
+ const suffixStart = normalized.length - MOCK_FIRESTORE_MODULE.length;
35
+ return suffixStart === 0 || normalized[suffixStart - 1] === '/';
36
+ };
37
+ /**
38
+ * Reports whether a flagged declaration sits on the module's export surface,
39
+ * either as `export const mockFirestore = …` or as one binding of an exported
40
+ * multi-declarator `const`.
41
+ *
42
+ * An exported name is a cross-file contract whose importers a single-file fixer
43
+ * cannot reach: retiring the declaration drops the name from the surface and
44
+ * every importer fails to resolve it. The hazard lives entirely in those other
45
+ * files, so it does not depend on whether the declaring file also uses the name
46
+ * — a mock module with no local use sites is the most exposed shape, not the
47
+ * safest. `global-const-style` and `renameFixes` withhold their fixes on the
48
+ * same grounds.
49
+ *
50
+ * Only `ExportNamedDeclaration` can front a flagged node: `export default`
51
+ * takes an expression, never a `const`, and a class property — the other shape
52
+ * this rule flags — belongs to its class rather than to the module, so
53
+ * `export default class { mockFirestore = … }` exports nothing by that name.
54
+ */
55
+ function isExportedDeclaration(node) {
56
+ const statement = node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration
57
+ ? node.parent
58
+ : node;
59
+ return statement.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
60
+ }
8
61
  function isHorizontalWhitespace(character) {
9
62
  return character === ' ' || character === '\t';
10
63
  }
@@ -58,16 +111,20 @@ const STATEMENT_CONTAINERS = new Set([
58
111
  utils_1.AST_NODE_TYPES.TSModuleBlock,
59
112
  ]);
60
113
  /**
61
- * Widens a declaration to the `export` that fronts it, whose keyword lives
62
- * outside the declaration's own range and would otherwise be stranded.
114
+ * The declaration when it stands as a statement of its own, and nothing
115
+ * otherwise.
116
+ *
117
+ * An `export const` is deliberately not widened to the `export` that fronts it:
118
+ * swallowing the keyword retires the name from the module's export surface, so
119
+ * such a declaration has no retirable statement at all and its enclosing
120
+ * `ExportNamedDeclaration` is absent from `STATEMENT_CONTAINERS` for that
121
+ * reason. `isExportedDeclaration` refuses the same shape ahead of this call and
122
+ * covers the multi-declarator form this branch never sees.
63
123
  */
64
124
  function retirableStatement(declaration) {
65
- const statement = declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration
66
- ? declaration.parent
67
- : declaration;
68
- return statement.parent &&
69
- STATEMENT_CONTAINERS.has(statement.parent.type)
70
- ? statement
125
+ return declaration.parent &&
126
+ STATEMENT_CONTAINERS.has(declaration.parent.type)
127
+ ? declaration
71
128
  : undefined;
72
129
  }
73
130
  function retiredSpan(node) {
@@ -167,6 +224,20 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
167
224
  },
168
225
  defaultOptions: [],
169
226
  create(context) {
227
+ // A processor hands the rule a virtual filename for an extracted code block;
228
+ // the physical path is the one that identifies the module on disk.
229
+ const filename = context.getPhysicalFilename
230
+ ? context.getPhysicalFilename()
231
+ : context.getFilename();
232
+ // The centralized module is exempt outright rather than reported without a
233
+ // fix: its local definition IS the canonical one this rule directs every
234
+ // other file to, so there is nothing for its author to do — the message's
235
+ // remedy, "import mockFirestore from the centralized test util", names the
236
+ // file it is reported in. `use-custom-memo`, `use-custom-router` and
237
+ // `use-custom-link` exempt their wrapper implementations the same way.
238
+ if (isCentralizedMockModule(filename)) {
239
+ return {};
240
+ }
170
241
  let hasCentralizedImport = false;
171
242
  const mockFirestoreNodes = new Set();
172
243
  const customMockFirestoreNames = new Set();
@@ -302,6 +373,13 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
302
373
  // them.
303
374
  const removals = [];
304
375
  for (const node of mockFirestoreNodes) {
376
+ if (isExportedDeclaration(node)) {
377
+ // Retiring an exported declaration takes the name off the
378
+ // module's export surface, breaking importers this fixer
379
+ // cannot see. The report stands so the local mock is still
380
+ // surfaced; collapsing it is a cross-file edit a human owns.
381
+ return null;
382
+ }
305
383
  const span = retiredSpan(node);
306
384
  if (!span) {
307
385
  // A declaration that cannot be excised cleanly gets no
@@ -8,6 +8,14 @@ const utils_1 = require("@typescript-eslint/utils");
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const createRule_1 = require("../utils/createRule");
10
10
  const TRANSPARENT_TYPE_NAMES = new Set(['Readonly', 'Resolve']);
11
+ // A binding introduced by any of these declares a type whose shape lives in
12
+ // another module. This rule is purely syntactic and reads a single file, so
13
+ // such a type is opaque to it.
14
+ const IMPORT_BINDING_NODE_TYPES = new Set([
15
+ utils_1.AST_NODE_TYPES.ImportSpecifier,
16
+ utils_1.AST_NODE_TYPES.ImportDefaultSpecifier,
17
+ utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier,
18
+ ]);
11
19
  exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
12
20
  name: 'enforce-identifiable-firestore-type',
13
21
  meta: {
@@ -47,6 +55,10 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
47
55
  let matchingAliasNode = null;
48
56
  let matchingAliasInlineExported = false;
49
57
  let matchingAliasHasIdentifiable = false;
58
+ // Set when resolving the matching alias runs into a type that is declared
59
+ // in another module. Such a type is opaque to this single-file walk, so the
60
+ // absence of `Identifiable` is unproven rather than disproven.
61
+ let matchingAliasLeavesModule = false;
50
62
  const locallyExportedNames = new Set();
51
63
  return {
52
64
  Program() {
@@ -54,6 +66,7 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
54
66
  matchingAliasNode = null;
55
67
  matchingAliasInlineExported = false;
56
68
  matchingAliasHasIdentifiable = false;
69
+ matchingAliasLeavesModule = false;
57
70
  locallyExportedNames.clear();
58
71
  },
59
72
  'Program:exit'(node) {
@@ -69,7 +82,13 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
69
82
  },
70
83
  });
71
84
  }
72
- else if (!matchingAliasHasIdentifiable) {
85
+ else if (!matchingAliasHasIdentifiable &&
86
+ !matchingAliasLeavesModule) {
87
+ // A chain that leaves the module is unknowable here: the imported
88
+ // type may well intersect `Identifiable`, and reporting would demand
89
+ // a type-theoretically redundant `Identifiable &` to silence a claim
90
+ // the rule cannot substantiate. Staying silent trades a false
91
+ // positive for a false negative, which this repo prefers.
73
92
  context.report({
74
93
  node,
75
94
  messageId: 'notExtendingIdentifiable',
@@ -95,6 +114,23 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
95
114
  matchingAliasNode = node;
96
115
  matchingAliasInlineExported =
97
116
  node.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
117
+ // Raised by the resolution walk below whenever a referenced type name
118
+ // resolves to an imported binding, i.e. the alias chain ran out of
119
+ // this file. Scoped to this alias so an import that no chain ever
120
+ // reaches — an unrelated `Timestamp`, or a type argument the walk
121
+ // never descends into — grants no amnesty.
122
+ let leavesModule = false;
123
+ const isImportedBinding = (name) => {
124
+ let scope = context.getScope();
125
+ while (scope) {
126
+ const variable = scope.variables.find((variableNode) => variableNode.name === name);
127
+ if (variable) {
128
+ return variable.defs.some((definition) => IMPORT_BINDING_NODE_TYPES.has(definition.node.type));
129
+ }
130
+ scope = scope.upper;
131
+ }
132
+ return false;
133
+ };
98
134
  const findTypeAliasAnnotation = (typeName) => {
99
135
  let scope = context.getScope();
100
136
  while (scope) {
@@ -108,11 +144,37 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
108
144
  definition.node.typeAnnotation) {
109
145
  return definition.node.typeAnnotation;
110
146
  }
147
+ // The name is bound, but only to something this file cannot
148
+ // see through: an import. Record that the chain crossed the
149
+ // module boundary so the caller distinguishes "proved no
150
+ // Identifiable" from "could not look". A name with no binding
151
+ // at all (a lib global such as `Readonly` or `Map`) is left
152
+ // alone: those are known not to be Identifiable-bearing
153
+ // aliases, and treating them as unknown would silence the
154
+ // rule almost everywhere.
155
+ if (variable.defs.some((definition) => IMPORT_BINDING_NODE_TYPES.has(definition.node.type))) {
156
+ leavesModule = true;
157
+ }
111
158
  }
112
159
  scope = scope.upper;
113
160
  }
114
161
  return null;
115
162
  };
163
+ // `Types.Team` from `import * as Types from '../Team'` names a type
164
+ // in another module just as `Team` from a named import does, but it
165
+ // carries a TSQualifiedName that the walks below never resolve. The
166
+ // leftmost identifier is the namespace binding, so it answers the
167
+ // same question: did the chain leave this file?
168
+ const noteQualifiedNamespaceImport = (qualifiedName) => {
169
+ let left = qualifiedName.left;
170
+ while (left.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
171
+ left = left.left;
172
+ }
173
+ if (left.type === utils_1.AST_NODE_TYPES.Identifier &&
174
+ isImportedBinding(left.name)) {
175
+ leavesModule = true;
176
+ }
177
+ };
116
178
  const isParenthesizedType = (node) => node?.type === 'TSParenthesizedType';
117
179
  const isReadonlyTypeOperator = (node) => node?.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
118
180
  node.operator === 'readonly';
@@ -229,12 +291,18 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
229
291
  const aliasAnnotation = findTypeAliasAnnotation(typeName);
230
292
  return checkType(aliasAnnotation, visitedTypes);
231
293
  }
294
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
295
+ resolvedType.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
296
+ noteQualifiedNamespaceImport(resolvedType.typeName);
297
+ return false;
298
+ }
232
299
  if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
233
300
  return resolvedType.types.some((part) => checkType(part, new Set(visitedTypes)));
234
301
  }
235
302
  return false;
236
303
  };
237
304
  matchingAliasHasIdentifiable = checkType(node.typeAnnotation);
305
+ matchingAliasLeavesModule = leavesModule;
238
306
  }
239
307
  },
240
308
  };
@@ -421,14 +421,17 @@ exports.default = (0, createRule_1.createRule)({
421
421
  if (!declaredVariable) {
422
422
  return null;
423
423
  }
424
- // Exported symbols with in-file use sites are cross-file
425
- // contracts whose importers a single-file fixer cannot reach;
426
- // rewriting the local sites alone would still leave the export
427
- // renamed and importers broken. Report-only. (A bare exported
428
- // declaration with no extra references keeps the historical
429
- // rename behavior nothing to orphan in-file.)
430
- const hasExtraReferences = declaredVariable.references.some((ref) => ref.identifier !== idNode);
431
- if (isExported && hasExtraReferences) {
424
+ // An exported binding's name is a cross-file contract: every
425
+ // importer spells it out in a file this single-file fixer
426
+ // cannot reach, so renaming the declaration breaks them all
427
+ // (TS2724/TS2305, an unresolved JSX element, a `jest.mock`
428
+ // factory key). The hazard lives entirely in those other files,
429
+ // so it does not depend on whether the declaring file also uses
430
+ // the name a constants module with no local use sites is the
431
+ // most exposed shape, not the safest. Report-only; the sibling
432
+ // `as const` fix still applies because it never touches the
433
+ // export name.
434
+ if (isExported) {
432
435
  return null;
433
436
  }
434
437
  // Suppress the fix when `newName` already binds something in a
@@ -95,6 +95,49 @@ function getRenamedPropertyInfo(property) {
95
95
  }
96
96
  return null;
97
97
  }
98
+ /**
99
+ * Node types that can sit between a renamed property and the declarator whose
100
+ * binding pattern it belongs to.
101
+ */
102
+ const BINDING_PATTERN_TYPES = new Set([
103
+ utils_1.AST_NODE_TYPES.ObjectPattern,
104
+ utils_1.AST_NODE_TYPES.ArrayPattern,
105
+ utils_1.AST_NODE_TYPES.AssignmentPattern,
106
+ utils_1.AST_NODE_TYPES.RestElement,
107
+ utils_1.AST_NODE_TYPES.Property,
108
+ ]);
109
+ /**
110
+ * Reports whether `pattern` destructures an `export const { … } = …`
111
+ * declaration.
112
+ *
113
+ * The alias of an exported destructuring IS the module's public export name, so
114
+ * collapsing `export const { id: renamedId }` to `export const { id }` renames
115
+ * the export. Every importer spells the old name out in a file this single-file
116
+ * fixer cannot reach, so the rewrite breaks them all (TS2305/TS2724) with no
117
+ * local symptom. The hazard therefore does not depend on how the declaring file
118
+ * uses the binding — a module that only re-publishes the value is the most
119
+ * exposed shape, not the safest. `global-const-style` and the shared rename
120
+ * fixer withhold their rewrites on the same grounds.
121
+ *
122
+ * The walk climbs the binding pattern rather than reading the property's
123
+ * immediate parent because the rename can be nested
124
+ * (`export const { user: { name: userName } } = data`). It stops at the first
125
+ * ancestor that is not part of a binding pattern, so a destructured parameter
126
+ * of an exported function answers false: that binding is function-local and
127
+ * never an export name.
128
+ */
129
+ function isExportedDestructuringPattern(pattern) {
130
+ let current = pattern;
131
+ while (current && BINDING_PATTERN_TYPES.has(current.type)) {
132
+ current = current.parent;
133
+ }
134
+ if (!current || current.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
135
+ return false;
136
+ }
137
+ const declaration = current.parent;
138
+ return (declaration?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
139
+ declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration);
140
+ }
98
141
  exports.noUnnecessaryDestructuringRename = (0, createRule_1.createRule)({
99
142
  name: 'no-unnecessary-destructuring-rename',
100
143
  meta: {
@@ -310,10 +353,16 @@ exports.noUnnecessaryDestructuringRename = (0, createRule_1.createRule)({
310
353
  return fixes;
311
354
  }
312
355
  function reportAndFixCandidates(candidatesByPattern) {
313
- for (const patternCandidates of candidatesByPattern.values()) {
356
+ for (const [pattern, patternCandidates] of candidatesByPattern) {
357
+ // The destructuring edit and the object-literal edit are one atomic
358
+ // batch — applying either alone leaves the file referencing a name that
359
+ // no longer binds — so an exported pattern withholds the whole batch and
360
+ // stays report-only. Every candidate in a group shares one pattern, so
361
+ // the export answer is a property of the group.
362
+ const withholdFix = isExportedDestructuringPattern(pattern);
314
363
  patternCandidates.forEach((candidate, index) => {
315
364
  const { propertyNode, originalName, aliasIdentifier } = candidate;
316
- const isFixCarrier = index === 0;
365
+ const isFixCarrier = index === 0 && !withholdFix;
317
366
  context.report({
318
367
  node: propertyNode,
319
368
  messageId: 'unnecessaryDestructuringRename',
@@ -16,6 +16,24 @@ const NEXTJS_DATA_FUNCTIONS = new Set([
16
16
  'getStaticProps',
17
17
  'getStaticPaths',
18
18
  ]);
19
+ /**
20
+ * Lexicalized verb-particle compounds whose head happens to be a disallowed
21
+ * prefix. "check in" names an operation exactly — its meaning is not the verb
22
+ * `check` applied to the object `in` — so the generic-prefix heuristic is
23
+ * measuring the wrong lexeme and reports a false positive.
24
+ *
25
+ * Entries are `<verb> <particle>` in lowercase and are matched against the
26
+ * first TWO camelCase segments, so derived names (checkInAndSet, checkOutTeam)
27
+ * inherit the exemption while `check` + object (checkUserPermissions) does not.
28
+ *
29
+ * Criterion for adding an entry: the verb+particle pair must be a *lexicalized*
30
+ * compound — a phrasal verb or domain noun whose meaning is not the sum of its
31
+ * parts (check in, check out) — and its head must be a disallowed prefix.
32
+ * A merely grammatical verb+particle sequence does NOT qualify: `getOutOfSync`,
33
+ * `updateInPlace`, and `processOutQueue` are compositional, so the generic verb
34
+ * still hides what the function does and the rule must keep reporting them.
35
+ */
36
+ const COMPOUND_LEXEMES = new Set(['check in', 'check out']);
19
37
  const SUGGESTED_ALTERNATIVES = {
20
38
  get: ['fetch', 'retrieve', 'compute', 'derive'],
21
39
  update: ['modify', 'set', 'apply'],
@@ -50,6 +68,24 @@ function extractFirstWord(name) {
50
68
  }
51
69
  return firstWord;
52
70
  }
71
+ /**
72
+ * The first two camelCase segments of a name, joined by a space
73
+ * (`checkInAndSet` -> `check in`).
74
+ *
75
+ * Whole-segment equality is load-bearing. `checkInput` segments as
76
+ * `check` + `Input`, so a substring test such as `name.startsWith('checkin')`
77
+ * would exempt it — along with checkInputValidation, CheckInputData and every
78
+ * other `check` + object name that merely begins with a particle's letters.
79
+ */
80
+ function extractCompoundHead(name) {
81
+ const firstWord = extractFirstWord(name);
82
+ const remainder = name.slice(firstWord.length);
83
+ const secondWord = remainder ? extractFirstWord(remainder) : '';
84
+ return `${firstWord} ${secondWord}`.toLowerCase();
85
+ }
86
+ function isCompoundLexeme(name) {
87
+ return COMPOUND_LEXEMES.has(extractCompoundHead(name));
88
+ }
53
89
  exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
54
90
  name: 'semantic-function-prefixes',
55
91
  meta: {
@@ -65,32 +101,33 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
65
101
  },
66
102
  defaultOptions: [],
67
103
  create(context) {
68
- function checkMethodName(node) {
69
- // Skip getters and setters
70
- if (node.kind === 'get' || node.kind === 'set') {
71
- return;
72
- }
73
- const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : '';
74
- if (!methodName)
75
- return;
76
- // Skip if method starts with 'is' (boolean check methods are okay)
77
- if (methodName.startsWith('is'))
104
+ /**
105
+ * Single detection path for every visited shape. Methods and functions
106
+ * differ only in which node carries the report, so sharing the guards keeps
107
+ * an exemption from applying to one shape and not the other.
108
+ */
109
+ function reportIfGenericPrefix(reportNode, functionName) {
110
+ // Skip if the name starts with 'is' (boolean check functions are okay)
111
+ if (functionName.startsWith('is'))
78
112
  return;
79
113
  // Skip Next.js data-fetching functions
80
- if (NEXTJS_DATA_FUNCTIONS.has(methodName))
114
+ if (NEXTJS_DATA_FUNCTIONS.has(functionName))
115
+ return;
116
+ // Skip compound lexemes whose head merely coincides with a banned prefix
117
+ if (isCompoundLexeme(functionName))
81
118
  return;
82
119
  // Extract first word from PascalCase/camelCase
83
- const firstWord = extractFirstWord(methodName);
120
+ const firstWord = extractFirstWord(functionName);
84
121
  // Check for disallowed prefixes
85
122
  // Only flag if the disallowed word is used as a prefix (not the entire name)
86
123
  for (const prefix of DISALLOWED_PREFIXES) {
87
124
  if (firstWord.toLowerCase() === prefix.toLowerCase() &&
88
- firstWord.length < methodName.length) {
125
+ firstWord.length < functionName.length) {
89
126
  context.report({
90
- node: node.key,
127
+ node: reportNode,
91
128
  messageId: 'avoidGenericPrefix',
92
129
  data: {
93
- functionName: methodName,
130
+ functionName,
94
131
  prefix,
95
132
  alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
96
133
  },
@@ -99,6 +136,16 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
99
136
  }
100
137
  }
101
138
  }
139
+ function checkMethodName(node) {
140
+ // Skip getters and setters
141
+ if (node.kind === 'get' || node.kind === 'set') {
142
+ return;
143
+ }
144
+ const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : '';
145
+ if (!methodName)
146
+ return;
147
+ reportIfGenericPrefix(node.key, methodName);
148
+ }
102
149
  function checkFunctionName(node) {
103
150
  // Skip anonymous functions
104
151
  if (!node.id && node.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
@@ -115,31 +162,7 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
115
162
  }
116
163
  if (!functionName)
117
164
  return;
118
- // Skip if function starts with 'is' (boolean check functions are okay)
119
- if (functionName.startsWith('is'))
120
- return;
121
- // Skip Next.js data-fetching functions
122
- if (NEXTJS_DATA_FUNCTIONS.has(functionName))
123
- return;
124
- // Extract first word from PascalCase/camelCase
125
- const firstWord = extractFirstWord(functionName);
126
- // Check for disallowed prefixes
127
- // Only flag if the disallowed word is used as a prefix (not the entire name)
128
- for (const prefix of DISALLOWED_PREFIXES) {
129
- if (firstWord.toLowerCase() === prefix.toLowerCase() &&
130
- firstWord.length < functionName.length) {
131
- context.report({
132
- node: node.id || node,
133
- messageId: 'avoidGenericPrefix',
134
- data: {
135
- functionName,
136
- prefix,
137
- alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
138
- },
139
- });
140
- break;
141
- }
142
- }
165
+ reportIfGenericPrefix(node.id || node, functionName);
143
166
  }
144
167
  return {
145
168
  FunctionDeclaration: checkFunctionName,
@@ -157,11 +157,12 @@ const buildVariableRenameFixes = ({ fixer, sourceCode, variable, declarationId,
157
157
  // above. Skipping it also avoids overlapping fix ranges, which ESLint rejects.
158
158
  const references = variable.references.filter((reference) => reference.identifier !== declarationId);
159
159
  // An exported symbol is a cross-file contract whose importers a single-file
160
- // fixer cannot reach. Rewriting the local use sites alone still renames the
161
- // export and breaks every importer, so decline. (A bare exported declaration
162
- // with no other in-file reference keeps the rename: there is nothing to
163
- // orphan locally, matching the established behavior in `global-const-style`.)
164
- if (references.length > 0 && isExportedDeclaration(declarationId)) {
160
+ // fixer cannot reach. Renaming the declaration breaks every importer, so
161
+ // decline. (The hazard lives entirely in those other files, so it does not
162
+ // depend on whether the declaring file also uses the name: a bare
163
+ // `export const` with no in-file reference is the most exposed shape, not the
164
+ // safest one. `global-const-style` withholds its rename on the same grounds.)
165
+ if (isExportedDeclaration(declarationId)) {
165
166
  return null;
166
167
  }
167
168
  const fixes = [declarationFix];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.99",
3
+ "version": "1.20.101",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,64 @@
1
1
  [
2
+ {
3
+ "version": "1.20.101",
4
+ "date": "2026-08-04T16:47:09.253Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-centralized-mock-firestore",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1703
11
+ ],
12
+ "summary": "exempt the centralized module and exported declarations (closes #1703)"
13
+ },
14
+ {
15
+ "name": "enforce-identifiable-firestore-type",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1705
19
+ ],
20
+ "summary": "stay silent when the alias chain leaves the module (closes #1705)"
21
+ },
22
+ {
23
+ "name": "enforce-react-type-naming",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1701
27
+ ],
28
+ "summary": "withhold the rename for every exported declaration (closes #1701)"
29
+ },
30
+ {
31
+ "name": "no-unnecessary-destructuring-rename",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1702
35
+ ],
36
+ "summary": "withhold the fix on an exported pattern (closes #1702)"
37
+ },
38
+ {
39
+ "name": "semantic-function-prefixes",
40
+ "changeType": "fix",
41
+ "issues": [
42
+ 1704
43
+ ],
44
+ "summary": "exempt the compound lexemes checkIn and checkOut (closes #1704)"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "version": "1.20.100",
50
+ "date": "2026-08-04T13:28:51.191Z",
51
+ "rules": [
52
+ {
53
+ "name": "global-const-style",
54
+ "changeType": "fix",
55
+ "issues": [
56
+ 1700
57
+ ],
58
+ "summary": "withhold the rename for every exported declaration (closes #1700)"
59
+ }
60
+ ]
61
+ },
2
62
  {
3
63
  "version": "1.20.99",
4
64
  "date": "2026-08-04T12:38:33.934Z",