@cplieger/deadset-ts 4.0.1 → 5.1.0

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 (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
package/src/manifest.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * The entry points a package manifest names, which are roots because something
3
3
  * outside the analyzed program reaches them: a consumer importing the package, or a
4
- * shell running its command.
4
+ * shell running its command or one of its scripts.
5
5
  *
6
- * Only the target's own manifest is read. A manifest below the target root belongs
7
- * to another package, whose entry points are that package's roots and not this
8
- * target's.
6
+ * One call reads one manifest, the one in the directory it is given.
9
7
  */
10
8
 
11
9
  import type { Host } from "./host.ts";
@@ -22,7 +20,9 @@ export type EntryRole =
22
20
  /** A consumer importing the package, which is a caller the analysis cannot see. */
23
21
  | "import"
24
22
  /** A shell running the package's command, which is a caller that exists. */
25
- | "run";
23
+ | "run"
24
+ /** A token of one of the package's scripts, which the package manager runs. */
25
+ | "script";
26
26
 
27
27
  /** One entry point the manifest names. */
28
28
  export interface ManifestEntry {
@@ -59,6 +59,24 @@ function isTarget(value: string, requireDot: boolean): boolean {
59
59
  return !requireDot || value.startsWith("./");
60
60
  }
61
61
 
62
+ /** Where a script's command line splits into tokens: white space and the shell's operators. */
63
+ const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
64
+
65
+ /** A token written inside one pair of quotes. */
66
+ const QUOTED = /^(["'])(.*)\1$/su;
67
+
68
+ /**
69
+ * Every token one script's command line holds, each with one pair of enclosing quotes
70
+ * removed. A token holding a wildcard is a pattern rather than a path, and names no
71
+ * entry point.
72
+ */
73
+ export function scriptTokens(command: string): readonly string[] {
74
+ return command
75
+ .split(SCRIPT_SEPARATORS)
76
+ .map((token) => token.replace(QUOTED, "$2"))
77
+ .filter((token) => token !== "" && !token.includes("*"));
78
+ }
79
+
62
80
  /** One member's spelling below another: `exports` then `exports["."]`. */
63
81
  function below(member: string, key: string): string {
64
82
  return `${member}[${JSON.stringify(key)}]`;
@@ -67,12 +85,9 @@ function below(member: string, key: string): string {
67
85
  /**
68
86
  * Every target one `exports` value carries, at the member each is written under.
69
87
  *
70
- * The value is a string, an object whose keys are subpaths or conditions, or an
71
- * array of alternatives, nested to any depth, and a `null` blocks a subpath rather
72
- * than naming a target. Every level is walked because a condition names a different
73
- * file of the same package and each is an entry point of its own; a key's meaning
74
- * is not read, because a subpath and a condition both lead to a target and the
75
- * walk needs no more than that.
88
+ * The value nests strings, subpath or condition objects and alternative arrays to
89
+ * any depth; a `null` blocks a subpath. Every level is walked, because each
90
+ * condition names its own entry file, and a key's meaning is never read.
76
91
  */
77
92
  function exportTargets(value: unknown, member: string, found: ManifestEntry[]): void {
78
93
  if (typeof value === "string") {
@@ -116,11 +131,12 @@ function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | u
116
131
  return isRecord(value) ? value : undefined;
117
132
  }
118
133
 
119
- /** What the target's own manifest declares about where the package is entered. */
134
+ /** What one manifest declares about where its package is entered. */
120
135
  export interface Manifest {
121
136
  /**
122
137
  * Every entry point the manifest names, in the order the members are read: `main`,
123
- * `module`, `types`, then each `bin` command, then every target of `exports`. Each
138
+ * `module`, `types`, then each `bin` command, then every target of `exports`, then
139
+ * every token of each script, which names a file only where one is at its path. Each
124
140
  * path is absolute.
125
141
  */
126
142
  readonly entries: readonly ManifestEntry[];
@@ -134,7 +150,7 @@ export interface Manifest {
134
150
  }
135
151
 
136
152
  /**
137
- * What the target's own manifest declares about where the package is entered.
153
+ * What the manifest in `targetRoot` declares about where its package is entered.
138
154
  *
139
155
  * A target is taken as written. It is not resolved to a source file here, because
140
156
  * which file a target names is a fact about the compiler configuration that emits
@@ -167,6 +183,23 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
167
183
 
168
184
  exportTargets(manifest["exports"], "exports", found);
169
185
 
186
+ const scripts = manifest["scripts"];
187
+ if (isRecord(scripts)) {
188
+ for (const [name, command] of Object.entries(scripts)) {
189
+ if (typeof command !== "string") {
190
+ continue;
191
+ }
192
+ for (const token of new Set(scriptTokens(command))) {
193
+ found.push({
194
+ member: below("scripts", name),
195
+ path: token,
196
+ role: "script",
197
+ published: false,
198
+ });
199
+ }
200
+ }
201
+ }
202
+
170
203
  return {
171
204
  entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
172
205
  declaresExports: manifest["exports"] !== undefined,
package/src/matrix.ts CHANGED
@@ -21,6 +21,14 @@ export interface Configured {
21
21
  readonly roots: readonly Root[];
22
22
  /** The paths the reference pass classified as test files. */
23
23
  readonly testFiles: readonly string[];
24
+ /** The paths of the test-support files, which only test code imports. */
25
+ readonly supportFiles?: readonly string[] | undefined;
26
+ /**
27
+ * The paths of the workspace-package files the configuration holds but does not
28
+ * compile itself. Their declarations count in its graph and its verdict on them is no
29
+ * finding.
30
+ */
31
+ readonly referenceOnly?: readonly string[] | undefined;
24
32
  }
25
33
 
26
34
  /** The run's configurations, indexed for the sweep. */
@@ -30,9 +38,10 @@ export interface Matrix {
30
38
  /** Per configuration, in the same order, the graph of what that configuration holds. */
31
39
  readonly graphs: readonly Graph[];
32
40
  /**
33
- * The graph over every configuration at once: every declaration any configuration
34
- * holds, once per position and in site order, and every reference any configuration
35
- * makes, a reference several configurations make at one position counted once.
41
+ * The graph over every configuration at once: every declaration a configuration
42
+ * holds in a file it does not hold only for its references, once per position and in
43
+ * site order, and every reference any configuration makes, a reference several
44
+ * configurations make at one position counted once.
36
45
  */
37
46
  readonly union: Graph;
38
47
  /**
@@ -88,31 +97,30 @@ export function referenceKey(reference: Reference): string {
88
97
 
89
98
  /**
90
99
  * Indexes what the passes answered for each configuration of one run, in the run's order.
91
- *
92
- * A declaration is keyed across configurations by its identifier, which is its rendered
93
- * position and so is the same in every configuration that compiles its file; the fields
94
- * of the first configuration that holds it are the ones kept. A declaration one
95
- * configuration alone holds is in the union with that one configuration, so it is
96
- * judged where it exists rather than skipped.
97
- *
98
- * One reference written at one position of a file several configurations compile is seen
99
- * once per configuration and is one reference, so the union holds it once; two references
100
- * one configuration makes at one position are two.
100
+ * A declaration is keyed by its rendered position, with the fields of the first
101
+ * configuration that judges it; one in a file every holder holds only for its references
102
+ * is judged by none and is not in the union. One reference at one position of a file
103
+ * several configurations compile is one reference; two one configuration makes are two.
101
104
  */
102
105
  export function matrixOf(per: readonly Configured[]): Matrix {
103
106
  const symbols: InventorySymbol[] = [];
104
107
  const holders = new Map<string, string[]>();
105
108
  const references: Reference[] = [];
106
109
  const firstSeen = new Map<string, number>();
110
+ const judged = new Set<string>();
107
111
  per.forEach((one, index) => {
112
+ const referenceOnly = new Set(one.referenceOnly);
108
113
  for (const symbol of one.symbols) {
109
114
  const held = holders.get(symbol.id);
110
115
  if (held === undefined) {
111
116
  holders.set(symbol.id, [one.configuration]);
112
- symbols.push(symbol);
113
117
  } else if (!held.includes(one.configuration)) {
114
118
  held.push(one.configuration);
115
119
  }
120
+ if (!referenceOnly.has(symbol.position.path) && !judged.has(symbol.id)) {
121
+ judged.add(symbol.id);
122
+ symbols.push(symbol);
123
+ }
116
124
  }
117
125
  for (const reference of one.references) {
118
126
  const key = referenceKey(reference);
@@ -129,12 +137,15 @@ export function matrixOf(per: readonly Configured[]): Matrix {
129
137
 
130
138
  return {
131
139
  configurations: per.map((one) => one.configuration),
132
- graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
140
+ graphs: per.map((one) =>
141
+ graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles),
142
+ ),
133
143
  union: graphOf(
134
144
  symbols,
135
145
  references,
136
146
  per.flatMap((one) => one.roots),
137
147
  [...new Set(per.flatMap((one) => one.testFiles))],
148
+ [...new Set(per.flatMap((one) => one.supportFiles ?? []))],
138
149
  ),
139
150
  heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
140
151
  };
@@ -197,6 +208,23 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
197
208
  };
198
209
  }
199
210
 
211
+ /**
212
+ * The file declarations at `paths`, below the target root, that one sweep holds live
213
+ * under both relations, the rule a judged declaration is live by.
214
+ */
215
+ export function liveFilesAt(
216
+ matrix: Matrix,
217
+ result: SweepResult,
218
+ paths: ReadonlySet<string>,
219
+ ): readonly InventorySymbol[] {
220
+ return matrix.union.symbols.filter(
221
+ (symbol) =>
222
+ symbol.kind === "file" &&
223
+ paths.has(symbol.position.path) &&
224
+ (result.liveUnder.get(symbol.id)?.length ?? 0) === RELATIONS.length,
225
+ );
226
+ }
227
+
200
228
  /** The relations in the order a declaration's entry lists them. */
201
229
  const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
202
230
 
package/src/partial.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { NotBuilt } from "./discover.ts";
2
+ import { relativePath } from "./paths.ts";
2
3
  import type { UnansweredQuestion } from "./query.ts";
3
4
 
4
5
  /** What a run read only in part. */
@@ -7,6 +8,8 @@ interface Partial {
7
8
  readonly notBuilt: readonly NotBuilt[];
8
9
  /** Every question the checker could not answer, each once. */
9
10
  readonly unanswered: readonly UnansweredQuestion[];
11
+ /** The target root, absolute, which every configuration file is named below. */
12
+ readonly targetRoot: string;
10
13
  }
11
14
 
12
15
  /** A count with its noun, so a line reads for one as well as for several. */
@@ -29,7 +32,9 @@ export function partialNotes(partial: Partial): readonly string[] {
29
32
  if (partial.unanswered.length > 0) {
30
33
  const byProject = new Map<string, number>();
31
34
  for (const question of partial.unanswered) {
32
- byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
35
+ const configFile =
36
+ relativePath(partial.targetRoot, question.configFile) ?? question.configFile;
37
+ byProject.set(configFile, (byProject.get(configFile) ?? 0) + 1);
33
38
  }
34
39
  const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
35
40
  notes.push(
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The `packages` list of a `pnpm-workspace.yaml`. The document is read as far as that
3
+ * one key needs and no further: a top-level `packages` key holding a block sequence or
4
+ * a one-line flow sequence of plain or quoted strings. Any other value of the key is
5
+ * refused rather than guessed at, because a list read wrong names the wrong packages.
6
+ */
7
+
8
+ /** What the document says about `packages`. */
9
+ export type PnpmPackages =
10
+ | { readonly kind: "listed"; readonly patterns: readonly string[] }
11
+ /** The document declares no `packages` key. */
12
+ | { readonly kind: "absent" }
13
+ /** The key holds something this reader does not read, with what it holds. */
14
+ | { readonly kind: "refused"; readonly reason: string };
15
+
16
+ /** The characters a plain scalar may not start with. */
17
+ const INDICATORS = new Set(["[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "%", "@", "`", "#"]);
18
+
19
+ /** One line with its comment removed, a `#` inside quotes kept. */
20
+ function uncommented(line: string): string {
21
+ let quote: string | undefined;
22
+ for (let at = 0; at < line.length; at += 1) {
23
+ const character = line[at];
24
+ if (quote !== undefined) {
25
+ if (character === quote) {
26
+ quote = undefined;
27
+ }
28
+ continue;
29
+ }
30
+ if (character === "'" || character === '"') {
31
+ quote = character;
32
+ } else if (character === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
33
+ return line.slice(0, at).trimEnd();
34
+ }
35
+ }
36
+ return line.trimEnd();
37
+ }
38
+
39
+ /** One scalar as written, or `undefined` where it is not a string this reader reads. */
40
+ function scalarOf(text: string): string | undefined {
41
+ const value = text.trim();
42
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
43
+ const inner = value.slice(1, -1);
44
+ return /'(?!')/u.test(inner.replaceAll("''", "")) ? undefined : inner.replaceAll("''", "'");
45
+ }
46
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
47
+ const inner = value.slice(1, -1);
48
+ return inner.includes("\\") || inner.includes('"') ? undefined : inner;
49
+ }
50
+ if (
51
+ value === "" ||
52
+ INDICATORS.has(value[0] ?? "") ||
53
+ value.startsWith("'") ||
54
+ value.startsWith('"')
55
+ ) {
56
+ return undefined;
57
+ }
58
+ if (value.startsWith("- ") || value.startsWith("? ") || /:(?:\s|$)/u.test(value)) {
59
+ return undefined;
60
+ }
61
+ return value;
62
+ }
63
+
64
+ /** The entries of a one-line flow sequence, or `undefined` where one does not read. */
65
+ function flowOf(text: string): readonly string[] | undefined {
66
+ const inner = text.slice(1, -1).trim();
67
+ if (inner === "") {
68
+ return [];
69
+ }
70
+ const entries = inner.split(",");
71
+ if (entries.length > 1 && entries.at(-1)?.trim() === "") {
72
+ entries.pop();
73
+ }
74
+ const found: string[] = [];
75
+ for (const entry of entries) {
76
+ const value = scalarOf(entry);
77
+ if (value === undefined) {
78
+ return undefined;
79
+ }
80
+ found.push(value);
81
+ }
82
+ return found;
83
+ }
84
+
85
+ const KEY = /^(?:packages|'packages'|"packages")\s*:(?:\s(.*))?$/u;
86
+
87
+ /** The `packages` list one `pnpm-workspace.yaml` text declares. */
88
+ export function pnpmPackages(text: string): PnpmPackages {
89
+ const lines = text.split(/\r?\n/u).map(uncommented);
90
+ const keys = lines.flatMap((line, at) => (KEY.test(line) ? [at] : []));
91
+ const [at] = keys;
92
+ if (at === undefined) {
93
+ return { kind: "absent" };
94
+ }
95
+ if (keys.length > 1) {
96
+ return { kind: "refused", reason: "the packages key is declared more than once" };
97
+ }
98
+ const rest = (KEY.exec(lines[at] ?? "")?.[1] ?? "").trim();
99
+ if (rest !== "") {
100
+ const listed = rest.startsWith("[") && rest.endsWith("]") ? flowOf(rest) : undefined;
101
+ return listed === undefined
102
+ ? {
103
+ kind: "refused",
104
+ reason: `packages holds ${JSON.stringify(rest)}, not a sequence of strings`,
105
+ }
106
+ : { kind: "listed", patterns: listed };
107
+ }
108
+ const patterns: string[] = [];
109
+ for (const line of lines.slice(at + 1)) {
110
+ if (line.trim() === "") {
111
+ continue;
112
+ }
113
+ const item = /^(\s*)-(?:\s+(.*))?$/u.exec(line);
114
+ if (item === null) {
115
+ if (/^\s/u.test(line)) {
116
+ return {
117
+ kind: "refused",
118
+ reason: `packages holds ${JSON.stringify(line.trim())}, not a sequence item`,
119
+ };
120
+ }
121
+ break;
122
+ }
123
+ const value = scalarOf(item[2] ?? "");
124
+ if (value === undefined) {
125
+ return {
126
+ kind: "refused",
127
+ reason: `the packages item ${JSON.stringify(line.trim())} is not a plain or quoted string`,
128
+ };
129
+ }
130
+ patterns.push(value);
131
+ }
132
+ return patterns.length === 0
133
+ ? { kind: "refused", reason: "packages holds no sequence" }
134
+ : { kind: "listed", patterns };
135
+ }
package/src/position.ts CHANGED
@@ -34,18 +34,39 @@ export class PositionError extends Error {
34
34
  }
35
35
  }
36
36
 
37
+ /** Whether the compiler read one file through a content mapper, which makes it a component file. */
38
+ export function isComponentFile(file: SourceFile): boolean {
39
+ return file.contentMapper !== undefined;
40
+ }
41
+
42
+ /**
43
+ * The length of the file as it is written. A component file's module holds the file's
44
+ * text at its own offsets and appends what it needs past its end.
45
+ */
46
+ export function writtenLength(file: SourceFile): number {
47
+ return isComponentFile(file) ? file.originalText.length : file.text.length;
48
+ }
49
+
37
50
  /**
38
51
  * The position of one offset of one file, rendered against the target root.
39
52
  *
40
53
  * `offset` is a UTF-16 code unit offset into the file's text, which is what every
41
- * node of the tree carries.
54
+ * node of the tree carries. An offset a component file's module appends renders where
55
+ * the module's span map places it in the file.
42
56
  */
43
57
  export function renderPosition(file: SourceFile, root: string, offset: number): Position {
44
58
  const path = relativePath(root, file.fileName);
45
59
  if (path === undefined) {
46
60
  throw new PositionError(file.fileName, root);
47
61
  }
48
- const { line, character } = file.getLineAndCharacterOfPosition(offset);
62
+ const written =
63
+ isComponentFile(file) && offset >= file.originalText.length
64
+ ? Math.min(
65
+ file.spanMap?.virtualToOriginalPosition(offset).position ?? offset,
66
+ file.originalText.length,
67
+ )
68
+ : offset;
69
+ const { line, character } = file.getLineAndCharacterOfPosition(written);
49
70
  return { path, line: line + 1, column: character + 1 };
50
71
  }
51
72
 
package/src/print.ts CHANGED
@@ -75,6 +75,8 @@ export function printConfig(config: Config, provenance: Provenance): string {
75
75
  ts: {
76
76
  test_files: config.ts.testFiles,
77
77
  entry_files: config.ts.entryFiles,
78
+ component_extensions: config.ts.componentExtensions,
79
+ disabled_conventions: config.ts.disabledConventions,
78
80
  injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
79
81
  lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
80
82
  components: entry.components.map(declarationOf),
package/src/query.ts CHANGED
@@ -127,6 +127,32 @@ interface Assignability {
127
127
  readonly target: Type;
128
128
  }
129
129
 
130
+ /**
131
+ * Each type's properties, asked of the checker once per type for every pass that shares
132
+ * the tables: the client keeps a type's properties once fetched, so a second pass asking
133
+ * for the same type makes no request, and only the first ask is counted as one.
134
+ */
135
+ export interface PropertyTables {
136
+ /** One type's properties, and whether this call was the one that asked for them. */
137
+ of(type: Type): { readonly properties: Answer<readonly TSSymbol[]>; readonly asked: boolean };
138
+ }
139
+
140
+ /** The property tables over one project's questions. */
141
+ export function propertyTables(queries: Pick<Queries, "propertiesOf">): PropertyTables {
142
+ const held = new Map<number, Answer<readonly TSSymbol[]>>();
143
+ return {
144
+ of: (type) => {
145
+ const known = held.get(type.id);
146
+ if (known !== undefined) {
147
+ return { properties: known, asked: false };
148
+ }
149
+ const properties = queries.propertiesOf(type);
150
+ held.set(type.id, properties);
151
+ return { properties, asked: true };
152
+ },
153
+ };
154
+ }
155
+
130
156
  /** The questions one project's analysis asks, each guarded. */
131
157
  export interface Queries {
132
158
  /** The symbol each node resolves to. */