@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.97

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.
@@ -152,6 +152,232 @@ function hasOverloadSignatures(node) {
152
152
  return !hasBody && key.name === targetName;
153
153
  });
154
154
  }
155
+ function getClassOf(node) {
156
+ const classBody = node.parent;
157
+ if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
158
+ return null;
159
+ }
160
+ const classNode = classBody.parent;
161
+ if (classNode &&
162
+ (classNode.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
163
+ classNode.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
164
+ return classNode;
165
+ }
166
+ return null;
167
+ }
168
+ /**
169
+ * A member typed as a function is as binding as a method signature: a getter
170
+ * returning the function's *result* is not assignable to the function type, so
171
+ * such a member is a contract match too.
172
+ */
173
+ function isFunctionTypeNode(node) {
174
+ if (!node)
175
+ return false;
176
+ switch (node.type) {
177
+ case utils_1.AST_NODE_TYPES.TSFunctionType:
178
+ case utils_1.AST_NODE_TYPES.TSConstructorType:
179
+ return true;
180
+ case utils_1.AST_NODE_TYPES.TSUnionType:
181
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
182
+ return node.types.some(isFunctionTypeNode);
183
+ default:
184
+ return false;
185
+ }
186
+ }
187
+ function knownMemberName(key, computed) {
188
+ if (!key || computed)
189
+ return null;
190
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier)
191
+ return key.name;
192
+ if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
193
+ return key.value;
194
+ }
195
+ return null;
196
+ }
197
+ function findVariableInScopeChain(scope, name) {
198
+ let current = scope;
199
+ while (current) {
200
+ const variable = current.variables.find((entry) => entry.name === name);
201
+ if (variable) {
202
+ return variable;
203
+ }
204
+ current = current.upper;
205
+ }
206
+ return null;
207
+ }
208
+ function collectTypeElementMembers(elements, out) {
209
+ for (const element of elements) {
210
+ if (element.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
211
+ // A `get`/`set` signature already describes property access, so a getter
212
+ // implementation satisfies it — only call signatures bind.
213
+ if (element.kind === 'get' || element.kind === 'set')
214
+ continue;
215
+ const name = knownMemberName(element.key, element.computed);
216
+ if (name)
217
+ out.instance.add(name);
218
+ continue;
219
+ }
220
+ if (element.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
221
+ isFunctionTypeNode(element.typeAnnotation?.typeAnnotation)) {
222
+ const name = knownMemberName(element.key, element.computed);
223
+ if (name)
224
+ out.instance.add(name);
225
+ }
226
+ }
227
+ }
228
+ function collectClassContractMembers(classNode, out) {
229
+ for (const member of classNode.body.body) {
230
+ if (member.type === utils_1.AST_NODE_TYPES.StaticBlock)
231
+ continue;
232
+ const isStatic = member.static ?? false;
233
+ const target = isStatic ? out.staticSide : out.instance;
234
+ if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
235
+ member.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
236
+ if (member.kind !== 'method')
237
+ continue;
238
+ const name = knownMemberName(member.key, member.computed);
239
+ if (name)
240
+ target.add(name);
241
+ continue;
242
+ }
243
+ if ((member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
244
+ member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) &&
245
+ isFunctionTypeNode(member.typeAnnotation?.typeAnnotation)) {
246
+ const name = knownMemberName(member.key, member.computed);
247
+ if (name)
248
+ target.add(name);
249
+ }
250
+ }
251
+ }
252
+ function collectContractFromName(name, scope, seen, out) {
253
+ if (seen.has(name))
254
+ return;
255
+ seen.add(name);
256
+ const variable = findVariableInScopeChain(scope, name);
257
+ // No binding at all (a global/ambient type) or a binding with no declaration
258
+ // node (a lib type) leaves the contract unknowable from this file.
259
+ if (!variable || variable.defs.length === 0) {
260
+ out.resolved = false;
261
+ return;
262
+ }
263
+ for (const def of variable.defs) {
264
+ const declaration = def.node;
265
+ switch (declaration.type) {
266
+ case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
267
+ collectContractFromInterface(declaration, scope, seen, out);
268
+ break;
269
+ case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
270
+ collectContractFromTypeNode(declaration.typeAnnotation, scope, seen, out);
271
+ break;
272
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
273
+ case utils_1.AST_NODE_TYPES.ClassExpression:
274
+ collectContractFromClass(declaration, scope, seen, out);
275
+ break;
276
+ default:
277
+ // An import binding, an enum, a value — nothing whose members this file
278
+ // can enumerate.
279
+ out.resolved = false;
280
+ }
281
+ }
282
+ }
283
+ function collectContractFromHeritage(heritage, scope, seen, out) {
284
+ // A qualified reference (`ns.Type`) names a declaration this lookup cannot
285
+ // reach, so the contract stays unresolved.
286
+ if (heritage.expression.type !== utils_1.AST_NODE_TYPES.Identifier) {
287
+ out.resolved = false;
288
+ return;
289
+ }
290
+ collectContractFromName(heritage.expression.name, scope, seen, out);
291
+ }
292
+ function collectContractFromInterface(declaration, scope, seen, out) {
293
+ collectTypeElementMembers(declaration.body.body, out);
294
+ for (const heritage of [
295
+ ...(declaration.extends ?? []),
296
+ ...(declaration.implements ?? []),
297
+ ]) {
298
+ collectContractFromHeritage(heritage, scope, seen, out);
299
+ }
300
+ }
301
+ function collectContractFromClass(declaration, scope, seen, out) {
302
+ collectClassContractMembers(declaration, out);
303
+ if (declaration.superClass) {
304
+ if (declaration.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
305
+ collectContractFromName(declaration.superClass.name, scope, seen, out);
306
+ }
307
+ else {
308
+ // A mixin call or member expression base class: unknowable members.
309
+ out.resolved = false;
310
+ }
311
+ }
312
+ for (const heritage of declaration.implements ?? []) {
313
+ collectContractFromHeritage(heritage, scope, seen, out);
314
+ }
315
+ }
316
+ function collectContractFromTypeNode(typeNode, scope, seen, out) {
317
+ switch (typeNode.type) {
318
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
319
+ collectTypeElementMembers(typeNode.members, out);
320
+ return;
321
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
322
+ for (const part of typeNode.types) {
323
+ collectContractFromTypeNode(part, scope, seen, out);
324
+ }
325
+ return;
326
+ case utils_1.AST_NODE_TYPES.TSTypeReference:
327
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
328
+ collectContractFromName(typeNode.typeName.name, scope, seen, out);
329
+ return;
330
+ }
331
+ out.resolved = false;
332
+ return;
333
+ default:
334
+ // Mapped, conditional, indexed-access and utility-type shapes describe
335
+ // members this rule cannot enumerate syntactically.
336
+ out.resolved = false;
337
+ }
338
+ }
339
+ /**
340
+ * Resolves what the class's `extends`/`implements` clauses oblige its members
341
+ * to look like, using only declarations visible in the file being linted.
342
+ */
343
+ function resolveHeritageContract(classNode, scope) {
344
+ const heritageNames = [];
345
+ let hasOpaqueHeritage = false;
346
+ if (classNode.superClass) {
347
+ if (classNode.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
348
+ heritageNames.push(classNode.superClass.name);
349
+ }
350
+ else {
351
+ hasOpaqueHeritage = true;
352
+ }
353
+ }
354
+ for (const implemented of classNode.implements ?? []) {
355
+ if (implemented.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
356
+ heritageNames.push(implemented.expression.name);
357
+ }
358
+ else {
359
+ hasOpaqueHeritage = true;
360
+ }
361
+ }
362
+ if (!heritageNames.length && !hasOpaqueHeritage) {
363
+ return { status: 'none' };
364
+ }
365
+ if (hasOpaqueHeritage) {
366
+ return { status: 'unresolvable' };
367
+ }
368
+ const members = {
369
+ instance: new Set(),
370
+ staticSide: new Set(),
371
+ resolved: true,
372
+ };
373
+ const seen = new Set();
374
+ for (const name of heritageNames) {
375
+ collectContractFromName(name, scope, seen, members);
376
+ }
377
+ return members.resolved
378
+ ? { status: 'resolved', members }
379
+ : { status: 'unresolvable' };
380
+ }
155
381
  exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
156
382
  name: 'prefer-getter-over-parameterless-method',
157
383
  meta: {
@@ -535,6 +761,37 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
535
761
  }
536
762
  }
537
763
  }
764
+ const heritageContracts = new WeakMap();
765
+ /**
766
+ * A method that satisfies an `implements` clause or overrides a base-class
767
+ * member cannot become a getter: the heritage type declares a *method*, so
768
+ * the conversion the rule prescribes is a TS2416/TS2417 compile error. When
769
+ * every heritage reference resolves in-file the exemption is per method —
770
+ * only names the contract declares are spared. When any reference leaves
771
+ * the file (an import, a global, a third-party `.d.ts`, a mixin call) the
772
+ * contract's members are unknowable, so no method of that class can be
773
+ * proven safe and the whole class is spared: a false negative is preferable
774
+ * to prescribing a remedy that does not compile.
775
+ */
776
+ function isConstrainedByHeritage(node) {
777
+ const classNode = getClassOf(node);
778
+ if (!classNode)
779
+ return false;
780
+ let contract = heritageContracts.get(classNode);
781
+ if (!contract) {
782
+ contract = resolveHeritageContract(classNode, context.getScope());
783
+ heritageContracts.set(classNode, contract);
784
+ }
785
+ if (contract.status === 'none')
786
+ return false;
787
+ if (contract.status === 'unresolvable')
788
+ return true;
789
+ const name = node.key.name;
790
+ const isStatic = node.static ?? false;
791
+ return isStatic
792
+ ? contract.members.staticSide.has(name)
793
+ : contract.members.instance.has(name);
794
+ }
538
795
  const callUsedNamesByClass = new WeakMap();
539
796
  const callUsedNamesInFile = new Set();
540
797
  const candidates = [];
@@ -594,6 +851,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
594
851
  return;
595
852
  if (!node.value.body)
596
853
  return;
854
+ if (isConstrainedByHeritage(node))
855
+ return;
597
856
  const name = node.key.name;
598
857
  if (ignoredMethods.has(name))
599
858
  return;
@@ -100,7 +100,8 @@ function collectReferencedTypeNames(node, acc = new Set()) {
100
100
  collectReferencedTypeNames(mapped.typeAnnotation, acc);
101
101
  if (mapped.nameType)
102
102
  collectReferencedTypeNames(mapped.nameType, acc);
103
- if (mapped.typeParameter && mapped.typeParameter.constraint) {
103
+ if (mapped.typeParameter &&
104
+ mapped.typeParameter.constraint) {
104
105
  collectReferencedTypeNames(mapped.typeParameter.constraint, acc);
105
106
  }
106
107
  break;
@@ -162,6 +163,61 @@ function collectReferencedTypeNames(node, acc = new Set()) {
162
163
  }
163
164
  return acc;
164
165
  }
166
+ /**
167
+ * Type-level operators that keep a `typeof` query in the position where the
168
+ * alias derives its own type from the constant, rather than moving the query
169
+ * into a member, parameter or declaration slot. Unions, intersections, `keyof`,
170
+ * element extraction, array wrappers and utility application all still describe
171
+ * "this alias IS the constant's type", which is the remedy this rule asks for.
172
+ *
173
+ * Utility application belongs here for a convergence reason: the remedy for a
174
+ * reported `function f(x: Readonly<typeof CONST>)` is to name that type once as
175
+ * `type T = Readonly<typeof CONST>`. Reporting the extracted alias too would
176
+ * make the remedy its own violation, leaving that class of code with nowhere to
177
+ * land.
178
+ */
179
+ function isAliasDerivationWrapper(parent, child) {
180
+ switch (parent.type) {
181
+ case utils_1.AST_NODE_TYPES.TSUnionType:
182
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
183
+ case utils_1.AST_NODE_TYPES.TSTypeOperator:
184
+ case utils_1.AST_NODE_TYPES.TSArrayType:
185
+ case utils_1.AST_NODE_TYPES.TSTypeReference:
186
+ case utils_1.AST_NODE_TYPES.TSTypeParameterInstantiation:
187
+ return true;
188
+ case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
189
+ // Only the object side derives from the constant. `Foo[typeof KEY]` reads
190
+ // the constant as a lookup key, which is a consumer position like any
191
+ // other annotation.
192
+ return parent.objectType === child;
193
+ default:
194
+ return isParenthesizedType(parent);
195
+ }
196
+ }
197
+ /**
198
+ * True when the query defines the alias itself — `type T = typeof CONST` and the
199
+ * derivation wrappers around it, such as `keyof typeof CONST`,
200
+ * `(typeof CONST)[number]` or `Readonly<typeof CONST>` (Issues #1117, #1175).
201
+ * A query sitting in a member, index signature, function-type parameter,
202
+ * conditional branch, mapped-type constraint or type-parameter default inside the
203
+ * alias body is a use site and stays reportable, matching what the same shape
204
+ * does outside an alias.
205
+ */
206
+ function isCanonicalAliasDerivation(node) {
207
+ let child = node;
208
+ let parent = node.parent;
209
+ while (parent) {
210
+ if (parent.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
211
+ return parent.typeAnnotation === child;
212
+ }
213
+ if (!isAliasDerivationWrapper(parent, child)) {
214
+ return false;
215
+ }
216
+ child = parent;
217
+ parent = parent.parent;
218
+ }
219
+ return false;
220
+ }
165
221
  /** Collects module-level consts and type aliases for quick lookup */
166
222
  function collectTopLevelContext(program) {
167
223
  const topLevelConstInitByName = new Map();
@@ -292,9 +348,12 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
292
348
  if (!collected)
293
349
  return;
294
350
  const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
295
- // Skip if inside a type alias declaration (Issue #1117, #1175)
296
- // This allows 'type T = typeof CONST' as the canonical way to define the alias.
297
- if (ancestors.some((a) => a.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration)) {
351
+ // Skip the alias definition itself (Issue #1117, #1175), which is the
352
+ // canonical way to name the constant's type. The exemption covers the
353
+ // declared type of the alias and its derivation wrappers only; a query
354
+ // buried in an alias member is a use site and reports like the same
355
+ // shape written on an interface or a parameter (Issue #1680).
356
+ if (isCanonicalAliasDerivation(node)) {
298
357
  return;
299
358
  }
300
359
  // Skip `keyof typeof X` as it's a canonical way to derive a union of keys from a constant object.
@@ -26,8 +26,12 @@ module.exports = (0, createRule_1.createRule)({
26
26
  },
27
27
  schema: [],
28
28
  messages: {
29
- useHttpsError: 'Throwing "{{constructorName}}" in Cloud Functions returns a generic 500 and drops the structured status code clients rely on. Throw the proprietary HttpsError instead so responses include the correct status, sanitized message, and logging context.',
30
- useProprietaryHttpsError: '{{reference}} comes from {{source}} and bypasses our proprietary HttpsError wrapper, so responses skip standardized status codes, logging, and client-safe payloads. Import and throw HttpsError from @our-company/errors to keep errors consistent.',
29
+ // The remedy names the module SHAPE, never a package name: the proprietary
30
+ // wrapper lives inside the consuming repository (a shared
31
+ // util/errors/HttpsError), so prescribing an npm specifier sends the
32
+ // reader to install something that does not exist (issue #1685).
33
+ useHttpsError: 'Throwing "{{constructorName}}" in Cloud Functions returns a generic 500 and drops the structured status code clients rely on. Throw the proprietary HttpsError instead so responses include the correct status, sanitized message, and logging context. Import it from the proprietary HttpsError module this codebase owns, such as a shared util/errors/HttpsError, rather than from firebase-admin.',
34
+ useProprietaryHttpsError: '{{reference}} comes from {{source}} and bypasses our proprietary HttpsError wrapper, so responses skip standardized status codes, logging, and client-safe payloads. Import HttpsError from the proprietary HttpsError module this codebase owns, such as a shared util/errors/HttpsError, and throw that to keep errors consistent.',
31
35
  },
32
36
  },
33
37
  defaultOptions: [],
@@ -2,6 +2,42 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useCustomLink = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
+ const LINK_MODULE_PATH = 'src/components/Link';
6
+ /**
7
+ * MUI's documented Next.js integration names the component that adapts
8
+ * `next/link` for the wrapper `NextLinkComposed`, and `src/components/Link`
9
+ * renders it. Keying on the basename alone — rather than a full path — matches
10
+ * wherever the integration component is colocated.
11
+ */
12
+ const INTEGRATION_COMPONENT = 'NextLinkComposed';
13
+ const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
14
+ /**
15
+ * The two modules that implement the wrapper are the ones that must keep
16
+ * importing `next/link`: `src/components/Link` is the module the fixer points
17
+ * every other import at, and it renders `NextLinkComposed`. Rewriting either
18
+ * one manufactures a cycle — `Link` importing itself, or
19
+ * `Link → NextLinkComposed → Link` — so the wrapper evaluates circularly and
20
+ * every consumer of it breaks.
21
+ *
22
+ * The linted path is matched by suffix because it reaches the rule in whatever
23
+ * form the caller used — absolute (`/repo/src/components/Link.tsx`) or
24
+ * project-relative (`src/components/Link.tsx`). The suffix has to land on a
25
+ * path-segment boundary, otherwise `notsrc/components/Link.tsx` — an unrelated
26
+ * module — would be exempted too, and the integration component is matched by
27
+ * basename equality so that `MyNextLinkComposed.tsx` stays reportable.
28
+ */
29
+ const isWrapperImplementation = (filename) => {
30
+ const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
31
+ const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
32
+ if (basename === INTEGRATION_COMPONENT) {
33
+ return true;
34
+ }
35
+ if (!normalized.endsWith(LINK_MODULE_PATH)) {
36
+ return false;
37
+ }
38
+ const suffixStart = normalized.length - LINK_MODULE_PATH.length;
39
+ return suffixStart === 0 || normalized[suffixStart - 1] === '/';
40
+ };
5
41
  exports.useCustomLink = (0, createRule_1.createRule)({
6
42
  name: 'use-custom-link',
7
43
  meta: {
@@ -18,6 +54,14 @@ exports.useCustomLink = (0, createRule_1.createRule)({
18
54
  },
19
55
  defaultOptions: [],
20
56
  create(context) {
57
+ // A processor hands the rule a virtual filename for an extracted code block;
58
+ // the physical path is the one that identifies the module on disk.
59
+ const filename = context.getPhysicalFilename
60
+ ? context.getPhysicalFilename()
61
+ : context.getFilename();
62
+ if (isWrapperImplementation(filename)) {
63
+ return {};
64
+ }
21
65
  return {
22
66
  ImportDeclaration(node) {
23
67
  if (node.source.value === 'next/link') {
@@ -41,7 +85,7 @@ exports.useCustomLink = (0, createRule_1.createRule)({
41
85
  defaultAsSpecifier?.local?.name ||
42
86
  'Link';
43
87
  // Create the new import statement
44
- const newImport = `import ${localName} from 'src/components/Link';`;
88
+ const newImport = `import ${localName} from '${LINK_MODULE_PATH}';`;
45
89
  return fixer.replaceText(node, newImport);
46
90
  },
47
91
  });
@@ -4,6 +4,28 @@ exports.useCustomMemo = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const MEMO_MODULE = `'src/util/memo'`;
7
+ /** `MEMO_MODULE` carries the quotes the fixer emits; a path never does. */
8
+ const MEMO_MODULE_PATH = MEMO_MODULE.slice(1, -1);
9
+ const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
10
+ /**
11
+ * The module the fixer points every `memo` import at is the one module that must
12
+ * keep importing `memo` from `react`: rewriting it makes it import itself, and a
13
+ * self-import evaluates circularly, so the wrapper exports `undefined` and every
14
+ * consumer of it breaks.
15
+ *
16
+ * The linted path is matched by suffix because it reaches the rule in whatever
17
+ * form the caller used — absolute (`/repo/src/util/memo.ts`) or project-relative
18
+ * (`src/util/memo.ts`). The suffix has to land on a path-segment boundary,
19
+ * otherwise `notsrc/util/memo.ts` — an unrelated module — would be exempted too.
20
+ */
21
+ const isMemoModule = (filename) => {
22
+ const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
23
+ if (!normalized.endsWith(MEMO_MODULE_PATH)) {
24
+ return false;
25
+ }
26
+ const suffixStart = normalized.length - MEMO_MODULE_PATH.length;
27
+ return suffixStart === 0 || normalized[suffixStart - 1] === '/';
28
+ };
7
29
  const isComment = (token) => token.type === utils_1.AST_TOKEN_TYPES.Line || token.type === utils_1.AST_TOKEN_TYPES.Block;
8
30
  const isMemoSpecifier = (specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
9
31
  specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -162,6 +184,14 @@ exports.useCustomMemo = (0, createRule_1.createRule)({
162
184
  },
163
185
  defaultOptions: [],
164
186
  create(context) {
187
+ // A processor hands the rule a virtual filename for an extracted code block;
188
+ // the physical path is the one that identifies the module on disk.
189
+ const filename = context.getPhysicalFilename
190
+ ? context.getPhysicalFilename()
191
+ : context.getFilename();
192
+ if (isMemoModule(filename)) {
193
+ return {};
194
+ }
165
195
  return {
166
196
  ImportDeclaration(node) {
167
197
  if (node.source.value !== 'react') {
@@ -3,6 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useCustomRouter = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ROUTER_MODULE_PATH = 'src/hooks/routing/useRouter';
7
+ const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
8
+ /**
9
+ * The module the fixer points every `useRouter` import at is the one module that
10
+ * must keep importing `useRouter` from `next/router`: rewriting it makes it
11
+ * import itself, and a self-import evaluates circularly, so the wrapper exports
12
+ * `undefined` and every consumer of it breaks.
13
+ *
14
+ * The linted path is matched by suffix because it reaches the rule in whatever
15
+ * form the caller used — absolute (`/repo/src/hooks/routing/useRouter.ts`) or
16
+ * project-relative (`src/hooks/routing/useRouter.ts`). The suffix has to land on
17
+ * a path-segment boundary, otherwise `notsrc/hooks/routing/useRouter.ts` — an
18
+ * unrelated module — would be exempted too.
19
+ */
20
+ const isRouterModule = (filename) => {
21
+ const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
22
+ if (!normalized.endsWith(ROUTER_MODULE_PATH)) {
23
+ return false;
24
+ }
25
+ const suffixStart = normalized.length - ROUTER_MODULE_PATH.length;
26
+ return suffixStart === 0 || normalized[suffixStart - 1] === '/';
27
+ };
6
28
  exports.useCustomRouter = (0, createRule_1.createRule)({
7
29
  name: 'use-custom-router',
8
30
  meta: {
@@ -19,6 +41,14 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
19
41
  },
20
42
  defaultOptions: [],
21
43
  create(context) {
44
+ // A processor hands the rule a virtual filename for an extracted code block;
45
+ // the physical path is the one that identifies the module on disk.
46
+ const filename = context.getPhysicalFilename
47
+ ? context.getPhysicalFilename()
48
+ : context.getFilename();
49
+ if (isRouterModule(filename)) {
50
+ return {};
51
+ }
22
52
  return {
23
53
  ImportDeclaration(node) {
24
54
  if (node.source.value === 'next/router') {
@@ -45,7 +75,7 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
45
75
  .map((s) => s.local.name !== s.imported.name
46
76
  ? `useRouter as ${s.local.name}`
47
77
  : 'useRouter')
48
- .join(', ')} } from 'src/hooks/routing/useRouter';`);
78
+ .join(', ')} } from '${ROUTER_MODULE_PATH}';`);
49
79
  }
50
80
  else {
51
81
  // Create a new import for useRouter and keep other imports
@@ -53,7 +83,7 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
53
83
  .map((s) => s.local.name !== s.imported.name
54
84
  ? `useRouter as ${s.local.name}`
55
85
  : 'useRouter')
56
- .join(', ')} } from 'src/hooks/routing/useRouter';\n`;
86
+ .join(', ')} } from '${ROUTER_MODULE_PATH}';\n`;
57
87
  const otherImports = `import { ${otherSpecifiers
58
88
  .map((s) => s.local.name)
59
89
  .join(', ')} } from 'next/router';`;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Collects every `RuleTester` case the suite declares WITHOUT executing any of
3
+ * them, by shadowing `run` on the shared tester instances and then loading each
4
+ * suite for its declarations alone.
5
+ *
6
+ * A guard that wants to exercise fixtures rather than documented snippets has no
7
+ * other way in. `src/tests/*.test.ts` call `RuleTester.run` at module scope, so
8
+ * importing one normally re-executes it — measured at 2350 tests, 2 minutes and
9
+ * 48 cross-file side-effect failures, which is why
10
+ * `recommended-config-fix-closure.test.ts` reads docs fenced blocks instead.
11
+ * Shadowing `run` before the load turns each of those calls into a declaration
12
+ * capture, so the cases are collected at the cost of loading the module and
13
+ * nothing more.
14
+ *
15
+ * The fixtures are worth reaching precisely because they are not the docs: a
16
+ * rule's `valid` list is written to sit on its carve-out boundaries, which is
17
+ * where a sibling fixer destroys an exemption. Every finding of that class
18
+ * (#1595-#1599, #1603, #1677-#1682) came from this corpus; the docs corpus
19
+ * caught none of them.
20
+ */
21
+ /** A single `ruleTester.run(name, rule, tests)` call, captured but not run. */
22
+ export type HarvestedSuite = {
23
+ /** The display name the suite passed to `run`. */
24
+ name: string;
25
+ /** Which shared tester export declared it, which fixes the parser. */
26
+ tester: string;
27
+ /** Basename of the declaring file, so a finding is reproducible by hand. */
28
+ file: string;
29
+ /**
30
+ * The rule object itself. Callers resolve a rule NAME from this by identity
31
+ * against the plugin's own map rather than from `name`: 100 of the suites
32
+ * pass a display name that is not a rule name (`requireMemo`,
33
+ * `prefer-next-dynamic (JSX scenarios)`, `no-hungarian-phone-number-test`),
34
+ * and name-keyed matching silently drops every one of them.
35
+ */
36
+ rule: unknown;
37
+ valid: readonly unknown[];
38
+ invalid: readonly unknown[];
39
+ };
40
+ export type HarvestResult = {
41
+ suites: HarvestedSuite[];
42
+ /** Files that threw while loading, `basename: message`. */
43
+ failures: string[];
44
+ /** Non-vacuity accounting: a silent drop here would fake a clean sweep. */
45
+ filesLoaded: number;
46
+ filesSkipped: number;
47
+ };
48
+ export declare function harvestRuleTesterCases(): HarvestResult;