@blumintinc/eslint-plugin-blumint 1.20.85 → 1.20.87

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noExplicitReturnType = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const importRemoval_1 = require("../utils/importRemoval");
6
7
  const defaultOptions = {
7
8
  allowRecursiveFunctions: true,
8
9
  allowOverloadedFunctions: true,
@@ -725,16 +726,45 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
725
726
  return (Boolean(mergedOptions.allowVoidReturnTypes) &&
726
727
  declaresVoidResult(returnType));
727
728
  }
728
- function fixReturnType(fixer, node) {
729
- // Some nodes expose returnType directly while others nest it under value.
730
- const returnType = 'returnType' in node
731
- ? node.returnType
732
- : 'value' in node
733
- ? node.value.returnType
734
- : null;
735
- if (!returnType)
736
- return null;
737
- return fixer.remove(returnType);
729
+ /**
730
+ * Reports an annotation, taking with it any import it was the only
731
+ * consumer of. The two are one fix: applying either half alone leaves the
732
+ * file worse than applying neither.
733
+ *
734
+ * Orphanhood is judged against this annotation's own removal and the file
735
+ * as it stands, never against what the rest of the `--fix` run might also
736
+ * delete. A sibling annotation naming the same type may be
737
+ * `eslint-disable`d — which a rule cannot see, since suppression is
738
+ * applied to reports after they are emitted — so an edit that assumes its
739
+ * sibling will also go deletes an import the surviving annotation still
740
+ * references, trading an unused import for a dangling type. Judging one
741
+ * edit at a time is suppression-safe by construction: a suppressed
742
+ * report's fix never applies, so it can never have been depended on.
743
+ *
744
+ * The cost is that a type shared by several strippable annotations is not
745
+ * unbound in a single pass; each pass removes the annotations it can see,
746
+ * and only a pass that leaves the binding with no reference at all
747
+ * removes the import.
748
+ */
749
+ function reportAnnotation(node, returnType, strippable) {
750
+ const importRanges = strippable
751
+ ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [returnType.range])
752
+ : null;
753
+ context.report({
754
+ node: returnType,
755
+ messageId: strippable
756
+ ? 'noExplicitReturnTypeInferable'
757
+ : 'noExplicitReturnTypeNonInferable',
758
+ data: { functionKind: describeFunctionKind(node) },
759
+ ...(importRanges
760
+ ? {
761
+ fix: (fixer) => [
762
+ fixer.remove(returnType),
763
+ ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
764
+ ],
765
+ }
766
+ : {}),
767
+ });
738
768
  }
739
769
  return {
740
770
  FunctionDeclaration(node) {
@@ -749,17 +779,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
749
779
  isReturnTypeRequiredByRecursion(node)) {
750
780
  return;
751
781
  }
752
- const isInferable = Boolean(node.body);
753
- context.report({
754
- node: returnType,
755
- messageId: isInferable
756
- ? 'noExplicitReturnTypeInferable'
757
- : 'noExplicitReturnTypeNonInferable',
758
- data: { functionKind: describeFunctionKind(node) },
759
- ...(isInferable
760
- ? { fix: (fixer) => fixReturnType(fixer, node) }
761
- : {}),
762
- });
782
+ reportAnnotation(node, returnType, Boolean(node.body));
763
783
  },
764
784
  FunctionExpression(node) {
765
785
  const returnType = node.returnType;
@@ -776,12 +796,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
776
796
  isReturnTypeRequiredByRecursion(node)) {
777
797
  return;
778
798
  }
779
- context.report({
780
- node: returnType,
781
- messageId: 'noExplicitReturnTypeInferable',
782
- data: { functionKind: describeFunctionKind(node) },
783
- fix: (fixer) => fixReturnType(fixer, node),
784
- });
799
+ reportAnnotation(node, returnType, true);
785
800
  },
786
801
  ArrowFunctionExpression(node) {
787
802
  const returnType = node.returnType;
@@ -793,12 +808,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
793
808
  isReturnTypeRequiredByRecursion(node)) {
794
809
  return;
795
810
  }
796
- context.report({
797
- node: returnType,
798
- messageId: 'noExplicitReturnTypeInferable',
799
- data: { functionKind: describeFunctionKind(node) },
800
- fix: (fixer) => fixReturnType(fixer, node),
801
- });
811
+ reportAnnotation(node, returnType, true);
802
812
  },
803
813
  TSMethodSignature(node) {
804
814
  const returnType = node.returnType;
@@ -811,11 +821,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
811
821
  isOverloadedFunction(node)) {
812
822
  return;
813
823
  }
814
- context.report({
815
- node: returnType,
816
- messageId: 'noExplicitReturnTypeNonInferable',
817
- data: { functionKind: describeFunctionKind(node) },
818
- });
824
+ reportAnnotation(node, returnType, false);
819
825
  },
820
826
  MethodDefinition(node) {
821
827
  const returnType = node.value.returnType;
@@ -829,17 +835,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
829
835
  isReturnTypeRequiredByRecursion(node)) {
830
836
  return;
831
837
  }
832
- const isInferable = Boolean(node.value.body);
833
- context.report({
834
- node: returnType,
835
- messageId: isInferable
836
- ? 'noExplicitReturnTypeInferable'
837
- : 'noExplicitReturnTypeNonInferable',
838
- data: { functionKind: describeFunctionKind(node) },
839
- ...(isInferable
840
- ? { fix: (fixer) => fixReturnType(fixer, node) }
841
- : {}),
842
- });
838
+ reportAnnotation(node, returnType, Boolean(node.value.body));
843
839
  },
844
840
  TSAbstractMethodDefinition(node) {
845
841
  const returnType = node.value.returnType;
@@ -852,11 +848,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
852
848
  }
853
849
  // Abstract methods never have bodies; they are always non-inferable and
854
850
  // intentionally have no fixer.
855
- context.report({
856
- node: returnType,
857
- messageId: 'noExplicitReturnTypeNonInferable',
858
- data: { functionKind: describeFunctionKind(node) },
859
- });
851
+ reportAnnotation(node, returnType, false);
860
852
  },
861
853
  TSDeclareFunction(node) {
862
854
  const returnType = node.returnType;
@@ -869,11 +861,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
869
861
  isOverloadedTsDeclareFunction(node)) {
870
862
  return;
871
863
  }
872
- context.report({
873
- node: returnType,
874
- messageId: 'noExplicitReturnTypeNonInferable',
875
- data: { functionKind: describeFunctionKind(node) },
876
- });
864
+ reportAnnotation(node, returnType, false);
877
865
  },
878
866
  };
879
867
  },
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const ASTHelpers_1 = require("../utils/ASTHelpers");
7
7
  const disableDirectives_1 = require("../utils/disableDirectives");
8
+ const importInsertion_1 = require("../utils/importInsertion");
8
9
  const REACT_MODULE = 'react';
9
10
  const FRAGMENT_NAME = 'Fragment';
10
11
  /**
@@ -151,29 +152,6 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
151
152
  }
152
153
  return null;
153
154
  }
154
- /**
155
- * Where a brand-new import statement can be spliced in without changing
156
- * what the file's directives govern. A whole-line insertion directly before
157
- * the first statement slides in between an `eslint-disable-next-line`
158
- * comment and the line it applies to, silently retargeting that directive
159
- * at the import. Anchoring above such comments keeps every directive
160
- * pointed at the code its author aimed it at.
161
- */
162
- function findImportAnchor() {
163
- const firstStatement = sourceCode.ast.body[0];
164
- let anchor = firstStatement;
165
- const leadingComments = sourceCode.getCommentsBefore(firstStatement);
166
- for (let index = leadingComments.length - 1; index >= 0; index--) {
167
- const comment = leadingComments[index];
168
- const [directive] = (0, disableDirectives_1.parseDisableDirectives)([comment]);
169
- if (directive?.kind !== 'disable-next-line' ||
170
- comment.loc.end.line + 1 !== anchor.loc.start.line) {
171
- break;
172
- }
173
- anchor = comment;
174
- }
175
- return anchor;
176
- }
177
155
  /**
178
156
  * Adds Fragment to an existing react import or creates a new one. The
179
157
  * emitted shape is a value named specifier, since the rewritten element
@@ -205,10 +183,14 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
205
183
  return fixer.insertTextAfter(lastSpecifier, `, { ${FRAGMENT_NAME} }`);
206
184
  }
207
185
  }
208
- // No React import found, create a new one
209
- const importText = `import { ${FRAGMENT_NAME} } from '${REACT_MODULE}';\n`;
210
- const indentation = sourceCode.text.match(/^[ \t]*/m)?.[0] || '';
211
- return fixer.insertTextBefore(findImportAnchor(), indentation + importText);
186
+ // No react declaration can host the specifier, so the fix emits its own
187
+ // declaration. The shared anchor keeps the file's prologue intact: a
188
+ // `'use client'` directive stays the first statement, a `#!` shebang
189
+ // stays at character 0, a header comment stays above the code it covers,
190
+ // and an `eslint-disable-next-line` keeps pointing at the line its author
191
+ // aimed it at instead of at the inserted import.
192
+ const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
193
+ return (0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, anchor, `import { ${FRAGMENT_NAME} } from '${REACT_MODULE}';\n`);
212
194
  }
213
195
  return {
214
196
  // Find JSX Fragment shorthand (<></>)
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const utils_1 = require("@typescript-eslint/utils");
9
9
  const createRule_1 = require("../utils/createRule");
10
10
  const ASTHelpers_1 = require("../utils/ASTHelpers");
11
+ const importInsertion_1 = require("../utils/importInsertion");
11
12
  // The module's path below the project root doubles as the bare specifier,
12
13
  // which is precisely why the root tsconfig `paths` and the Jest mapper resolve
13
14
  // it.
@@ -321,23 +322,6 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
321
322
  function findImportKey(importMap, predicate) {
322
323
  return Array.from(importMap.entries()).find(([, value]) => predicate(value))?.[0];
323
324
  }
324
- /**
325
- * Helper to find the last directive prologue in a statement list
326
- */
327
- function findLastDirective(body) {
328
- let lastDirective;
329
- for (const stmt of body) {
330
- if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
331
- stmt.expression.type === utils_1.AST_NODE_TYPES.Literal &&
332
- typeof stmt.expression.value === 'string' &&
333
- typeof stmt.directive === 'string') {
334
- lastDirective = stmt;
335
- continue;
336
- }
337
- break;
338
- }
339
- return lastDirective;
340
- }
341
325
  return {
342
326
  // Track imports from queryKeys.ts
343
327
  ImportDeclaration(node) {
@@ -514,13 +498,23 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
514
498
  fixes.push(fixer.insertTextBefore(firstImport, importText));
515
499
  }
516
500
  else {
517
- const lastDirective = findLastDirective(sourceCode.ast.body);
518
- if (lastDirective) {
519
- fixes.push(fixer.insertTextAfter(lastDirective, `\n${importText}`));
520
- }
521
- else {
522
- fixes.push(fixer.insertTextBeforeRange([0, 0], importText));
523
- }
501
+ // A file's first import may cross only the
502
+ // whitespace the source opens with. The
503
+ // shared anchor is the floor of that climb:
504
+ // it clears the directive prologue, but
505
+ // also a `#!` shebang or a leading header
506
+ // comment, which text spliced at character
507
+ // 0 would displace.
508
+ const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
509
+ const anchorIndex = anchor.kind === 'before'
510
+ ? anchor.target.range[0]
511
+ : anchor.index;
512
+ const opensFile = sourceCode.text
513
+ .slice(0, anchorIndex)
514
+ .trim() === '';
515
+ fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, opensFile
516
+ ? { kind: 'index', index: 0 }
517
+ : anchor, importText));
524
518
  }
525
519
  }
526
520
  scheduledQueryKeyNamedImports.add(suggestedConstant);
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.preferSpreadOverReassembly = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
4
9
  const utils_1 = require("@typescript-eslint/utils");
5
10
  const createRule_1 = require("../utils/createRule");
6
11
  const ASTHelpers_1 = require("../utils/ASTHelpers");
@@ -216,12 +221,8 @@ function namesOfMembers(members) {
216
221
  return names;
217
222
  }
218
223
  /**
219
- * Finds a type alias or interface declared at the top level of the file being
220
- * linted, including one that is exported.
221
- *
222
- * Resolution stops at the file boundary on purpose: an imported name's members
223
- * live in a module this rule cannot read, and guessing at them would be the
224
- * opposite of a proof.
224
+ * Finds a type alias or interface declared at the top level of a program,
225
+ * including one that is exported.
225
226
  */
226
227
  function findLocalTypeDeclaration(program, name) {
227
228
  for (const statement of program.body) {
@@ -237,6 +238,170 @@ function findLocalTypeDeclaration(program, name) {
237
238
  }
238
239
  return null;
239
240
  }
241
+ /**
242
+ * Finds a type alias or interface a module exports by writing `export` in front
243
+ * of the declaration itself.
244
+ *
245
+ * Only that spelling qualifies. `export { X }` and `export { X } from './y'` are
246
+ * indistinguishable at the specifier — the second names a declaration in a third
247
+ * module — and `export * from './y'` names no declaration at all, so following
248
+ * either would be a guess rather than a proof.
249
+ */
250
+ function findDirectlyExportedTypeDeclaration(program, name) {
251
+ for (const statement of program.body) {
252
+ if (statement.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
253
+ !statement.declaration) {
254
+ continue;
255
+ }
256
+ const declaration = statement.declaration;
257
+ if ((declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
258
+ declaration.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) &&
259
+ declaration.id.name === name) {
260
+ return declaration;
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+ const RELATIVE_SOURCE = /^\.\.?\//;
266
+ /**
267
+ * A type declaration overwhelmingly lives in a `.ts` sibling, so that extension
268
+ * is tried first; the rest follow a bundler's own order.
269
+ */
270
+ const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
271
+ /**
272
+ * The resolution cache stores which path a specifier picks, never the file's
273
+ * contents: a resolution *miss* is safe to keep because a module created later
274
+ * stays unresolved, which reports. The member cache instead carries the resolved
275
+ * file's stamp as its VALUE, so a sibling edited under a long-lived host (the VS
276
+ * Code ESLint extension, eslint_d) is re-read rather than answered from a stale
277
+ * member list — a stale list is what would silence a report the rule owes. The
278
+ * stamp lives in the value so an edited file replaces its entry instead of
279
+ * accumulating one per revision.
280
+ */
281
+ const moduleResolutionCache = new Map();
282
+ const importedMembersCache = new Map();
283
+ /**
284
+ * Stat a candidate path, returning its identity stamp when it is a file. The
285
+ * single stat both resolves existence and stamps the file, so reading a sibling
286
+ * never pays for two.
287
+ */
288
+ function statModule(candidate) {
289
+ try {
290
+ const stats = fs_1.default.statSync(candidate);
291
+ return stats.isFile()
292
+ ? { filePath: candidate, mtimeMs: stats.mtimeMs, size: stats.size }
293
+ : null;
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ /**
300
+ * Resolve a relative import specifier the way a bundler would: sibling file
301
+ * first, then the directory's index file. Package specifiers never reach here —
302
+ * a bare specifier names a module whose location depends on resolution settings
303
+ * this rule does not read, so it proves nothing.
304
+ */
305
+ function resolveRelativeModule(fromDir, source) {
306
+ const cacheKey = JSON.stringify([fromDir, source]);
307
+ const cached = moduleResolutionCache.get(cacheKey);
308
+ if (cached !== undefined) {
309
+ return cached === null ? null : statModule(cached);
310
+ }
311
+ const base = path_1.default.resolve(fromDir, source);
312
+ let resolved = null;
313
+ for (const extension of MODULE_EXTENSIONS) {
314
+ resolved = statModule(`${base}${extension}`);
315
+ if (resolved) {
316
+ break;
317
+ }
318
+ }
319
+ if (!resolved) {
320
+ for (const extension of MODULE_EXTENSIONS) {
321
+ resolved = statModule(path_1.default.join(base, `index${extension}`));
322
+ if (resolved) {
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ moduleResolutionCache.set(cacheKey, resolved?.filePath ?? null);
328
+ return resolved;
329
+ }
330
+ /**
331
+ * `undefined` means the parser has not been looked up yet; `null` means the
332
+ * lookup failed and must not be retried.
333
+ */
334
+ let foreignParse;
335
+ /**
336
+ * The sibling module is read with the parser that backs
337
+ * `@typescript-eslint/utils` itself, loaded lazily and memoized (failure
338
+ * included) so the resolution cost is paid once per process. An install that
339
+ * somehow lacks the parser degrades to "proves nothing", which keeps the rule
340
+ * reporting rather than throwing.
341
+ */
342
+ function getForeignParse() {
343
+ if (foreignParse === undefined) {
344
+ try {
345
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
346
+ const estree = require('@typescript-eslint/typescript-estree');
347
+ foreignParse = estree?.parse ?? null;
348
+ }
349
+ catch {
350
+ foreignParse = null;
351
+ }
352
+ }
353
+ return foreignParse;
354
+ }
355
+ /**
356
+ * Parse a sibling module into an AST. Only a real parse can decide what a type
357
+ * declares: text that merely looks like a declaration — inside a string, a
358
+ * template literal, a comment or a nested scope — must never stand in for it. A
359
+ * file that cannot be read or parsed proves nothing.
360
+ */
361
+ function parseModuleProgram(filePath) {
362
+ const parse = getForeignParse();
363
+ if (!parse) {
364
+ return null;
365
+ }
366
+ try {
367
+ return parse(fs_1.default.readFileSync(filePath, 'utf8'), {
368
+ jsx: true,
369
+ loc: false,
370
+ range: false,
371
+ comment: false,
372
+ });
373
+ }
374
+ catch {
375
+ return null;
376
+ }
377
+ }
378
+ /**
379
+ * Locate the relative import that introduces `localName` as a type.
380
+ *
381
+ * Only a named specifier qualifies, in either the `import type { X }` or the
382
+ * `import { X }` spelling. A namespace import is referenced as `Ns.X`, a
383
+ * qualified name this enumerator does not read, and a default-exported type has
384
+ * no name to look up in the sibling.
385
+ */
386
+ function findRelativeTypeImport(program, localName) {
387
+ for (const statement of program.body) {
388
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
389
+ typeof statement.source.value !== 'string' ||
390
+ !RELATIVE_SOURCE.test(statement.source.value)) {
391
+ continue;
392
+ }
393
+ for (const specifier of statement.specifiers) {
394
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
395
+ specifier.local.name === localName) {
396
+ return {
397
+ source: statement.source.value,
398
+ exported: specifier.imported.name,
399
+ };
400
+ }
401
+ }
402
+ }
403
+ return null;
404
+ }
240
405
  /**
241
406
  * Enumerates every property name a type node declares, or null when the member
242
407
  * list cannot be established with certainty.
@@ -247,6 +412,9 @@ function findLocalTypeDeclaration(program, name) {
247
412
  * conditional type, any other generic instantiation and an interface with an
248
413
  * `extends` clause all describe a member set assembled elsewhere, so none of
249
414
  * them can prove anything here.
415
+ *
416
+ * A name the file does not declare is looked up in the relative module that
417
+ * imports it, once; see {@link importedTypeMemberNames}.
250
418
  */
251
419
  function memberNamesOf(typeNode, scope, seen = new Set()) {
252
420
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
@@ -273,7 +441,20 @@ function memberNamesOf(typeNode, scope, seen = new Set()) {
273
441
  }
274
442
  seen.add(name);
275
443
  const declaration = findLocalTypeDeclaration(scope.program, name);
276
- if (!declaration || declaration.typeParameters) {
444
+ if (!declaration) {
445
+ // The file does not declare the name, so the only remaining source of a
446
+ // written-down member list is the module it comes from.
447
+ return scope.resolveImportedMembers?.(name) ?? null;
448
+ }
449
+ return memberNamesOfDeclaration(declaration, scope, seen);
450
+ }
451
+ /**
452
+ * The member names a type alias or interface declares. A generic declaration
453
+ * describes a different member set per instantiation and an interface with an
454
+ * `extends` clause inherits members written elsewhere, so neither enumerates.
455
+ */
456
+ function memberNamesOfDeclaration(declaration, scope, seen) {
457
+ if (declaration.typeParameters) {
277
458
  return null;
278
459
  }
279
460
  if (declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
@@ -284,6 +465,65 @@ function memberNamesOf(typeNode, scope, seen = new Set()) {
284
465
  }
285
466
  return namesOfMembers(declaration.body.body);
286
467
  }
468
+ /**
469
+ * The member names of a type the file under lint imports from a relative
470
+ * sibling, or null when the sibling proves nothing.
471
+ *
472
+ * Organising a module's types in a neighbouring `types.ts` is the ordinary
473
+ * layout, so a proof that stops at the file boundary misses the common case
474
+ * rather than the edge case (#1644). The hop is a single one: the sibling's own
475
+ * scope carries no `resolveImportedMembers`, so a type it in turn imports, a
476
+ * re-export and a barrel all stop the walk and leave the pick unproven, which
477
+ * reports. Within the sibling, resolution is the same enumerator the file under
478
+ * lint gets — alias chains, `Readonly`/`Required`/`Partial` unwrapping and the
479
+ * shadowing guard all behave identically there.
480
+ */
481
+ function importedTypeMemberNames(program, fromDir, localName) {
482
+ const typeImport = findRelativeTypeImport(program, localName);
483
+ if (!typeImport) {
484
+ return null;
485
+ }
486
+ const resolved = resolveRelativeModule(fromDir, typeImport.source);
487
+ if (!resolved) {
488
+ return null;
489
+ }
490
+ const cacheKey = JSON.stringify([resolved.filePath, typeImport.exported]);
491
+ const cached = importedMembersCache.get(cacheKey);
492
+ if (cached &&
493
+ cached.mtimeMs === resolved.mtimeMs &&
494
+ cached.size === resolved.size) {
495
+ return cached.members;
496
+ }
497
+ const members = readExportedTypeMembers(resolved.filePath, typeImport.exported);
498
+ importedMembersCache.set(cacheKey, {
499
+ mtimeMs: resolved.mtimeMs,
500
+ size: resolved.size,
501
+ members,
502
+ });
503
+ return members;
504
+ }
505
+ function readExportedTypeMembers(filePath, exported) {
506
+ const siblingProgram = parseModuleProgram(filePath);
507
+ if (!siblingProgram) {
508
+ return null;
509
+ }
510
+ const declaration = findDirectlyExportedTypeDeclaration(siblingProgram, exported);
511
+ if (!declaration) {
512
+ return null;
513
+ }
514
+ let siblingBoundNames = null;
515
+ const siblingScope = {
516
+ program: siblingProgram,
517
+ isLocallyBound: (name) => {
518
+ if (!siblingBoundNames) {
519
+ siblingBoundNames = new Set();
520
+ collectLocallyBoundNames(siblingProgram, siblingBoundNames);
521
+ }
522
+ return siblingBoundNames.has(name);
523
+ },
524
+ };
525
+ return memberNamesOfDeclaration(declaration, siblingScope, new Set([exported]));
526
+ }
287
527
  /**
288
528
  * For a JSX element, returns the set of destructured names that are forwarded
289
529
  * with identical key names (e.g. `hits={hits}`, `isLoading={isLoading}`).
@@ -707,6 +947,17 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
707
947
  const minFields = options?.minFields ?? DEFAULT_MIN_FIELDS;
708
948
  const sourceCode = context.getSourceCode();
709
949
  const program = sourceCode.ast;
950
+ // A relative specifier is anchored at the directory ESLint was configured
951
+ // with, never the node process cwd. The two differ under the VS Code ESLint
952
+ // extension, in monorepos and for any programmatic `new Linter({ cwd })`,
953
+ // and anchoring at the process cwd there reads the wrong directory — which
954
+ // in this rule's safe direction merely loses the proof, but loses it exactly
955
+ // where the sibling `types.ts` layout is most common (issue #1476).
956
+ const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
957
+ const rawFilename = context.getFilename();
958
+ const containingDir = path_1.default.dirname(path_1.default.isAbsolute(rawFilename)
959
+ ? rawFilename
960
+ : path_1.default.resolve(cwd, rawFilename));
710
961
  // The scan walks the whole file, so it is deferred until a key-preserving
711
962
  // operator actually turns up in a position the proof depends on — most
712
963
  // files never reach it.
@@ -720,6 +971,9 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
720
971
  }
721
972
  return locallyBoundNames.has(name);
722
973
  },
974
+ // Reached only from the about-to-report path, so a file with nothing to
975
+ // report never stats or reads a sibling.
976
+ resolveImportedMembers: (name) => importedTypeMemberNames(program, containingDir, name),
723
977
  };
724
978
  /**
725
979
  * Walks an expression toward its syntactic root and returns the type node
@@ -857,10 +1111,11 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
857
1111
  *
858
1112
  * The proof runs in the safe direction only. A member set that matches the
859
1113
  * pick exactly is exhaustive, so the rewrite is behavior-preserving and the
860
- * rule still reports; a type it cannot resolve — imported, a union, an
861
- * index signature, or an instantiation of anything but a key-preserving
862
- * operator — yields no proof and the rule likewise still reports. Silence
863
- * is reserved for the case where the widening is demonstrated.
1114
+ * rule still reports; a type it cannot resolve — a union, an index
1115
+ * signature, an instantiation of anything but a key-preserving operator, or
1116
+ * an import whose module it declines to follow — yields no proof and the
1117
+ * rule likewise still reports. Silence is reserved for the case where the
1118
+ * widening is demonstrated.
864
1119
  */
865
1120
  function isProvablyNarrowingPick(fn, param, destructuredNames) {
866
1121
  // An explicit annotation overrides whatever the call site would imply,