@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
@@ -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
+ }
@@ -0,0 +1,252 @@
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
+ import { isClassDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
9
+ import { packageOfSpecifier } from "./dependencies.js";
10
+ import { nodeKey } from "./inventory.js";
11
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
12
+ import { isAnswered } from "./query.js";
13
+ /** The compiler's codes for an import whose module it cannot find. */
14
+ const MODULE_NOT_FOUND = new Set([2307, 2792]);
15
+ const MANIFEST = "package.json";
16
+ const MANIFEST_SECTIONS = [
17
+ "dependencies",
18
+ "devDependencies",
19
+ "peerDependencies",
20
+ "optionalDependencies",
21
+ ];
22
+ /** Whether a finding's position falls inside a skipped unit. */
23
+ export function insideSkipped(skipped, position) {
24
+ return skipped.some((unit) => unit.path === position.path && unit.fromLine <= position.line && position.line <= unit.toLine);
25
+ }
26
+ /** The string literal holding one offset, innermost first. */
27
+ function literalAt(file, offset) {
28
+ let found;
29
+ const visit = (node) => {
30
+ if (offset < node.pos || offset >= node.end) {
31
+ return;
32
+ }
33
+ if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
34
+ found = node.text;
35
+ }
36
+ node.forEachChild(visit);
37
+ };
38
+ file.forEachChild(visit);
39
+ return found;
40
+ }
41
+ /** Whether one `paths` key matches one specifier: exactly, or around its one `*`. */
42
+ function mappedBy(key, specifier) {
43
+ const star = key.indexOf("*");
44
+ if (star < 0) {
45
+ return key === specifier;
46
+ }
47
+ const prefix = key.slice(0, star);
48
+ const suffix = key.slice(star + 1);
49
+ return (specifier.length >= prefix.length + suffix.length &&
50
+ specifier.startsWith(prefix) &&
51
+ specifier.endsWith(suffix));
52
+ }
53
+ /** Whether a relative import's path names something on disk, as written or with any extension. */
54
+ function provided(host, path) {
55
+ if (host.kindOf(path) !== "absent") {
56
+ return true;
57
+ }
58
+ const dir = dirnamePath(path);
59
+ const stem = `${path.slice(dir.length + 1)}.`;
60
+ try {
61
+ return host.readDirectory(dir).some((entry) => entry.name.startsWith(stem));
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
68
+ function declaringManifest(host, dir, targetRoot, name) {
69
+ for (let at = dir;; at = dirnamePath(at)) {
70
+ const path = joinPath(at, MANIFEST);
71
+ let manifest;
72
+ try {
73
+ manifest = JSON.parse(host.readFile(path));
74
+ }
75
+ catch {
76
+ manifest = undefined;
77
+ }
78
+ if (typeof manifest === "object" && manifest !== null) {
79
+ const sections = manifest;
80
+ for (const section of MANIFEST_SECTIONS) {
81
+ const declared = sections[section];
82
+ if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
83
+ return path;
84
+ }
85
+ }
86
+ }
87
+ if (at === targetRoot || relativePath(targetRoot, at) === undefined || dirnamePath(at) === at) {
88
+ return undefined;
89
+ }
90
+ }
91
+ }
92
+ /**
93
+ * Why one import that the compiler cannot resolve names a module the project expects,
94
+ * with the fix, or undefined where the module is one the project does not expect.
95
+ */
96
+ function expectedModule(context, importer, specifier) {
97
+ const { host, targetRoot } = context;
98
+ const fix = "run the generator or the build that writes it";
99
+ if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
100
+ return provided(host, resolvePath(dirnamePath(importer), specifier))
101
+ ? undefined
102
+ : `which names no file of the target: ${fix}`;
103
+ }
104
+ if (specifier.startsWith("#")) {
105
+ return `which the manifest's imports map to no file: ${fix}`;
106
+ }
107
+ if (context.paths.some((key) => mappedBy(key, specifier))) {
108
+ return `which the compiler configuration's paths map to no file: ${fix}`;
109
+ }
110
+ const name = packageOfSpecifier(specifier);
111
+ const manifest = name === undefined
112
+ ? undefined
113
+ : declaringManifest(host, dirnamePath(importer), targetRoot, name);
114
+ if (manifest === undefined) {
115
+ return undefined;
116
+ }
117
+ const where = relativePath(targetRoot, manifest) ?? manifest;
118
+ return `a dependency ${where} declares that is not installed: run the package manager's install`;
119
+ }
120
+ /**
121
+ * The unit a type error at one offset skips: the class member holding it where a
122
+ * top-level class does, else the top-level statement.
123
+ */
124
+ function unitAt(file, offset) {
125
+ const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
126
+ if (statement === undefined || !isClassDeclaration(statement)) {
127
+ return statement;
128
+ }
129
+ return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
130
+ }
131
+ /** The compiler's message on one line, the target root spelled relative to itself. */
132
+ function messageOf(text, targetRoot) {
133
+ const line = text.split(`${targetRoot}/`).join("").split(/\s+/u).join(" ").trim();
134
+ return line === "" ? "a type error" : line;
135
+ }
136
+ /**
137
+ * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
138
+ * is no error here, and one in a file that is not the project's own is that file's
139
+ * package's to answer for.
140
+ */
141
+ export function readErrors(project, errors, context) {
142
+ const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
143
+ const missing = [];
144
+ const skips = [];
145
+ const failing = [];
146
+ for (const located of errors) {
147
+ const { diagnostic, at } = located;
148
+ if (diagnostic.fileName === undefined) {
149
+ failing.push(located);
150
+ continue;
151
+ }
152
+ const file = files.get(diagnostic.fileName);
153
+ if (file === undefined || diagnostic.reportsUnnecessary === true) {
154
+ continue;
155
+ }
156
+ const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
157
+ const line = at?.line ?? 1;
158
+ const specifier = MODULE_NOT_FOUND.has(diagnostic.code)
159
+ ? literalAt(file, diagnostic.pos)
160
+ : undefined;
161
+ const expected = specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
162
+ if (specifier !== undefined && expected !== undefined) {
163
+ missing.push({
164
+ setupClass: "missing-module",
165
+ detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(specifier)}, ${expected}`,
166
+ });
167
+ continue;
168
+ }
169
+ const unit = unitAt(file, diagnostic.pos);
170
+ if (unit !== undefined) {
171
+ skips.push({
172
+ record: { path, line, message: messageOf(diagnostic.text, context.targetRoot) },
173
+ file,
174
+ unit,
175
+ });
176
+ }
177
+ }
178
+ return { missing, skips, failing };
179
+ }
180
+ /** The lines each skip withholds, below the target root. */
181
+ export function skippedUnits(skips, targetRoot) {
182
+ return skips.map(({ file, unit }) => ({
183
+ path: relativePath(targetRoot, file.fileName) ?? file.fileName,
184
+ fromLine: file.getLineAndCharacterOfPosition(unit.getStart()).line + 1,
185
+ toLine: file.getLineAndCharacterOfPosition(unit.end).line + 1,
186
+ }));
187
+ }
188
+ /**
189
+ * The declarations the skips keep live: every one a skipped unit declares, and every
190
+ * member of the type of each operand on which the unit selects a member the checker
191
+ * could not resolve, because the selection could have named any of them.
192
+ */
193
+ export function skippedRoots(project, held, skips) {
194
+ const kept = new Set();
195
+ const selections = [];
196
+ for (const { file, unit } of skips) {
197
+ const visit = (node) => {
198
+ const id = held.declarations.get(nodeKey(file, node));
199
+ if (id !== undefined) {
200
+ kept.add(id);
201
+ }
202
+ if (isPropertyAccessExpression(node)) {
203
+ selections.push(node);
204
+ }
205
+ node.forEachChild(visit);
206
+ };
207
+ visit(unit);
208
+ }
209
+ if (selections.length === 0) {
210
+ return [...kept];
211
+ }
212
+ const names = project.symbolsAt(selections.map((node) => project.handle(node.name)));
213
+ // A name the checker did not answer for may have named any member, as an unresolved one may.
214
+ const operands = selections.flatMap((node, at) => {
215
+ const name = names[at];
216
+ return name === undefined || !isAnswered(name) ? [node.expression] : [];
217
+ });
218
+ const own = project.ownPaths();
219
+ const keepMembers = (type) => {
220
+ const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
221
+ for (const part of isAnswered(parts) ? parts : []) {
222
+ const properties = project.queries.propertiesOf(part);
223
+ for (const property of isAnswered(properties) ? properties : []) {
224
+ for (const handle of property.declarations) {
225
+ const node = own.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
226
+ const id = node === undefined
227
+ ? undefined
228
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
229
+ if (id !== undefined) {
230
+ kept.add(id);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ };
236
+ for (const type of project.queries.typesAt(operands)) {
237
+ if (type !== undefined && isAnswered(type)) {
238
+ keepMembers(type);
239
+ }
240
+ }
241
+ return [...kept];
242
+ }
243
+ /** The keys of one compiler configuration's `paths` mapping, none where it cannot be read. */
244
+ export function pathsOf(engine, configFile) {
245
+ try {
246
+ const paths = engine.parseConfigFile(configFile).options.paths;
247
+ return typeof paths === "object" && paths !== null ? Object.keys(paths) : [];
248
+ }
249
+ catch {
250
+ return [];
251
+ }
252
+ }
@@ -0,0 +1,92 @@
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
+ import { isIdentifier, isImportTypeNode, isIndexedAccessTypeNode, isLiteralTypeNode, isModuleBlock, isModuleDeclaration, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ /** Whether one type is `typeof import("<string>")`, with no type argument. */
9
+ function typeOfImport(node) {
10
+ if (!isImportTypeNode(node) || !node.isTypeOf || node.typeArguments !== undefined) {
11
+ return false;
12
+ }
13
+ const argument = node.argument;
14
+ return isLiteralTypeNode(argument) && isStringLiteral(argument.literal);
15
+ }
16
+ /** The node naming the export one alias type selects, or undefined where the type is no alias. */
17
+ function exportedBy(type) {
18
+ if (isIndexedAccessTypeNode(type)) {
19
+ const index = type.indexType;
20
+ const object = type.objectType;
21
+ if (typeOfImport(object) &&
22
+ isImportTypeNode(object) &&
23
+ object.qualifier === undefined &&
24
+ isLiteralTypeNode(index) &&
25
+ isStringLiteral(index.literal)) {
26
+ return index.literal;
27
+ }
28
+ return undefined;
29
+ }
30
+ if (typeOfImport(type) && isImportTypeNode(type) && type.qualifier !== undefined) {
31
+ return isIdentifier(type.qualifier) ? type.qualifier : undefined;
32
+ }
33
+ return undefined;
34
+ }
35
+ /** Whether one module declaration is a `declare global` block. */
36
+ function isGlobalBlock(node) {
37
+ return (isModuleDeclaration(node) &&
38
+ node.keyword !== SyntaxKind.NamespaceKeyword &&
39
+ isIdentifier(node.name) &&
40
+ node.name.text === "global");
41
+ }
42
+ /** The aliases one list of statements declares as globals. */
43
+ function aliasesIn(statements, containers) {
44
+ const found = [];
45
+ for (const statement of statements) {
46
+ if (!isVariableStatement(statement)) {
47
+ continue;
48
+ }
49
+ for (const declaration of statement.declarationList.declarations) {
50
+ const type = declaration.type;
51
+ const exported = type === undefined ? undefined : exportedBy(type);
52
+ if (type === undefined ||
53
+ exported === undefined ||
54
+ !isIdentifier(declaration.name) ||
55
+ !(isIdentifier(exported) || isStringLiteral(exported))) {
56
+ continue;
57
+ }
58
+ found.push({ declaration, type, exported, name: exported.text, containers });
59
+ }
60
+ }
61
+ return found;
62
+ }
63
+ /**
64
+ * Every global one file declares as an alias of a module's export: at the top level of
65
+ * a declaration file that is no module, and in a `declare global` block written at the
66
+ * top level of a declaration file or of an ambient module it declares.
67
+ */
68
+ export function typeQueryAliases(file) {
69
+ if (!file.isDeclarationFile) {
70
+ return [];
71
+ }
72
+ const found = file.externalModuleIndicator === undefined ? aliasesIn(file.statements, []) : [];
73
+ const blocks = (statements, outer) => {
74
+ for (const statement of statements) {
75
+ if (!isModuleDeclaration(statement) || statement.body === undefined) {
76
+ continue;
77
+ }
78
+ const body = statement.body;
79
+ if (!isModuleBlock(body)) {
80
+ continue;
81
+ }
82
+ if (isGlobalBlock(statement)) {
83
+ found.push(...aliasesIn(body.statements, [...outer, statement]));
84
+ }
85
+ else if (!isIdentifier(statement.name)) {
86
+ blocks(body.statements, [statement]);
87
+ }
88
+ }
89
+ };
90
+ blocks(file.statements, []);
91
+ return found;
92
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The values a program hands to a position the checker types from its context: the
3
+ * initializer of a declaration whose type is written, an assignment's right side, an
4
+ * argument, a returned value, an arrow function's expression body, an array element, an
5
+ * object property, and the operand of `as` or `satisfies`.
6
+ */
7
+ import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isIdentifier, isNewExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ /** The operators of an assignment whose right side is stored into the left side's type. */
9
+ const STORES = new Set([
10
+ SyntaxKind.EqualsToken,
11
+ SyntaxKind.QuestionQuestionEqualsToken,
12
+ SyntaxKind.BarBarEqualsToken,
13
+ SyntaxKind.AmpersandAmpersandEqualsToken,
14
+ ]);
15
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
16
+ function writesType(node) {
17
+ return node.type !== undefined;
18
+ }
19
+ /**
20
+ * The values one written expression hands on: the expression itself, or what it
21
+ * evaluates to through parentheses, a conditional and the operators whose result is
22
+ * one of their operands.
23
+ */
24
+ export function valuesOf(expression) {
25
+ if (isParenthesizedExpression(expression)) {
26
+ return valuesOf(expression.expression);
27
+ }
28
+ if (isConditionalExpression(expression)) {
29
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
30
+ }
31
+ if (isBinaryExpression(expression)) {
32
+ const operator = expression.operatorToken.kind;
33
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
34
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
35
+ }
36
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
37
+ return valuesOf(expression.right);
38
+ }
39
+ }
40
+ return [expression];
41
+ }
42
+ /** The expressions one node hands to a position typed by its context. */
43
+ export function handedOn(node) {
44
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
45
+ node.initializer !== undefined &&
46
+ writesType(node)) {
47
+ return [node.initializer];
48
+ }
49
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
50
+ return [node.right];
51
+ }
52
+ if (isCallExpression(node) || isNewExpression(node)) {
53
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
54
+ }
55
+ if (isReturnStatement(node)) {
56
+ return node.expression === undefined ? [] : [node.expression];
57
+ }
58
+ if (isArrowFunction(node)) {
59
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
60
+ }
61
+ if (isArrayLiteralExpression(node)) {
62
+ return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
63
+ }
64
+ if (isPropertyAssignment(node)) {
65
+ return [node.initializer];
66
+ }
67
+ if (isShorthandPropertyAssignment(node)) {
68
+ return isIdentifier(node.name) ? [node.name] : [];
69
+ }
70
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
71
+ return [node.expression];
72
+ }
73
+ return [];
74
+ }
@@ -1,4 +1,4 @@
1
- import { runAnalysis, runBaselineWrite } from "../analysis.js";
1
+ import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
2
2
  import { ConfigError } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
@@ -160,22 +160,30 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
160
160
  let recorded;
161
161
  let verdict;
162
162
  let partial;
163
+ let warnings;
164
+ let unread;
163
165
  try {
164
166
  const root = runRelative(invoked, targetRoot);
165
167
  const mode = { production: true };
166
168
  let analysis;
167
169
  if (documents.baseline === undefined) {
168
- analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
170
+ analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
169
171
  recorded = [];
170
172
  }
171
173
  else {
172
174
  const reason = rowReason(analyzerProvenance(version));
173
- const write = runBaselineWrite(openClient(false), host, scoped, config, provenance, mode, reason);
175
+ const write = runBaselineWrite(openClient(config), host, scoped, config, provenance, mode, reason);
174
176
  analysis = write.first;
175
177
  recorded = write.rows;
176
178
  }
177
179
  const { result, run } = analysis;
178
- partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
180
+ warnings = run.componentWarnings;
181
+ unread = run.componentsUnread.map(({ configFile, reason }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`);
182
+ partial = partialNotes({
183
+ notBuilt: run.notBuilt,
184
+ unanswered: run.unanswered,
185
+ targetRoot: resolvePath(invoked, targetRoot),
186
+ });
179
187
  const built = buildReport({
180
188
  contractVersion: CONTRACT_VERSION,
181
189
  version,
@@ -191,6 +199,9 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
191
199
  unavailable: [],
192
200
  result,
193
201
  testFileRules: run.testFileRules,
202
+ typeErrorSkips: run.typeErrorSkips,
203
+ unanswered: unansweredCounts(analysis),
204
+ conventionsApplied: run.conventionsApplied,
194
205
  });
195
206
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
196
207
  verdict = verdictOf(result, config);
@@ -226,6 +237,15 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
226
237
  for (const note of partial) {
227
238
  err.write(`deadset-ts: ${note}\n`);
228
239
  }
240
+ for (const line of unread) {
241
+ err.write(`deadset-ts: ${line}\n`);
242
+ }
243
+ for (const warning of warnings) {
244
+ err.write(`deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`);
245
+ }
246
+ for (const skip of report.type_error_skips) {
247
+ err.write(`deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`);
248
+ }
229
249
  if (report.totals.pending > 0) {
230
250
  err.write(`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`);
231
251
  }
@@ -4,6 +4,7 @@ import { ConfigError } from "../config.js";
4
4
  import { OUTSIDE, swept as judged } from "../graph.js";
5
5
  import { referenceKey } from "../matrix.js";
6
6
  import { partialNotes } from "../partial.js";
7
+ import { resolvePath } from "../paths.js";
7
8
  import { positionKey } from "../position.js";
8
9
  import { resolve } from "../resolve.js";
9
10
  import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
@@ -303,13 +304,15 @@ function explanation(analysis, subject, cascade) {
303
304
  export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
304
305
  const named = namedSymbol(option);
305
306
  const { config, provenance } = resolve(inputs());
306
- const analysis = runAnalysis(openClient(false), host, scope(), config, provenance, {
307
+ const scoped = scope();
308
+ const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, {
307
309
  production: true,
308
310
  });
309
311
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
310
312
  const notes = partialNotes({
311
313
  notBuilt: analysis.run.notBuilt,
312
314
  unanswered: analysis.run.unanswered,
315
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
313
316
  });
314
317
  if (subject === undefined) {
315
318
  err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);