@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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.
Files changed (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The serialization-contract class: the data members and `toJSON` of every type a value's
3
3
  * static type reaches, where the value reaches `JSON.stringify` or a call `ts.serializers`
4
- * names, and `toString` too where it reaches an `unknown` or `any` parameter of a function
5
- * outside the analysis or an erased property of a type the program does not declare. A
6
- * function of the project handing such a parameter to a destination is one, to a fixpoint,
7
- * and a function invoked through `call`, `apply` or `bind` is the callee.
4
+ * names, and `toString` too where it reaches an erased parameter of a function outside the
5
+ * analysis, or an erased property of a type the program does not declare. A function of
6
+ * the project handing such a parameter to a destination is one, to a fixpoint, and a
7
+ * function invoked through `call`, `apply` or `bind` is the callee.
8
8
  */
9
9
 
10
10
  import {
@@ -45,6 +45,8 @@ import {
45
45
  SignatureKind,
46
46
  SymbolFlags,
47
47
  TypeFlags,
48
+ type NodeHandle,
49
+ type Signature,
48
50
  type Symbol as TSSymbol,
49
51
  type Type,
50
52
  } from "@typescript/native/sync";
@@ -64,7 +66,7 @@ import { renderPosition } from "./position.ts";
64
66
  import { must } from "./query.ts";
65
67
  import type { ProjectView } from "./session.ts";
66
68
  import { erasedStores, type ErasedStores } from "./erased-stores.ts";
67
- import { checkedTypes, valueReach, type Reached } from "./value-reach.ts";
69
+ import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
68
70
 
69
71
  /** The serializer every run reads, whatever the configuration names. */
70
72
  const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
@@ -78,6 +80,9 @@ const SERIALIZED_CONVERSION = "toJSON";
78
80
  /** The parameter types that keep nothing of the value they are given. */
79
81
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
80
82
 
83
+ /** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
84
+ const MENTION_DEPTH = 3;
85
+
81
86
  /**
82
87
  * What a destination holds back of a class: its properties, or its properties and its
83
88
  * two conversion methods. The second holds back everything the first does.
@@ -484,14 +489,19 @@ function destinations<Brand>(
484
489
  own.has(path) || input.consumers.some((root) => path === root || path.startsWith(`${root}/`));
485
490
  const implemented = implementedOwners(project, inAnalysis);
486
491
  /**
487
- * Whether every declaration of a callee is outside the analysis or ambient. An ambient
488
- * declaration has no body, so the code that runs is outside the analysis, unless it is a
489
- * member of an interface or an object type a class or a typed object literal of the
490
- * analysis implements; `settled` false skips that question, which asks the checker.
492
+ * Whether every one of `declarations`, all of the callee's by default, is outside the
493
+ * analysis or ambient. An ambient declaration has no body, so the code that runs is
494
+ * outside the analysis, unless it is a member of an interface or an object type a class
495
+ * or a typed object literal of the analysis implements; `settled` false skips that
496
+ * question, which asks the checker.
491
497
  */
492
- const outside = (target: TSSymbol, settled: boolean): boolean =>
493
- target.declarations.length > 0 &&
494
- target.declarations.every((handle) => {
498
+ const outside = (
499
+ target: TSSymbol,
500
+ settled: boolean,
501
+ declarations: readonly NodeHandle[] = target.declarations,
502
+ ): boolean =>
503
+ declarations.length > 0 &&
504
+ declarations.every((handle) => {
495
505
  if (!inAnalysis(handle.path)) {
496
506
  return true;
497
507
  }
@@ -505,6 +515,13 @@ function destinations<Brand>(
505
515
  const owner = must(queries.parentOf(target));
506
516
  return owner === undefined || !implemented().has(owner.id);
507
517
  });
518
+ /**
519
+ * Whether some one declaration of a callee is outside the analysis or ambient, without
520
+ * asking the checker. A project's declaration merged into a library's symbol leaves the
521
+ * library's beside it, so a call can still resolve to the library's.
522
+ */
523
+ const mayBeOutside = (target: TSSymbol): boolean =>
524
+ target.declarations.some((handle) => outside(target, false, [handle]));
508
525
  const invocation = (call: CallExpression): Invocation => {
509
526
  const form = forms.get(call);
510
527
  const method = form === undefined ? undefined : callees.get(form.method);
@@ -513,7 +530,7 @@ function destinations<Brand>(
513
530
  if (
514
531
  form !== undefined &&
515
532
  method !== undefined &&
516
- outside(method, false) &&
533
+ mayBeOutside(method) &&
517
534
  FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
518
535
  ) {
519
536
  return form.invocation;
@@ -524,6 +541,56 @@ function destinations<Brand>(
524
541
  const name = calleeName(invocation(call).callee);
525
542
  return name === undefined ? undefined : callees.get(name);
526
543
  };
544
+ const signatures = new Map<CallExpression, Signature | undefined>();
545
+ const resolvedOf = (call: CallExpression): Signature | undefined => {
546
+ if (!signatures.has(call)) {
547
+ signatures.set(call, must(queries.resolvedSignature(call)));
548
+ }
549
+ return signatures.get(call);
550
+ };
551
+ /**
552
+ * The declarations of a callee that the declaration of a direct call's resolved
553
+ * signature lies in, or all of them where it lies in none: a declaration merged into
554
+ * the callee's symbol that the call does not resolve to does not decide where it runs.
555
+ */
556
+ const calledDeclarations = (call: CallExpression, target: TSSymbol): readonly NodeHandle[] => {
557
+ const declaration = invocation(call).indirect ? undefined : resolvedOf(call)?.declaration;
558
+ const signature =
559
+ declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
560
+ if (declaration === undefined || signature === undefined) {
561
+ return target.declarations;
562
+ }
563
+ const containing = target.declarations.filter((handle) => {
564
+ const node =
565
+ handle.path === declaration.path ? project.declarationAt(handle)?.node : undefined;
566
+ return node !== undefined && node.pos <= signature.pos && signature.end <= node.end;
567
+ });
568
+ return containing.length === 0 ? target.declarations : containing;
569
+ };
570
+ /** Whether a type names a type parameter, through constituents, type arguments and call signatures. */
571
+ const mentions = (type: Type, parameter: Type, depth: number): boolean => {
572
+ // A conditional type an alias names, `Awaited<T>` among them, carries its arguments
573
+ // only on the alias the client's type object reports.
574
+ const aliased = (type as { readonly aliasTypeArguments?: readonly number[] })
575
+ .aliasTypeArguments;
576
+ if (type.id === parameter.id || aliased?.includes(parameter.id) === true) {
577
+ return true;
578
+ }
579
+ if (depth >= MENTION_DEPTH) {
580
+ return false;
581
+ }
582
+ const inner = [
583
+ ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
584
+ ...must(queries.typeArguments(type)),
585
+ ...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
586
+ ...signature.parameters.map((_parameter, at) =>
587
+ must(queries.typeAtPosition(signature, at)),
588
+ ),
589
+ ...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
590
+ ]),
591
+ ];
592
+ return inner.some((one) => mentions(one, parameter, depth + 1));
593
+ };
527
594
  const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
528
595
  const erasedAt = (call: CallExpression, argument: number): boolean => {
529
596
  let indexes = erasedArguments.get(call);
@@ -536,9 +603,33 @@ function destinations<Brand>(
536
603
  ? type === undefined
537
604
  ? []
538
605
  : must(queries.signaturesOf(type, SignatureKind.Call))
539
- : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
606
+ : [resolvedOf(call)].filter((one) => one !== undefined);
607
+ // A type parameter of the callee that nothing constrains and its result does not name
608
+ // keeps nothing of the value either: the instantiated signature names the value's own
609
+ // type there instead, and a result that names it hands the value back.
610
+ const generic = invoked.indirect ? undefined : resolvedOf(call);
611
+ const declared = generic === undefined ? undefined : must(queries.genericOf(generic));
612
+ const unconstrained = (index: number): boolean => {
613
+ const parameter =
614
+ declared === undefined ? undefined : must(queries.parameterType(declared, index));
615
+ const parts =
616
+ parameter === undefined
617
+ ? []
618
+ : parameter.isUnionType()
619
+ ? must(queries.constituents(parameter))
620
+ : [parameter];
621
+ const result = declared === undefined ? undefined : must(queries.returnTypeOf(declared));
622
+ return parts.some((part) => {
623
+ if (!part.isTypeParameter() || (result !== undefined && mentions(result, part, 0))) {
624
+ return false;
625
+ }
626
+ const constraint = must(queries.constraintOf(part));
627
+ return constraint === undefined || (constraint.flags & ERASED) !== 0;
628
+ });
629
+ };
540
630
  indexes = new Set(
541
631
  invoked.arguments.flatMap((_unused, index) =>
632
+ unconstrained(index) ||
542
633
  signatures.some((signature) => {
543
634
  const parameter = must(queries.parameterType(signature, index));
544
635
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
@@ -563,7 +654,7 @@ function destinations<Brand>(
563
654
  if (entry !== undefined) {
564
655
  return { reach: "properties", callee: spelledEntry(entry.entry, held) };
565
656
  }
566
- if (outside(target, true)) {
657
+ if (outside(target, true, calledDeclarations(call, target))) {
567
658
  return erasedAt(call, argument)
568
659
  ? {
569
660
  reach: "conversions",
@@ -602,7 +693,7 @@ function destinations<Brand>(
602
693
  if (target === undefined) {
603
694
  return false;
604
695
  }
605
- if (named.some((one) => names(one, target, chains)) || outside(target, false)) {
696
+ if (named.some((one) => names(one, target, chains)) || mayBeOutside(target)) {
606
697
  return true;
607
698
  }
608
699
  return chains
@@ -733,7 +824,7 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
733
824
  // the value left the analysis.
734
825
  const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
735
826
  (flags & SymbolFlags.Property) !== 0 ||
736
- ((flags & SymbolFlags.Method) !== 0 &&
827
+ ((flags & CONVERSION_MEMBERS) !== 0 &&
737
828
  (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
738
829
  const found: Evidence[] = [];
739
830
  for (const flow of flows) {
package/src/session.ts CHANGED
@@ -129,6 +129,7 @@ export function openEngine(options: EngineOptions): Engine {
129
129
  ...(mapper === undefined ? {} : { runExternalCode: true }),
130
130
  });
131
131
  let closed = false;
132
+ const parsed = new Map<string, { readonly text: string; readonly file: SourceFile }>();
132
133
  return {
133
134
  parseConfigFile: (file) => api.parseConfigFile(file),
134
135
  parseConfigJson: (json, configFile) =>
@@ -160,8 +161,14 @@ export function openEngine(options: EngineOptions): Engine {
160
161
  moduleResolutions: { fallback: "resolve", entries: [...entries] },
161
162
  }),
162
163
  parseSourceFile: (fileName, text) => {
164
+ // Every project of a run reads the same files outside its program, so one parse serves all.
165
+ const known = parsed.get(fileName);
166
+ if (known?.text === text) {
167
+ return known.file;
168
+ }
163
169
  const retained = api.createSourceFile(fileName, text);
164
170
  try {
171
+ parsed.set(fileName, { text, file: retained.sourceFile });
165
172
  return retained.sourceFile;
166
173
  } finally {
167
174
  retained.dispose();
@@ -419,6 +426,8 @@ export interface ProjectDiagnostics {
419
426
  readonly syntactic: readonly Diagnostic[];
420
427
  readonly semantic: readonly Diagnostic[];
421
428
  readonly configParsing: readonly Diagnostic[];
429
+ /** The program's diagnostics placed in no file, each once, its `fileName` undefined. */
430
+ readonly unplaced: readonly Diagnostic[];
422
431
  /** The line and column of one offset into one of the program's files, counted from one. */
423
432
  readonly locate: (
424
433
  fileName: string,
@@ -426,12 +435,31 @@ export interface ProjectDiagnostics {
426
435
  ) => { line: number; column: number } | undefined;
427
436
  }
428
437
 
429
- /** The three diagnostic sets one project carries, read before any analysis. */
438
+ /**
439
+ * The program's diagnostics placed in no file. The per-file sets never carry one; the
440
+ * client answers its `fileName` as `null`, and the global set repeats the program set:
441
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/api/session.go#L3726-L3752
442
+ */
443
+ function unplacedOf(program: Program): readonly Diagnostic[] {
444
+ const seen = new Set<string>();
445
+ return [...program.getGlobalDiagnostics(), ...program.getProgramDiagnostics()]
446
+ .filter((diagnostic) => diagnostic.fileName == null)
447
+ .filter((diagnostic) => {
448
+ const key = `${String(diagnostic.code)}\u0000${diagnostic.text}`;
449
+ const fresh = !seen.has(key);
450
+ seen.add(key);
451
+ return fresh;
452
+ })
453
+ .map((diagnostic) => ({ ...diagnostic, fileName: undefined }));
454
+ }
455
+
456
+ /** The diagnostic sets one project carries, read before any analysis. */
430
457
  export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagnostics {
431
458
  return {
432
459
  syntactic: project.program.getSyntacticDiagnostics(),
433
460
  semantic: project.program.getSemanticDiagnostics(),
434
461
  configParsing: project.program.getConfigFileParsingDiagnostics(),
462
+ unplaced: unplacedOf(project.program),
435
463
  locate: (fileName, offset) => {
436
464
  const file = project.program.getSourceFile(fileName);
437
465
  if (file === undefined || offset < 0 || offset > file.text.length) {
@@ -6,12 +6,18 @@
6
6
 
7
7
  import type { SourceFile } from "@typescript/native/ast";
8
8
  import { commonDirectory, emittedFrom } from "./emit-map.ts";
9
- import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
9
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
11
 
12
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
13
13
  const SUBPATH_WILDCARD = "*";
14
14
 
15
+ /** The extension a source file's name drops when the project writes it out. */
16
+ const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
17
+
18
+ /** The extensions the project may write a source file's output under. */
19
+ const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
20
+
15
21
  /**
16
22
  * Whether one path is what a subpath pattern stands for: the text before the
17
23
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -40,6 +46,47 @@ export interface SourceFiles {
40
46
  named(path: string): readonly SourceFile[];
41
47
  /** The directories the project writes its output below, absolute: its output and declaration directories. */
42
48
  readonly outputDirs: readonly string[];
49
+ /** The output paths, absolute, the project writes one own file to, each of which {@link named} reads back to it. */
50
+ writtenAs(file: SourceFile): readonly string[];
51
+ /**
52
+ * The own files one absolute path names read as an output of the project written below
53
+ * `outputDir`: a manifest that ships from its build directory names its entries there.
54
+ */
55
+ namedAsOutput(outputDir: string, path: string): readonly SourceFile[];
56
+ }
57
+
58
+ /** The first own file of the paths given that the project holds, or none. */
59
+ function heldAt(
60
+ byName: ReadonlyMap<string, SourceFile>,
61
+ paths: readonly string[],
62
+ ): readonly SourceFile[] {
63
+ for (const candidate of paths) {
64
+ const file = byName.get(candidate);
65
+ if (file !== undefined) {
66
+ return [file];
67
+ }
68
+ }
69
+ return [];
70
+ }
71
+
72
+ /** Where one project writes its output, and the directory it writes it from, absolute. */
73
+ function layoutOf<Brand>(
74
+ project: ProjectView<Brand>,
75
+ fileNames: readonly string[],
76
+ ): {
77
+ readonly outDir: string | undefined;
78
+ readonly declarationDir: string | undefined;
79
+ readonly sourceDir: string;
80
+ } {
81
+ const options = project.program.getCompilerOptions();
82
+ const configDir = dirnamePath(project.configFile);
83
+ const at = (option: string | undefined): string | undefined =>
84
+ option === undefined ? undefined : resolvePath(configDir, option);
85
+ return {
86
+ outDir: at(options.outDir),
87
+ declarationDir: at(options.declarationDir),
88
+ sourceDir: at(options.rootDir) ?? commonDirectory(fileNames),
89
+ };
43
90
  }
44
91
 
45
92
  /**
@@ -62,14 +109,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
62
109
  }
63
110
  }
64
111
 
65
- const options = project.program.getCompilerOptions();
66
- const configDir = dirnamePath(project.configFile);
67
- const at = (option: string | undefined): string | undefined =>
68
- option === undefined ? undefined : resolvePath(configDir, option);
69
- const outDir = at(options.outDir);
70
- const declarationDir = at(options.declarationDir);
71
- const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
72
-
112
+ const { outDir, declarationDir, sourceDir } = layoutOf(project, [...byName.keys()]);
73
113
  const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
74
114
 
75
115
  /** The source paths one written path may stand for, the path itself first. */
@@ -79,15 +119,25 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
79
119
  byPath,
80
120
  byName,
81
121
  outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
122
+ writtenAs: (file) => {
123
+ const below = relativePath(sourceDir, file.fileName);
124
+ if (below === undefined) {
125
+ return [];
126
+ }
127
+ const written = [outDir, declarationDir].flatMap((dir) =>
128
+ dir === undefined
129
+ ? []
130
+ : OUTPUT_EXTENSIONS.map((extension) =>
131
+ joinPath(dir, below.replace(SOURCE_EXTENSION, extension)),
132
+ ),
133
+ );
134
+ return [...new Set(written)].filter((path) => heldAt(byName, candidates(path))[0] === file);
135
+ },
136
+ namedAsOutput: (outputDir, path) =>
137
+ heldAt(byName, emittedFrom(path, { outDir: outputDir, sourceDirs: [sourceDir] })),
82
138
  named: (path) => {
83
139
  if (!path.includes(SUBPATH_WILDCARD)) {
84
- for (const candidate of candidates(path)) {
85
- const held = byName.get(candidate);
86
- if (held !== undefined) {
87
- return [held];
88
- }
89
- }
90
- return [];
140
+ return heldAt(byName, candidates(path));
91
141
  }
92
142
  const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
93
143
  return [...byName.values()].filter((file) =>
@@ -0,0 +1,102 @@
1
+ import {
2
+ isBindingElement,
3
+ isBreakStatement,
4
+ isClassDeclaration,
5
+ isClassExpression,
6
+ isContinueStatement,
7
+ isEnumDeclaration,
8
+ isEnumMember,
9
+ isExportDeclaration,
10
+ isExportSpecifier,
11
+ isFunctionDeclaration,
12
+ isFunctionExpression,
13
+ isGetAccessorDeclaration,
14
+ isImportClause,
15
+ isImportEqualsDeclaration,
16
+ isImportSpecifier,
17
+ isInterfaceDeclaration,
18
+ isJsxAttribute,
19
+ isLabeledStatement,
20
+ isMetaProperty,
21
+ isMethodDeclaration,
22
+ isMethodSignatureDeclaration,
23
+ isModuleDeclaration,
24
+ isNamespaceExport,
25
+ isNamespaceExportDeclaration,
26
+ isNamespaceImport,
27
+ isParameterDeclaration,
28
+ isPropertyAccessExpression,
29
+ isPropertyAssignment,
30
+ isPropertyDeclaration,
31
+ isPropertySignatureDeclaration,
32
+ isQualifiedName,
33
+ isSetAccessorDeclaration,
34
+ isTypeAliasDeclaration,
35
+ isTypeParameterDeclaration,
36
+ isVariableDeclaration,
37
+ type Identifier,
38
+ } from "@typescript/native/ast";
39
+
40
+ /**
41
+ * Whether one identifier spells a name without referring to a binding of it: a member's
42
+ * name after a dot, a property or member an object literal, a class, a type or an enum
43
+ * declares, the name a declaration binds, the property a binding element destructures,
44
+ * an import or export specifier's name in another module or under a rename, a JSX
45
+ * attribute's name, a statement label, or the name of a meta-property. A shorthand
46
+ * property and an unrenamed local export are no such spelling: each reads the binding.
47
+ */
48
+ export function spellsNoBinding(node: Identifier): boolean {
49
+ const parent = node.parent;
50
+ if (isPropertyAccessExpression(parent) || isMetaProperty(parent)) {
51
+ return parent.name === node;
52
+ }
53
+ if (isQualifiedName(parent)) {
54
+ return parent.right === node;
55
+ }
56
+ if (isBindingElement(parent)) {
57
+ return parent.propertyName === node || parent.name === node;
58
+ }
59
+ if (isImportSpecifier(parent)) {
60
+ return true;
61
+ }
62
+ if (isExportSpecifier(parent)) {
63
+ return (
64
+ (isExportDeclaration(parent.parent.parent) &&
65
+ parent.parent.parent.moduleSpecifier !== undefined) ||
66
+ (parent.propertyName !== undefined && parent.name === node)
67
+ );
68
+ }
69
+ if (
70
+ isPropertyAssignment(parent) ||
71
+ isPropertyDeclaration(parent) ||
72
+ isMethodDeclaration(parent) ||
73
+ isPropertySignatureDeclaration(parent) ||
74
+ isMethodSignatureDeclaration(parent) ||
75
+ isGetAccessorDeclaration(parent) ||
76
+ isSetAccessorDeclaration(parent) ||
77
+ isEnumMember(parent) ||
78
+ isJsxAttribute(parent) ||
79
+ isVariableDeclaration(parent) ||
80
+ isParameterDeclaration(parent) ||
81
+ isFunctionDeclaration(parent) ||
82
+ isFunctionExpression(parent) ||
83
+ isClassDeclaration(parent) ||
84
+ isClassExpression(parent) ||
85
+ isInterfaceDeclaration(parent) ||
86
+ isTypeAliasDeclaration(parent) ||
87
+ isEnumDeclaration(parent) ||
88
+ isModuleDeclaration(parent) ||
89
+ isTypeParameterDeclaration(parent) ||
90
+ isImportClause(parent) ||
91
+ isNamespaceImport(parent) ||
92
+ isImportEqualsDeclaration(parent) ||
93
+ isNamespaceExport(parent) ||
94
+ isNamespaceExportDeclaration(parent)
95
+ ) {
96
+ return parent.name === node;
97
+ }
98
+ return (
99
+ (isLabeledStatement(parent) || isBreakStatement(parent) || isContinueStatement(parent)) &&
100
+ parent.label === node
101
+ );
102
+ }
@@ -19,6 +19,7 @@ import {
19
19
  UNSCOPED_ENTRY,
20
20
  blankReason,
21
21
  isEntryPath,
22
+ isPartKind,
22
23
  isSymbolRef,
23
24
  siteText,
24
25
  type Mechanism,
@@ -98,7 +99,7 @@ type ByReference = ReadonlyMap<string, readonly InventorySymbol[]>;
98
99
  */
99
100
  function readRecord(value: unknown, site: Position, byReference: ByReference, shape: Shape): Read {
100
101
  const refuse = (text: string, want: string): never => {
101
- throw new SuppressionError(shape.mechanism, `${shape.file} ${siteText(site)}`, text, want);
102
+ throw new SuppressionError(shape.mechanism, siteText(site), text, want);
102
103
  };
103
104
  if (!isObject(value)) {
104
105
  return refuse(JSON.stringify(value), shape.recordWant);
@@ -155,13 +156,23 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
155
156
  if (refusals.length > 0) {
156
157
  return { records: [], refusals };
157
158
  }
158
- const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
159
+ const named =
160
+ byReference.get(declarationKey(symbol, written.path)) ??
161
+ (isPartKind(code)
162
+ ? byReference.get(declarationKey(parameterHolder(symbol), written.path))
163
+ : undefined) ??
164
+ [];
159
165
  if (named.length === 0) {
160
166
  return { records: [{ ...written, bound: "" }], refusals: [] };
161
167
  }
162
168
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
163
169
  }
164
170
 
171
+ /** The reference of the declaration a parameter's reference names it under, or `ref` itself. */
172
+ function parameterHolder(ref: string): string {
173
+ return ref.endsWith(")") ? ref.slice(0, ref.lastIndexOf("(")) : ref;
174
+ }
175
+
165
176
  function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
166
177
  const byReference = new Map<string, InventorySymbol[]>();
167
178
  for (const symbol of symbols) {
@@ -217,7 +228,7 @@ function readDocument(
217
228
  const records: SuppressionRecord[] = [];
218
229
  const refusals: Refusal[] = [];
219
230
  (held as readonly unknown[]).forEach((value, index) => {
220
- const offset = walk.opened[index];
231
+ const offset = walk.starts[index];
221
232
  const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
222
233
  const read = readRecord(value, site, byReference, shape);
223
234
  records.push(...read.records);
@@ -10,8 +10,10 @@ import {
10
10
  type Node,
11
11
  type SourceFile,
12
12
  } from "@typescript/native/ast";
13
+ import { importBindings } from "./import-bindings.ts";
13
14
  import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
14
15
  import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
16
+ import { importRef } from "./ref.ts";
15
17
  import type { ProjectView } from "./session.ts";
16
18
  import { signatureOf } from "./signatures.ts";
17
19
  import {
@@ -134,7 +136,9 @@ function lineComments(file: SourceFile): readonly LineComment[] {
134
136
  const spans: { readonly start: number; readonly end: number }[] = [];
135
137
  const visit = (node: Node): void => {
136
138
  if (LITERALS.has(node.kind)) {
137
- spans.push({ start: tokenStart(text, node.pos), end: node.end });
139
+ // JSX text carries no trivia: its first code unit is at `pos`, even a `//`.
140
+ const start = node.kind === SyntaxKind.JsxText ? node.pos : tokenStart(text, node.pos);
141
+ spans.push({ start, end: node.end });
138
142
  }
139
143
  node.forEachChild(visit);
140
144
  };
@@ -198,6 +202,7 @@ function declarationLines(
198
202
  file: SourceFile,
199
203
  held: Inventory,
200
204
  symbols: ReadonlyMap<string, Declared>,
205
+ targetRoot: string,
201
206
  ): ReadonlyMap<number, readonly Binding[]> {
202
207
  const below = new Map<number, Binding[]>();
203
208
  const add = (offset: number, one: Binding): void => {
@@ -243,6 +248,20 @@ function declarationLines(
243
248
  node.forEachChild(visit);
244
249
  };
245
250
  file.forEachChild(visit);
251
+ // An import binding is a subject of its own, bound on its line as a declaration is.
252
+ const fileId = held.declarations.get(nodeKey(file, file));
253
+ const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
254
+ for (const name of moduleRef === "" ? [] : importBindings(file)) {
255
+ const position = renderPosition(file, targetRoot, name.getStart());
256
+ const one = { id: positionKey(position), ref: importRef(moduleRef, name.text), position };
257
+ let statement: Node = name;
258
+ while (statement.parent.kind !== SyntaxKind.SourceFile) {
259
+ statement = statement.parent;
260
+ }
261
+ for (const at of [statement.pos, name.getStart()]) {
262
+ add(at, { declared: one, part: one });
263
+ }
264
+ }
246
265
  return below;
247
266
  }
248
267
 
@@ -283,7 +302,7 @@ export function inlineDirectives<Brand>(
283
302
  if (comments.length === 0) {
284
303
  continue;
285
304
  }
286
- const below = declarationLines(file, held, symbols);
305
+ const below = declarationLines(file, held, symbols, targetRoot);
287
306
  for (const comment of comments) {
288
307
  const site = renderPosition(file, targetRoot, comment.offset);
289
308
  const target = trails(file, comment.offset) ? site.line : site.line + 1;
package/src/suppress.ts CHANGED
@@ -243,10 +243,11 @@ function claimKey(finding: Finding): string {
243
243
 
244
244
  /**
245
245
  * Whether one record names one finding. A record reaches a finding in two ways: it bound
246
- * the declaration at the finding's position, or it names the finding's reference and the
247
- * file the finding is reported in, each exactly. The second is how an entry copied from a
248
- * finding about a part of a declaration reaches the part, and the only way any record
249
- * reaches a dependency row.
246
+ * the declaration at the finding's position, or it names the finding's reference, or the
247
+ * reference of the declaration holding a part that has one of its own, and the file the
248
+ * finding is reported in, each exactly. The second is how an entry copied from a finding
249
+ * about a part of a declaration reaches the part, and the only way any record reaches a
250
+ * dependency row.
250
251
  */
251
252
  export function pairs(record: SuppressionRecord, finding: Finding): boolean {
252
253
  if (record.code !== finding.code) {
@@ -265,7 +266,10 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
265
266
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
266
267
  return true;
267
268
  }
268
- return record.symbol === finding.symbol.ref && record.path === finding.position.path;
269
+ return (
270
+ (record.symbol === finding.symbol.ref || record.symbol === finding.symbol.holder) &&
271
+ record.path === finding.position.path
272
+ );
269
273
  }
270
274
 
271
275
  /**