@cplieger/deadset-ts 4.0.0 → 5.0.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 (117) hide show
  1. package/README.md +68 -50
  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 +230 -37
  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 +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +11 -4
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +6 -3
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +4 -2
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +395 -71
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +11 -7
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +6 -3
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +4 -2
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
@@ -71,6 +71,8 @@ export const SCHEMA_ROOT = {
71
71
  members: {
72
72
  test_files: leaf,
73
73
  entry_files: leaf,
74
+ component_extensions: leaf,
75
+ disabled_conventions: leaf,
74
76
  injection_registrations: declarations,
75
77
  lifecycle_contracts: {
76
78
  kind: "list",
@@ -218,10 +220,10 @@ export function unimplementedKey(label, path) {
218
220
  const nearest = nearestKey(path);
219
221
  let hint = "";
220
222
  if (nearest === path) {
221
- hint = `; ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
223
+ hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
222
224
  }
223
225
  else if (nearest !== "") {
224
- hint = `; the nearest implemented key is ${JSON.stringify(nearest)}`;
226
+ hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
225
227
  }
226
228
  return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented${hint}`);
227
229
  }
@@ -1,5 +1,7 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
+ import { componentLayer, mapperManifest } from "./component-layer.js";
2
3
  import { DiscoveryError } from "./discover.js";
4
+ import { joinPath } from "./paths.js";
3
5
  import { queriesOf, UnansweredLog, } from "./query.js";
4
6
  /**
5
7
  * Opens the compiler client the analysis reads through.
@@ -9,11 +11,55 @@ import { queriesOf, UnansweredLog, } from "./query.js";
9
11
  * second call has nothing left to do.
10
12
  */
11
13
  export function openEngine(options) {
12
- const api = new API({ collectTiming: options.collectTiming });
14
+ const { components } = options;
15
+ let mapper;
16
+ if (components !== undefined && components.extensions.length > 0) {
17
+ const made = components.host.temporaryDirectory();
18
+ try {
19
+ components.host.writeDocument(joinPath(made.path, "package.json"), [
20
+ mapperManifest(components.host.componentMapperCommand()),
21
+ ]);
22
+ }
23
+ catch (error) {
24
+ made.remove();
25
+ throw error;
26
+ }
27
+ mapper = made;
28
+ }
29
+ const api = new API({
30
+ collectTiming: options.collectTiming,
31
+ ...(mapper === undefined ? {} : { runExternalCode: true }),
32
+ });
13
33
  let closed = false;
14
34
  return {
15
35
  parseConfigFile: (file) => api.parseConfigFile(file),
16
- createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
36
+ parseConfigJson: (json, configFile) => api.parseJsonConfigFileContent(json, { configFileName: configFile }),
37
+ createSnapshot: (openProjects, createPrograms, configurations = new Map()) => api.createSnapshot({
38
+ openProjects: [...openProjects],
39
+ ...(createPrograms === undefined ? {} : { createPrograms: [...createPrograms] }),
40
+ ...(components === undefined
41
+ ? configurations.size === 0
42
+ ? {}
43
+ : { fileSystem: { kind: "layer", files: Object.fromEntries(configurations) } }
44
+ : {
45
+ fileSystem: componentLayer(components.host, openProjects, mapper === undefined
46
+ ? undefined
47
+ : { extensions: components.extensions, packageDirectory: mapper.path }, configurations),
48
+ }),
49
+ }),
50
+ readsComponents: mapper !== undefined,
51
+ createModuleResolver: (options, entries) => api.createModuleResolver(options, {
52
+ moduleResolutions: { fallback: "resolve", entries: [...entries] },
53
+ }),
54
+ parseSourceFile: (fileName, text) => {
55
+ const retained = api.createSourceFile(fileName, text);
56
+ try {
57
+ return retained.sourceFile;
58
+ }
59
+ finally {
60
+ retained.dispose();
61
+ }
62
+ },
17
63
  batch: (questions) => api.batch(...questions),
18
64
  ask: (_accessor, _locations, question) => question(),
19
65
  getTimingInfo: () => api.getTimingInfo(),
@@ -22,10 +68,35 @@ export function openEngine(options) {
22
68
  return;
23
69
  }
24
70
  closed = true;
25
- api.close();
71
+ try {
72
+ api.close();
73
+ }
74
+ finally {
75
+ mapper?.remove();
76
+ }
26
77
  },
27
78
  };
28
79
  }
80
+ /**
81
+ * The source files of one program that `own` admits, in the program's order, none of
82
+ * them one of the compiler's default libraries.
83
+ */
84
+ function filesOwned(program, own) {
85
+ return (program
86
+ .getSourceFileNames()
87
+ // The metadata decides which files belong to the target, and it is read by
88
+ // name. Fetching each file first and filtering afterwards costs one round
89
+ // trip per library file the answer then discards, and a program holds far
90
+ // more library files than target files.
91
+ .filter((name) => {
92
+ const metadata = program.getSourceFileMetadata(name);
93
+ return (metadata !== undefined &&
94
+ !metadata.isDefaultLibrary &&
95
+ own(name, metadata.isFromExternalLibrary));
96
+ })
97
+ .map((name) => program.getSourceFile(name))
98
+ .filter((file) => file !== undefined));
99
+ }
29
100
  function viewOf(project, configFile, engine, log) {
30
101
  const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
31
102
  // The snapshot is never updated, so a project's own file set is the same answer
@@ -35,23 +106,13 @@ function viewOf(project, configFile, engine, log) {
35
106
  let ownFiles;
36
107
  let ownKeys;
37
108
  const ownSourceFiles = () => {
38
- ownFiles ??= project.program
39
- .getSourceFileNames()
40
- // The metadata decides which files belong to the target, and it is read by
41
- // name. Fetching each file first and filtering afterwards costs one round
42
- // trip per library file the answer then discards, and a program holds far
43
- // more library files than target files.
44
- .filter((name) => {
45
- const metadata = project.program.getSourceFileMetadata(name);
46
- return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
47
- })
48
- .map((name) => project.program.getSourceFile(name))
49
- .filter((file) => file !== undefined);
109
+ ownFiles ??= filesOwned(project.program, (_name, fromExternalLibrary) => !fromExternalLibrary);
50
110
  return ownFiles;
51
111
  };
52
112
  return {
53
113
  configFile,
54
114
  program: project.program,
115
+ rootFiles: project.parsedCommandLine.fileNames,
55
116
  queries,
56
117
  ownSourceFiles,
57
118
  ownPaths: () => {
@@ -101,6 +162,26 @@ export function narrowedTo(project, keep) {
101
162
  },
102
163
  };
103
164
  }
165
+ /**
166
+ * The same project, its own source files the ones `own` admits among every file its
167
+ * program holds, given whether the compiler reached the file as a library's.
168
+ */
169
+ export function ownedAs(project, own) {
170
+ let owned;
171
+ let keys;
172
+ const ownSourceFiles = () => {
173
+ owned ??= filesOwned(project.program, own);
174
+ return owned;
175
+ };
176
+ return {
177
+ ...project,
178
+ ownSourceFiles,
179
+ ownPaths: () => {
180
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
181
+ return keys;
182
+ },
183
+ };
184
+ }
104
185
  /** The three diagnostic sets one project carries, read before any analysis. */
105
186
  export function diagnosticsOf(project) {
106
187
  return {
@@ -119,28 +200,54 @@ export function diagnosticsOf(project) {
119
200
  };
120
201
  }
121
202
  /**
122
- * Runs `visit` over every project the compiler configurations name, inside one
123
- * snapshot of one client, in the order they are named, and answers in that order.
124
- *
125
- * The snapshot is opened once with every configuration, so each becomes a project
126
- * with its own program and checker; it is never updated, because the analysis
127
- * reads and the tree does not change under it. The order the snapshot lists its
128
- * projects in is the compiler's own, so each named configuration is looked up in it
129
- * instead. A configuration the snapshot opened no project for ends the run under
130
- * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
131
- * the last project has been visited, and the client is released after it, whether the
132
- * visit completed or threw.
203
+ * The projects one snapshot holds for the configurations named, by configuration file. A
204
+ * project opened from a written configuration is the project of the configuration
205
+ * `written` maps it to.
133
206
  */
134
- export function runSession(engine, configFiles, visit, unopened = "refuse") {
207
+ function projectsOf(snapshot, built, written) {
208
+ const opened = new Map();
209
+ for (const project of snapshot.getProjects()) {
210
+ if (project.configFileName !== undefined) {
211
+ opened.set(written.get(project.configFileName) ?? project.configFileName, project);
212
+ }
213
+ }
214
+ (snapshot.operation.createdPrograms ?? []).forEach((program, at) => {
215
+ const project = snapshot.getProject(program.id);
216
+ const configFile = built[at];
217
+ if (project !== undefined && configFile !== undefined) {
218
+ opened.set(configFile, project);
219
+ }
220
+ });
221
+ return opened;
222
+ }
223
+ /**
224
+ * Runs `visit` over every project the compiler configurations name, inside one never updated
225
+ * snapshot of one client, and answers in their order. A configuration `programs` describes is
226
+ * built as that program, or opened from the configuration it writes, instead of as a project.
227
+ * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
228
+ * under `"record"`. The snapshot is disposed after the last visit and the client released
229
+ * after it, whether the visit completed or threw.
230
+ */
231
+ export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
135
232
  try {
136
- const snapshot = engine.createSnapshot(configFiles);
137
- try {
138
- const opened = new Map();
139
- for (const project of snapshot.getProjects()) {
140
- if (project.configFileName !== undefined) {
141
- opened.set(project.configFileName, project);
142
- }
233
+ const built = [];
234
+ const params = [];
235
+ const written = new Map();
236
+ const texts = new Map();
237
+ for (const configFile of configFiles) {
238
+ const program = programs.get(configFile);
239
+ if (program?.kind === "program") {
240
+ built.push(configFile);
241
+ params.push(program.params);
242
+ }
243
+ else if (program?.kind === "configuration") {
244
+ written.set(program.file, configFile);
245
+ texts.set(program.file, program.text);
143
246
  }
247
+ }
248
+ const snapshot = engine.createSnapshot([...configFiles.filter((configFile) => !programs.has(configFile)), ...written.keys()], params.length === 0 ? undefined : params, texts);
249
+ try {
250
+ const opened = projectsOf(snapshot, built, written);
144
251
  const log = new UnansweredLog();
145
252
  const projects = [];
146
253
  const missing = [];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A setup failure: a file or a component the analysis needs and the project does not
3
+ * provide. Each one names what is missing and the fix, on one line of the error stream
4
+ * that starts with its class.
5
+ */
6
+ /** The failure as the one line of the error stream that names it. */
7
+ export function setupLine(failure) {
8
+ return `setup failure: ${failure.setupClass}: ${failure.detail}`;
9
+ }
10
+ /**
11
+ * A run that met a setup failure where none may be dropped: in a configuration the
12
+ * configuration document or the scope declares, in a declared consumer, or in every
13
+ * configuration discovery derived. It ends the run with the failure code and no finding
14
+ * list.
15
+ */
16
+ export class SetupError extends Error {
17
+ failures;
18
+ constructor(failures) {
19
+ super(failures.map(setupLine).join("\n"));
20
+ this.name = "SetupError";
21
+ this.failures = failures;
22
+ }
23
+ }
@@ -3,45 +3,10 @@
3
3
  * in a document or a string names one: as the file itself, or as a file the
4
4
  * compiler emits from it.
5
5
  */
6
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
7
- /** The source extensions the compiler emits each output extension from. */
8
- const EMITTED = [
9
- { emitted: ".d.ts", sources: [".ts", ".tsx"] },
10
- { emitted: ".d.mts", sources: [".mts"] },
11
- { emitted: ".d.cts", sources: [".cts"] },
12
- { emitted: ".js", sources: [".ts", ".tsx"] },
13
- { emitted: ".jsx", sources: [".tsx"] },
14
- { emitted: ".mjs", sources: [".mts"] },
15
- { emitted: ".cjs", sources: [".cts"] },
16
- ];
6
+ import { commonDirectory, emittedFrom } from "./emit-map.js";
7
+ import { dirnamePath, relativePath, resolvePath } from "./paths.js";
17
8
  /** The one character a subpath pattern of a manifest's `exports` holds. */
18
9
  const SUBPATH_WILDCARD = "*";
19
- /** One path with its extension replaced, where it carries the one named. */
20
- function reExtended(path, emitted, source) {
21
- return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
22
- }
23
- /**
24
- * The longest directory every path shares, which is the directory the compiler
25
- * emits from where the configuration declares no root directory.
26
- */
27
- function commonDirectory(paths) {
28
- const first = paths[0];
29
- if (first === undefined) {
30
- return "";
31
- }
32
- let common = dirnamePath(first).split("/");
33
- for (const path of paths.slice(1)) {
34
- const segments = dirnamePath(path).split("/");
35
- let shared = 0;
36
- while (shared < common.length &&
37
- shared < segments.length &&
38
- common[shared] === segments[shared]) {
39
- shared += 1;
40
- }
41
- common = common.slice(0, shared);
42
- }
43
- return common.join("/");
44
- }
45
10
  /**
46
11
  * Whether one path is what a subpath pattern stands for: the text before the
47
12
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -77,31 +42,9 @@ export function sourceFilesOf(project, targetRoot) {
77
42
  const outDir = at(options.outDir);
78
43
  const declarationDir = at(options.declarationDir);
79
44
  const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
80
- /** One path read below the directory the compiler emits from, where it is below the one it emits to. */
81
- const fromEmit = (path, emitted) => {
82
- const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
83
- if (emitDir === undefined) {
84
- return undefined;
85
- }
86
- const below = relativePath(emitDir, path);
87
- return below === undefined ? undefined : joinPath(sourceDir, below);
88
- };
45
+ const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
89
46
  /** The source paths one written path may stand for, the path itself first. */
90
- const candidates = (path) => {
91
- const found = [path];
92
- for (const { emitted, sources } of EMITTED) {
93
- const mapped = fromEmit(path, emitted);
94
- for (const source of sources) {
95
- for (const from of [path, mapped]) {
96
- const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
97
- if (candidate !== undefined) {
98
- found.push(candidate);
99
- }
100
- }
101
- }
102
- }
103
- return found;
104
- };
47
+ const candidates = (path) => [path, ...emittedFrom(path, layout)];
105
48
  return {
106
49
  byPath,
107
50
  byName,
package/dist/src/sweep.js CHANGED
@@ -153,13 +153,15 @@ function reachable(graph, held, mode) {
153
153
  function admitTestsOfDeadCode(graph, dead, held) {
154
154
  const admitted = graph.symbols.map(() => false);
155
155
  graph.symbols.forEach((_symbol, at) => {
156
- if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
156
+ const testCode = graph.test[at] === true || graph.support[at] === true;
157
+ if (!testCode || graph.subject[at] !== true || held[at] === true) {
157
158
  return;
158
159
  }
159
160
  let targets = 0;
160
161
  let live = 0;
161
162
  for (const edge of graph.out[at] ?? []) {
162
- if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
163
+ const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
164
+ if (target || graph.subject[edge.to] !== true) {
163
165
  continue;
164
166
  }
165
167
  targets += 1;
@@ -11,9 +11,11 @@
11
11
  * instance members of classes and the members of interfaces and object types, on any
12
12
  * type, because nothing ties the name to a type.
13
13
  */
14
+ import { readComponent } from "./component-files.js";
14
15
  import { ConfigError } from "./config.js";
15
16
  import { memberNames } from "./member-names.js";
16
- import { isAbsolutePath, joinPath, normalizePath } from "./paths.js";
17
+ import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.js";
18
+ import { isComponentFile } from "./position.js";
17
19
  /** The kinds of declaration an action can name. */
18
20
  const READ_BY_A_TEMPLATE = new Set([
19
21
  "method",
@@ -21,6 +23,11 @@ const READ_BY_A_TEMPLATE = new Set([
21
23
  "interface-method",
22
24
  "type-member",
23
25
  ]);
26
+ /**
27
+ * The kinds of declaration a component file's markup can name. Its expressions are the
28
+ * language's own, so a static member and an enum member are reachable by name too.
29
+ */
30
+ const READ_BY_MARKUP = new Set([...READ_BY_A_TEMPLATE, "enum-member"]);
24
31
  /** One identifier, as the grammar of the analyzed language spells one. */
25
32
  const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
26
33
  /** Two strings ordered bytewise. */
@@ -133,6 +140,14 @@ function namesOf(text, delimiters) {
133
140
  from = close + delimiters.right.length;
134
141
  }
135
142
  }
143
+ /** Every identifier the markup of one component file writes in an action. */
144
+ function markupNamesOf(text) {
145
+ return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
146
+ name,
147
+ offset: one.start + offset,
148
+ action: one.action,
149
+ })));
150
+ }
136
151
  /** Where one offset of a template is, its column counted in UTF-16 code units. */
137
152
  function positionIn(file, offset) {
138
153
  const before = file.text.slice(0, offset);
@@ -146,25 +161,42 @@ function positionIn(file, offset) {
146
161
  /**
147
162
  * The template-field detector: every instance member of a class and every member of
148
163
  * an interface or an object type whose name an action of a configured template
149
- * writes, recorded at the identifier.
164
+ * writes, and every member of those and of an enum whose name a component file's
165
+ * markup writes, recorded at the identifier and held while that file is live.
150
166
  */
151
167
  export function templateField(input) {
152
168
  const { templates } = input;
153
- if (templates.files.length === 0) {
169
+ const components = input.project.ownSourceFiles().flatMap((file) => {
170
+ const path = isComponentFile(file) ? relativePath(input.targetRoot, file.fileName) : undefined;
171
+ return path === undefined ? [] : [{ path, text: file.originalText }];
172
+ });
173
+ if (templates.files.length === 0 && components.length === 0) {
154
174
  return [];
155
175
  }
156
176
  const members = memberNames(input.project.ownSourceFiles(), input.held);
157
177
  const found = [];
158
- for (const file of templates.files) {
159
- for (const named of namesOf(file.text, templates.delimiters)) {
178
+ const scanned = [
179
+ ...templates.files.map((file) => ({
180
+ file,
181
+ names: namesOf(file.text, templates.delimiters),
182
+ markup: false,
183
+ })),
184
+ ...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
185
+ ];
186
+ for (const { file, names, markup } of scanned) {
187
+ for (const named of names) {
160
188
  for (const member of members.get(named.name) ?? []) {
161
- if (!READ_BY_A_TEMPLATE.has(member.kind) || member.static) {
189
+ const readable = markup
190
+ ? READ_BY_MARKUP.has(member.kind)
191
+ : READ_BY_A_TEMPLATE.has(member.kind) && !member.static;
192
+ if (!readable) {
162
193
  continue;
163
194
  }
164
195
  found.push({
165
196
  id: member.id,
166
197
  detail: `named by ${named.action}`,
167
198
  site: positionIn(file, named.offset),
199
+ ...(markup ? { whileLive: file.path } : {}),
168
200
  });
169
201
  }
170
202
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Test-support code: a file that is not a test file by name and that only test code
3
+ * imports. At least one test file reaches it, directly or through other such files, and
4
+ * nothing else does, so it is judged with the tests: what it references is referenced by
5
+ * test code, and a declaration of it is live while test code references it.
6
+ */
7
+ /** The root kind that never takes a file out of test-support code: the skip only withholds. */
8
+ const SKIP_ROOT = "type-error";
9
+ /**
10
+ * The files of one project that are test-support code, below the target root. A file
11
+ * holding a root is not one, because a root has a caller outside the import graph: a
12
+ * runtime, a tool, a configuration, or a consumer of a library's published API.
13
+ * `testFiles` are the files the test-file rules classified.
14
+ */
15
+ export function testSupportFiles(symbols, references, roots, testFiles) {
16
+ const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
17
+ const users = new Map();
18
+ for (const reference of references) {
19
+ const used = fileOf.get(reference.to);
20
+ const from = reference.position.path;
21
+ if (reference.consumer !== undefined || used === undefined || used === from) {
22
+ continue;
23
+ }
24
+ const held = users.get(used);
25
+ if (held === undefined) {
26
+ users.set(used, new Set([from]));
27
+ }
28
+ else {
29
+ held.add(from);
30
+ }
31
+ }
32
+ const tests = new Set(testFiles);
33
+ const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
34
+ const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
35
+ const support = new Set();
36
+ for (let changed = true; changed;) {
37
+ changed = false;
38
+ for (const path of candidates) {
39
+ if (support.has(path)) {
40
+ continue;
41
+ }
42
+ const from = users.get(path) ?? new Set();
43
+ if ([...from].every((one) => tests.has(one) || support.has(one))) {
44
+ support.add(path);
45
+ changed = true;
46
+ }
47
+ }
48
+ }
49
+ return support;
50
+ }
51
+ /** The references with every one a test-support file makes counted as a test reference. */
52
+ export function supportReferences(references, support) {
53
+ if (support.size === 0) {
54
+ return references;
55
+ }
56
+ return references.map((reference) => !reference.test && support.has(reference.position.path)
57
+ ? { ...reference, test: true }
58
+ : reference);
59
+ }