@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
@@ -0,0 +1,340 @@
1
+ /**
2
+ * What the errors a project's own files carry do to the run. An import of a module the
3
+ * project expects and nothing provides is a setup failure. Any other error in an own file
4
+ * is a type error: the function, method or file-level statement holding it is skipped,
5
+ * which only ever withholds a finding. An error the compiler places in no file is read as
6
+ * a project that cannot be analyzed.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isNoSubstitutionTemplateLiteral,
12
+ isPropertyAccessExpression,
13
+ isStringLiteral,
14
+ type Node,
15
+ type PropertyAccessExpression,
16
+ type SourceFile,
17
+ } from "@typescript/native/unstable/ast";
18
+ import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
19
+ import { packageOfSpecifier } from "./dependencies.ts";
20
+ import type { LocatedDiagnostic } from "./discover.ts";
21
+ import type { Host } from "./host.ts";
22
+ import { nodeKey, type Inventory } from "./inventory.ts";
23
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
24
+ import { isAnswered } from "./query.ts";
25
+ import type { TypeErrorSkip } from "./report.ts";
26
+ import type { Engine, ProjectView } from "./session.ts";
27
+ import type { SetupFailure } from "./setup-failure.ts";
28
+
29
+ /** The compiler's codes for an import whose module it cannot find. */
30
+ const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
31
+
32
+ const MANIFEST = "package.json";
33
+ const MANIFEST_SECTIONS = [
34
+ "dependencies",
35
+ "devDependencies",
36
+ "peerDependencies",
37
+ "optionalDependencies",
38
+ ] as const;
39
+
40
+ /** The lines of one file a skip withholds every finding on. */
41
+ export interface SkippedUnit {
42
+ /** The file, below the target root. */
43
+ readonly path: string;
44
+ readonly fromLine: number;
45
+ readonly toLine: number;
46
+ }
47
+
48
+ /** Whether a finding's position falls inside a skipped unit. */
49
+ export function insideSkipped(
50
+ skipped: readonly SkippedUnit[],
51
+ position: { readonly path: string; readonly line: number },
52
+ ): boolean {
53
+ return skipped.some(
54
+ (unit) =>
55
+ unit.path === position.path && unit.fromLine <= position.line && position.line <= unit.toLine,
56
+ );
57
+ }
58
+
59
+ /** One type error of an own file, with the unit it skips. */
60
+ interface Skip {
61
+ readonly record: TypeErrorSkip;
62
+ readonly file: SourceFile;
63
+ readonly unit: Node;
64
+ }
65
+
66
+ /** What the errors of one project's own files are. */
67
+ interface ProjectErrorReading {
68
+ /** The imports of a module the project expects that nothing provides, one failure each. */
69
+ readonly missing: readonly SetupFailure[];
70
+ /** The type errors, each with the unit it skips. */
71
+ readonly skips: readonly Skip[];
72
+ /** The errors placed in no file, which leave the project unanalyzable. */
73
+ readonly failing: readonly LocatedDiagnostic[];
74
+ }
75
+
76
+ /** Where one project reads what its imports may name. */
77
+ interface ErrorContext {
78
+ readonly host: Host;
79
+ /** The target root, absolute. */
80
+ readonly targetRoot: string;
81
+ /** The keys of the project configuration's `paths` mapping. */
82
+ readonly paths: readonly string[];
83
+ }
84
+
85
+ /** The string literal holding one offset, innermost first. */
86
+ function literalAt(file: SourceFile, offset: number): string | undefined {
87
+ let found: string | undefined;
88
+ const visit = (node: Node): void => {
89
+ if (offset < node.pos || offset >= node.end) {
90
+ return;
91
+ }
92
+ if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
93
+ found = node.text;
94
+ }
95
+ node.forEachChild(visit);
96
+ };
97
+ file.forEachChild(visit);
98
+ return found;
99
+ }
100
+
101
+ /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
102
+ function mappedBy(key: string, specifier: string): boolean {
103
+ const star = key.indexOf("*");
104
+ if (star < 0) {
105
+ return key === specifier;
106
+ }
107
+ const prefix = key.slice(0, star);
108
+ const suffix = key.slice(star + 1);
109
+ return (
110
+ specifier.length >= prefix.length + suffix.length &&
111
+ specifier.startsWith(prefix) &&
112
+ specifier.endsWith(suffix)
113
+ );
114
+ }
115
+
116
+ /** Whether a relative import's path names something on disk, as written or with any extension. */
117
+ function provided(host: Host, path: string): boolean {
118
+ if (host.kindOf(path) !== "absent") {
119
+ return true;
120
+ }
121
+ const dir = dirnamePath(path);
122
+ const stem = `${path.slice(dir.length + 1)}.`;
123
+ try {
124
+ return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
131
+ function declaringManifest(
132
+ host: Host,
133
+ dir: string,
134
+ targetRoot: string,
135
+ name: string,
136
+ ): string | undefined {
137
+ for (let at = dir; ; at = dirnamePath(at)) {
138
+ const path = joinPath(at, MANIFEST);
139
+ let manifest: unknown;
140
+ try {
141
+ manifest = JSON.parse(host.readFile(path)) as unknown;
142
+ } catch {
143
+ manifest = undefined;
144
+ }
145
+ if (typeof manifest === "object" && manifest !== null) {
146
+ const sections = manifest as Record<string, unknown>;
147
+ for (const section of MANIFEST_SECTIONS) {
148
+ const declared = sections[section];
149
+ if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
150
+ return path;
151
+ }
152
+ }
153
+ }
154
+ if (at === targetRoot || relativePath(targetRoot, at) === undefined || dirnamePath(at) === at) {
155
+ return undefined;
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Why one import that the compiler cannot resolve names a module the project expects,
162
+ * with the fix, or undefined where the module is one the project does not expect.
163
+ */
164
+ function expectedModule(
165
+ context: ErrorContext,
166
+ importer: string,
167
+ specifier: string,
168
+ ): string | undefined {
169
+ const { host, targetRoot } = context;
170
+ const fix = "run the generator or the build that writes it";
171
+ if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
172
+ return provided(host, resolvePath(dirnamePath(importer), specifier))
173
+ ? undefined
174
+ : `which names no file of the target: ${fix}`;
175
+ }
176
+ if (specifier.startsWith("#")) {
177
+ return `which the manifest's imports map to no file: ${fix}`;
178
+ }
179
+ if (context.paths.some((key) => mappedBy(key, specifier))) {
180
+ return `which the compiler configuration's paths map to no file: ${fix}`;
181
+ }
182
+ const name = packageOfSpecifier(specifier);
183
+ const manifest =
184
+ name === undefined
185
+ ? undefined
186
+ : declaringManifest(host, dirnamePath(importer), targetRoot, name);
187
+ if (manifest === undefined) {
188
+ return undefined;
189
+ }
190
+ const where = relativePath(targetRoot, manifest) ?? manifest;
191
+ return `a dependency ${where} declares that is not installed: run the package manager's install`;
192
+ }
193
+
194
+ /**
195
+ * The unit a type error at one offset skips: the class member holding it where a
196
+ * top-level class does, else the top-level statement.
197
+ */
198
+ function unitAt(file: SourceFile, offset: number): Node | undefined {
199
+ const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
200
+ if (statement === undefined || !isClassDeclaration(statement)) {
201
+ return statement;
202
+ }
203
+ return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
204
+ }
205
+
206
+ /** The compiler's message on one line, the target root spelled relative to itself. */
207
+ function messageOf(text: string, targetRoot: string): string {
208
+ const line = text.split(`${targetRoot}/`).join("").split(/\s+/u).join(" ").trim();
209
+ return line === "" ? "a type error" : line;
210
+ }
211
+
212
+ /**
213
+ * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
214
+ * is no error here, and one in a file that is not the project's own is that file's
215
+ * package's to answer for.
216
+ */
217
+ export function readErrors<Brand>(
218
+ project: ProjectView<Brand>,
219
+ errors: readonly LocatedDiagnostic[],
220
+ context: ErrorContext,
221
+ ): ProjectErrorReading {
222
+ const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
223
+ const missing: SetupFailure[] = [];
224
+ const skips: Skip[] = [];
225
+ const failing: LocatedDiagnostic[] = [];
226
+ for (const located of errors) {
227
+ const { diagnostic, at } = located;
228
+ if (diagnostic.fileName === undefined) {
229
+ failing.push(located);
230
+ continue;
231
+ }
232
+ const file = files.get(diagnostic.fileName);
233
+ if (file === undefined || diagnostic.reportsUnnecessary === true) {
234
+ continue;
235
+ }
236
+ const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
237
+ const line = at?.line ?? 1;
238
+ const specifier = MODULE_NOT_FOUND.has(diagnostic.code)
239
+ ? literalAt(file, diagnostic.pos)
240
+ : undefined;
241
+ const expected =
242
+ specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
243
+ if (specifier !== undefined && expected !== undefined) {
244
+ missing.push({
245
+ setupClass: "missing-module",
246
+ detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(specifier)}, ${expected}`,
247
+ });
248
+ continue;
249
+ }
250
+ const unit = unitAt(file, diagnostic.pos);
251
+ if (unit !== undefined) {
252
+ skips.push({
253
+ record: { path, line, message: messageOf(diagnostic.text, context.targetRoot) },
254
+ file,
255
+ unit,
256
+ });
257
+ }
258
+ }
259
+ return { missing, skips, failing };
260
+ }
261
+
262
+ /** The lines each skip withholds, below the target root. */
263
+ export function skippedUnits(skips: readonly Skip[], targetRoot: string): readonly SkippedUnit[] {
264
+ return skips.map(({ file, unit }) => ({
265
+ path: relativePath(targetRoot, file.fileName) ?? file.fileName,
266
+ fromLine: file.getLineAndCharacterOfPosition(unit.getStart()).line + 1,
267
+ toLine: file.getLineAndCharacterOfPosition(unit.end).line + 1,
268
+ }));
269
+ }
270
+
271
+ /**
272
+ * The declarations the skips keep live: every one a skipped unit declares, and every
273
+ * member of the type of each operand on which the unit selects a member the checker
274
+ * could not resolve, because the selection could have named any of them.
275
+ */
276
+ export function skippedRoots<Brand>(
277
+ project: ProjectView<Brand>,
278
+ held: Inventory,
279
+ skips: readonly Skip[],
280
+ ): readonly string[] {
281
+ const kept = new Set<string>();
282
+ const selections: PropertyAccessExpression[] = [];
283
+ for (const { file, unit } of skips) {
284
+ const visit = (node: Node): void => {
285
+ const id = held.declarations.get(nodeKey(file, node));
286
+ if (id !== undefined) {
287
+ kept.add(id);
288
+ }
289
+ if (isPropertyAccessExpression(node)) {
290
+ selections.push(node);
291
+ }
292
+ node.forEachChild(visit);
293
+ };
294
+ visit(unit);
295
+ }
296
+ if (selections.length === 0) {
297
+ return [...kept];
298
+ }
299
+ const names = project.symbolsAt(selections.map((node) => project.handle(node.name)));
300
+ // A name the checker did not answer for may have named any member, as an unresolved one may.
301
+ const operands = selections.flatMap((node, at) => {
302
+ const name = names[at];
303
+ return name === undefined || !isAnswered(name) ? [node.expression] : [];
304
+ });
305
+ const own = project.ownPaths();
306
+ const keepMembers = (type: Type): void => {
307
+ const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
308
+ for (const part of isAnswered(parts) ? parts : []) {
309
+ const properties = project.queries.propertiesOf(part);
310
+ for (const property of isAnswered(properties) ? properties : ([] as TSSymbol[])) {
311
+ for (const handle of property.declarations) {
312
+ const node = own.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
313
+ const id =
314
+ node === undefined
315
+ ? undefined
316
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
317
+ if (id !== undefined) {
318
+ kept.add(id);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ };
324
+ for (const type of project.queries.typesAt(operands)) {
325
+ if (type !== undefined && isAnswered(type)) {
326
+ keepMembers(type);
327
+ }
328
+ }
329
+ return [...kept];
330
+ }
331
+
332
+ /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
333
+ export function pathsOf(engine: Engine, configFile: string): readonly string[] {
334
+ try {
335
+ const paths: unknown = engine.parseConfigFile(configFile).options.paths;
336
+ return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
337
+ } catch {
338
+ return [];
339
+ }
340
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The ambient globals a declaration file declares as an alias of a module's export:
3
+ * a variable whose type is `typeof import("<module>")["<name>"]` or
4
+ * `typeof import("<module>").<name>`. A use of the global is a use of the export,
5
+ * and the type query that says so is no use of its own.
6
+ */
7
+
8
+ import {
9
+ isIdentifier,
10
+ isImportTypeNode,
11
+ isIndexedAccessTypeNode,
12
+ isLiteralTypeNode,
13
+ isModuleBlock,
14
+ isModuleDeclaration,
15
+ isStringLiteral,
16
+ isVariableStatement,
17
+ SyntaxKind,
18
+ type Node,
19
+ type SourceFile,
20
+ } from "@typescript/native/unstable/ast";
21
+
22
+ /** One global that aliases a module's export. */
23
+ interface TypeQueryAlias {
24
+ /** The variable declaration of the global. */
25
+ readonly declaration: Node;
26
+ /** The type query, which names the export and is no reference. */
27
+ readonly type: Node;
28
+ /** The node whose symbol is the export: the index's string, or the qualifier's name. */
29
+ readonly exported: Node;
30
+ /** The name the module exports the declaration under. */
31
+ readonly name: string;
32
+ /**
33
+ * The `declare global` block the global is written in and every module declaration
34
+ * around it, outermost first; none at a script's top level.
35
+ */
36
+ readonly containers: readonly Node[];
37
+ }
38
+
39
+ /** Whether one type is `typeof import("<string>")`, with no type argument. */
40
+ function typeOfImport(node: Node): boolean {
41
+ if (!isImportTypeNode(node) || !node.isTypeOf || node.typeArguments !== undefined) {
42
+ return false;
43
+ }
44
+ const argument = node.argument;
45
+ return isLiteralTypeNode(argument) && isStringLiteral(argument.literal);
46
+ }
47
+
48
+ /** The node naming the export one alias type selects, or undefined where the type is no alias. */
49
+ function exportedBy(type: Node): Node | undefined {
50
+ if (isIndexedAccessTypeNode(type)) {
51
+ const index = type.indexType;
52
+ const object = type.objectType;
53
+ if (
54
+ typeOfImport(object) &&
55
+ isImportTypeNode(object) &&
56
+ object.qualifier === undefined &&
57
+ isLiteralTypeNode(index) &&
58
+ isStringLiteral(index.literal)
59
+ ) {
60
+ return index.literal;
61
+ }
62
+ return undefined;
63
+ }
64
+ if (typeOfImport(type) && isImportTypeNode(type) && type.qualifier !== undefined) {
65
+ return isIdentifier(type.qualifier) ? type.qualifier : undefined;
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /** Whether one module declaration is a `declare global` block. */
71
+ function isGlobalBlock(node: Node): boolean {
72
+ return (
73
+ isModuleDeclaration(node) &&
74
+ node.keyword !== SyntaxKind.NamespaceKeyword &&
75
+ isIdentifier(node.name) &&
76
+ node.name.text === "global"
77
+ );
78
+ }
79
+
80
+ /** The aliases one list of statements declares as globals. */
81
+ function aliasesIn(statements: readonly Node[], containers: readonly Node[]): TypeQueryAlias[] {
82
+ const found: TypeQueryAlias[] = [];
83
+ for (const statement of statements) {
84
+ if (!isVariableStatement(statement)) {
85
+ continue;
86
+ }
87
+ for (const declaration of statement.declarationList.declarations) {
88
+ const type = declaration.type;
89
+ const exported = type === undefined ? undefined : exportedBy(type);
90
+ if (
91
+ type === undefined ||
92
+ exported === undefined ||
93
+ !isIdentifier(declaration.name) ||
94
+ !(isIdentifier(exported) || isStringLiteral(exported))
95
+ ) {
96
+ continue;
97
+ }
98
+ found.push({ declaration, type, exported, name: exported.text, containers });
99
+ }
100
+ }
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * Every global one file declares as an alias of a module's export: at the top level of
106
+ * a declaration file that is no module, and in a `declare global` block written at the
107
+ * top level of a declaration file or of an ambient module it declares.
108
+ */
109
+ export function typeQueryAliases(file: SourceFile): readonly TypeQueryAlias[] {
110
+ if (!file.isDeclarationFile) {
111
+ return [];
112
+ }
113
+ const found: TypeQueryAlias[] =
114
+ file.externalModuleIndicator === undefined ? aliasesIn(file.statements, []) : [];
115
+ const blocks = (statements: readonly Node[], outer: readonly Node[]): void => {
116
+ for (const statement of statements) {
117
+ if (!isModuleDeclaration(statement) || statement.body === undefined) {
118
+ continue;
119
+ }
120
+ const body = statement.body;
121
+ if (!isModuleBlock(body)) {
122
+ continue;
123
+ }
124
+ if (isGlobalBlock(statement)) {
125
+ found.push(...aliasesIn(body.statements, [...outer, statement]));
126
+ } else if (!isIdentifier(statement.name)) {
127
+ blocks(body.statements, [statement]);
128
+ }
129
+ }
130
+ };
131
+ blocks(file.statements, []);
132
+ return found;
133
+ }
@@ -1,4 +1,11 @@
1
- import { runAnalysis, runBaselineWrite, type RunAnalysis, type RunProject } from "../analysis.ts";
1
+ import {
2
+ runAnalysis,
3
+ runBaselineWrite,
4
+ unansweredCounts,
5
+ type ComponentWarning,
6
+ type RunAnalysis,
7
+ type RunProject,
8
+ } from "../analysis.ts";
2
9
  import { ConfigError, type Format } from "../config.ts";
3
10
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
4
11
  import { moduleIdentity } from "../consumers.ts";
@@ -220,17 +227,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
220
227
  let recorded: readonly Recorded[];
221
228
  let verdict: number;
222
229
  let partial: readonly string[];
230
+ let warnings: readonly ComponentWarning[];
231
+ let unread: readonly string[];
223
232
  try {
224
233
  const root = runRelative(invoked, targetRoot);
225
234
  const mode = { production: true };
226
235
  let analysis: RunAnalysis;
227
236
  if (documents.baseline === undefined) {
228
- analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
237
+ analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
229
238
  recorded = [];
230
239
  } else {
231
240
  const reason = rowReason(analyzerProvenance(version));
232
241
  const write = runBaselineWrite(
233
- openClient(false),
242
+ openClient(config),
234
243
  host,
235
244
  scoped,
236
245
  config,
@@ -242,7 +251,16 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
242
251
  recorded = write.rows;
243
252
  }
244
253
  const { result, run } = analysis;
245
- partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
254
+ warnings = run.componentWarnings;
255
+ unread = run.componentsUnread.map(
256
+ ({ configFile, reason }) =>
257
+ `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`,
258
+ );
259
+ partial = partialNotes({
260
+ notBuilt: run.notBuilt,
261
+ unanswered: run.unanswered,
262
+ targetRoot: resolvePath(invoked, targetRoot),
263
+ });
246
264
  const built = buildReport({
247
265
  contractVersion: CONTRACT_VERSION,
248
266
  version,
@@ -258,6 +276,9 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
258
276
  unavailable: [],
259
277
  result,
260
278
  testFileRules: run.testFileRules,
279
+ typeErrorSkips: run.typeErrorSkips,
280
+ unanswered: unansweredCounts(analysis),
281
+ conventionsApplied: run.conventionsApplied,
261
282
  });
262
283
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
263
284
  verdict = verdictOf(result, config);
@@ -293,6 +314,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
293
314
  for (const note of partial) {
294
315
  err.write(`deadset-ts: ${note}\n`);
295
316
  }
317
+ for (const line of unread) {
318
+ err.write(`deadset-ts: ${line}\n`);
319
+ }
320
+ for (const warning of warnings) {
321
+ err.write(
322
+ `deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`,
323
+ );
324
+ }
325
+ for (const skip of report.type_error_skips) {
326
+ err.write(
327
+ `deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`,
328
+ );
329
+ }
296
330
  if (report.totals.pending > 0) {
297
331
  err.write(
298
332
  `deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
@@ -7,6 +7,7 @@ import { OUTSIDE, swept as judged } from "../graph.ts";
7
7
  import type { InventorySymbol } from "../inventory.ts";
8
8
  import { referenceKey, type Configured } from "../matrix.ts";
9
9
  import { partialNotes } from "../partial.ts";
10
+ import { resolvePath } from "../paths.ts";
10
11
  import { positionKey } from "../position.ts";
11
12
  import type { Reference } from "../references.ts";
12
13
  import { resolve } from "../resolve.ts";
@@ -365,13 +366,15 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
365
366
  export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
366
367
  const named = namedSymbol(option);
367
368
  const { config, provenance } = resolve(inputs());
368
- const analysis = runAnalysis(openClient(false), host, scope(), config, provenance, {
369
+ const scoped = scope();
370
+ const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, {
369
371
  production: true,
370
372
  });
371
373
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
372
374
  const notes = partialNotes({
373
375
  notBuilt: analysis.run.notBuilt,
374
376
  unanswered: analysis.run.unanswered,
377
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
375
378
  });
376
379
  if (subject === undefined) {
377
380
  err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
@@ -1,80 +1,28 @@
1
- import {
2
- discoverProjects,
3
- DiscoveryError,
4
- isGuess,
5
- projectErrors,
6
- renderDiagnostic,
7
- targetRelative,
8
- type NotBuilt,
9
- } from "../discover.ts";
1
+ import { runProjects } from "../analysis.ts";
10
2
  import { partialNotes } from "../partial.ts";
11
- import { relativePath, resolvePath } from "../paths.ts";
12
- import { resolveMatrix } from "../resolve.ts";
13
- import { diagnosticsOf, runSession } from "../session.ts";
14
- import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
3
+ import { resolvePath } from "../paths.ts";
4
+ import { resolveUnkinded } from "../resolve.ts";
5
+ import { EXIT_CLEAN, type Verb } from "./verb.ts";
15
6
 
16
7
  /**
17
- * Writes the name of every project one run analyzes, one per line, having read each
18
- * project's diagnostics first: the identifier the build matrix declares for it, or,
19
- * where discovery derived the project, its configuration file's path below the
20
- * target root. A derived project that does not load is not one the run analyzes, so it
21
- * is named on the error stream instead, as the analysis drops it.
8
+ * Writes the name of every project one run analyzes, one per line: the identifier the
9
+ * build matrix declares for it, or, where discovery derived the project, its
10
+ * configuration file's path below the target root. The projects are read as the
11
+ * analysis reads them, so a project the analysis drops is named on the error stream
12
+ * instead, and one that fails the analysis fails this command the same way.
22
13
  */
23
14
  export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
24
- const matrix = resolveMatrix(inputs());
15
+ const config = resolveUnkinded(inputs());
25
16
  const scoped = scope();
26
- const engine = openClient(false);
27
- const discovered = discoverProjects(engine, host, scoped, matrix);
28
- const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
29
- const root = resolvePath(host.workingDirectory(), scoped.target.path);
30
- const notBuilt: NotBuilt[] = [...discovered.notBuilt];
31
- const failures: string[] = [];
32
- const session = runSession(
33
- engine,
34
- discovered.configFiles,
35
- (project) => {
36
- const id = ids.get(project.configFile) ?? project.configFile;
37
- const errors = projectErrors(diagnosticsOf(project));
38
- const [unloaded] = errors.load;
39
- if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
40
- const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
41
- notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
42
- return undefined;
43
- }
44
- failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
45
- return id;
46
- },
47
- discovered.derived ? "record" : "refuse",
48
- );
49
- for (const configFile of session.unopened) {
50
- if (!isGuess(discovered, configFile)) {
51
- throw new DiscoveryError(`${configFile} opened no project`, []);
52
- }
53
- notBuilt.push({
54
- id: ids.get(configFile) ?? configFile,
55
- configFile,
56
- error: "the compiler opened no project for the configuration",
57
- });
58
- }
59
- if (failures.length > 0) {
60
- for (const line of failures) {
61
- err.write(`${line}\n`);
62
- }
63
- err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
64
- return EXIT_FAILURE;
65
- }
66
- const projects = session.projects.filter((id): id is string => id !== undefined);
67
- if (projects.length === 0 && notBuilt.length > 0) {
68
- for (const note of partialNotes({ notBuilt, unanswered: [] })) {
69
- err.write(`deadset-ts: ${note}\n`);
70
- }
71
- err.write("deadset-ts: no configuration discovery derived could be built\n");
72
- return EXIT_FAILURE;
73
- }
74
- for (const id of projects) {
17
+ const answer = runProjects(openClient(config), host, scoped, config);
18
+ for (const id of answer.configurations) {
75
19
  out.write(`${id}\n`);
76
20
  }
77
- for (const note of partialNotes({ notBuilt, unanswered: [] })) {
21
+ for (const note of partialNotes({
22
+ notBuilt: answer.notBuilt,
23
+ unanswered: answer.unanswered,
24
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
25
+ })) {
78
26
  err.write(`deadset-ts: ${note}\n`);
79
27
  }
80
28
  return EXIT_CLEAN;