@cplieger/deadset-ts 5.1.0 → 5.2.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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +79 -37
  3. package/dist/src/config.js +3 -0
  4. package/dist/src/configuration-files.js +24 -25
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/contextual-uses.js +46 -18
  7. package/dist/src/convention-rows.js +7 -0
  8. package/dist/src/conventions.js +97 -33
  9. package/dist/src/dependencies.js +8 -0
  10. package/dist/src/entry-point-gaps.js +10 -26
  11. package/dist/src/entry-points.js +4 -0
  12. package/dist/src/exempt.js +23 -8
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -11
  15. package/dist/src/findings/interfaces.js +2 -6
  16. package/dist/src/findings/intra-function.js +15 -3
  17. package/dist/src/findings/unused-declarations.js +5 -6
  18. package/dist/src/findings/visibility-narrowing.js +6 -1
  19. package/dist/src/generated-file.js +72 -12
  20. package/dist/src/graph.js +6 -5
  21. package/dist/src/html-entries.js +137 -0
  22. package/dist/src/inventory.js +13 -3
  23. package/dist/src/kinds.js +2 -2
  24. package/dist/src/manifest.js +34 -5
  25. package/dist/src/matrix.js +9 -3
  26. package/dist/src/namespace-objects.js +158 -0
  27. package/dist/src/outside-tests.js +139 -0
  28. package/dist/src/overrides.js +104 -0
  29. package/dist/src/paths.js +17 -0
  30. package/dist/src/references.js +150 -4
  31. package/dist/src/roots.js +172 -39
  32. package/dist/src/session.js +4 -3
  33. package/dist/src/sweep.js +17 -0
  34. package/dist/src/template-field.js +60 -3
  35. package/dist/src/test-support.js +35 -23
  36. package/dist/src/version.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +122 -49
  40. package/src/config.ts +3 -0
  41. package/src/configuration-files.ts +29 -25
  42. package/src/conformance-record.ts +2 -2
  43. package/src/contextual-uses.ts +51 -20
  44. package/src/convention-rows.ts +15 -0
  45. package/src/conventions.ts +119 -30
  46. package/src/dependencies.ts +9 -0
  47. package/src/entry-point-gaps.ts +11 -28
  48. package/src/entry-points.ts +7 -1
  49. package/src/exempt.ts +29 -12
  50. package/src/file-facts.ts +25 -21
  51. package/src/findings/completion.ts +31 -11
  52. package/src/findings/interfaces.ts +2 -9
  53. package/src/findings/intra-function.ts +17 -3
  54. package/src/findings/unused-declarations.ts +5 -8
  55. package/src/findings/visibility-narrowing.ts +7 -1
  56. package/src/generated-file.ts +80 -12
  57. package/src/graph.ts +9 -8
  58. package/src/html-entries.ts +170 -0
  59. package/src/inventory.ts +15 -3
  60. package/src/kinds.ts +2 -2
  61. package/src/manifest.ts +49 -7
  62. package/src/matrix.ts +21 -3
  63. package/src/namespace-objects.ts +216 -0
  64. package/src/outside-tests.ts +191 -0
  65. package/src/overrides.ts +136 -0
  66. package/src/paths.ts +18 -0
  67. package/src/references.ts +206 -9
  68. package/src/roots.ts +210 -53
  69. package/src/session.ts +4 -2
  70. package/src/sweep.ts +19 -0
  71. package/src/template-field.ts +65 -3
  72. package/src/test-support.ts +34 -22
  73. package/src/version.ts +1 -1
@@ -1,8 +1,74 @@
1
1
  /**
2
- * The generated-file class on TypeScript: every declaration in a file below a directory
3
- * an applied convention row names as generated is retained, because the framework owns
4
- * the file and a deletion there is undone at the next generation.
2
+ * The generated-file class on TypeScript: every declaration in a file that marks itself
3
+ * generated, or below a directory an applied convention row names as generated, is
4
+ * retained, because a generator owns the file and a deletion there is undone at the next
5
+ * generation.
5
6
  */
7
+ /** The generated-code header, matched against each line of a leading comment. */
8
+ const HEADER_LINE = /^\/\/ Code generated .* DO NOT EDIT\.$/u;
9
+ /** The documentation tag `@generated` as a word of a comment. */
10
+ const GENERATED_TAG = /(?<![\w@$])@generated(?![\w$-])/u;
11
+ /** The comments before a text's first token, a leading hashbang line skipped. */
12
+ function leadingComments(text) {
13
+ const comments = [];
14
+ let at = text.startsWith("\uFEFF") ? 1 : 0;
15
+ if (text.startsWith("#!", at)) {
16
+ const end = text.indexOf("\n", at);
17
+ at = end === -1 ? text.length : end + 1;
18
+ }
19
+ for (;;) {
20
+ while (at < text.length && /\s/u.test(text.charAt(at))) {
21
+ at++;
22
+ }
23
+ if (text.startsWith("//", at)) {
24
+ const end = text.slice(at).search(/[\r\n\u2028\u2029]/u);
25
+ const stop = end === -1 ? text.length : at + end;
26
+ comments.push(text.slice(at, stop));
27
+ at = stop;
28
+ }
29
+ else if (text.startsWith("/*", at)) {
30
+ const end = text.indexOf("*/", at + 2);
31
+ if (end === -1) {
32
+ return comments;
33
+ }
34
+ comments.push(text.slice(at, end + 2));
35
+ at = end + 2;
36
+ }
37
+ else {
38
+ return comments;
39
+ }
40
+ }
41
+ }
42
+ /** The detail a file's leading comments mark it generated by, or undefined where they mark nothing. */
43
+ export function generatedByComment(text) {
44
+ const comments = leadingComments(text);
45
+ if (comments.some((one) => one.split(/\r?\n/u).some((line) => HEADER_LINE.test(line)))) {
46
+ return "generated by the header line its leading comments hold";
47
+ }
48
+ if (comments.some((one) => GENERATED_TAG.test(one))) {
49
+ return "generated by the @generated tag its leading comments hold";
50
+ }
51
+ return undefined;
52
+ }
53
+ /**
54
+ * Every own file of one project that is generated, by its path below the target root,
55
+ * with the detail a retention in it names: its leading comments' mark, else the row
56
+ * naming its directory as generated.
57
+ */
58
+ export function generatedFilesOf(files, byRow) {
59
+ const found = new Map();
60
+ for (const [path, file] of files.byPath) {
61
+ const row = byRow.get(path);
62
+ const detail = generatedByComment(file.text) ??
63
+ (row === undefined
64
+ ? undefined
65
+ : `generated below a directory the convention row ${row} names`);
66
+ if (detail !== undefined) {
67
+ found.set(path, detail);
68
+ }
69
+ }
70
+ return found;
71
+ }
6
72
  /** The generated-file detector, recording each file's first line. */
7
73
  export function generatedFile(input) {
8
74
  const generated = input.generated;
@@ -10,15 +76,9 @@ export function generatedFile(input) {
10
76
  return [];
11
77
  }
12
78
  return input.held.symbols.flatMap((symbol) => {
13
- const row = generated.get(symbol.position.path);
14
- return symbol.kind === "file" || row === undefined
79
+ const detail = generated.get(symbol.position.path);
80
+ return symbol.kind === "file" || detail === undefined
15
81
  ? []
16
- : [
17
- {
18
- id: symbol.id,
19
- detail: `generated below a directory the convention row ${row} names`,
20
- site: { path: symbol.position.path, line: 1, column: 1 },
21
- },
22
- ];
82
+ : [{ id: symbol.id, detail, site: { path: symbol.position.path, line: 1, column: 1 } }];
23
83
  });
24
84
  }
package/dist/src/graph.js CHANGED
@@ -33,10 +33,10 @@ const SWEPT = {
33
33
  /**
34
34
  * Whether a root of each kind names a caller, which holds it live under both relations: a
35
35
  * tool reads the configuration it loads and the files its strings name, a runtime runs a
36
- * test file, a setup file, a worker, a command and a script, the manifest names what
37
- * enters the package, an alias global stands for the export it names, and an entry file,
38
- * a configured root and a pattern assert a caller. A library's published API only supposes
39
- * a consumer the run may not see, so it is live under reachability alone.
36
+ * test file, a setup file, a worker, a page's module script, a command and a script, the
37
+ * manifest names what enters the package, an alias global stands for the export it names,
38
+ * and an entry file, a configured root and a pattern assert a caller. A library's
39
+ * published API only supposes an unseen consumer, so it is live under reachability alone.
40
40
  */
41
41
  const NAMES_A_CALLER = {
42
42
  "entry-file": true,
@@ -55,6 +55,7 @@ const NAMES_A_CALLER = {
55
55
  "mutation-testing": true,
56
56
  "browser-tests": true,
57
57
  worker: true,
58
+ "html-entry": true,
58
59
  };
59
60
  /** Whether the sweep judges a declaration of one kind. */
60
61
  export function swept(kind) {
@@ -94,7 +95,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
94
95
  }
95
96
  }
96
97
  const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
97
- if (reference.consumer !== undefined) {
98
+ if (reference.consumer !== undefined || reference.unheld === true) {
98
99
  consumed.push(edge);
99
100
  continue;
100
101
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The files the module scripts of the target's HTML files load. An HTML file is read as
3
+ * text and no markup is parsed, so a `<script>` inside a comment is read as well.
4
+ */
5
+ import { isExportDeclaration, isImportDeclaration, isStringLiteral, } from "@typescript/native/unstable/ast";
6
+ import { resolveRelativeSpecifier } from "./configuration-files.js";
7
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
8
+ /** The directory no HTML file is read below. */
9
+ const SKIPPED = "node_modules";
10
+ /** A `<script` start tag, its attribute text captured. */
11
+ const SCRIPT_TAG = /<script(?=[\s>/])([^>]*)>/giu;
12
+ /** The end tag an inline module script's content runs to. */
13
+ const SCRIPT_END = /<\/script\s*>/iu;
14
+ /** One attribute: a name, then a double-quoted, single-quoted or unquoted value. */
15
+ const ATTRIBUTE = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/gu;
16
+ /** A value that names a resource on another origin, or by another scheme. */
17
+ const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
18
+ /** The prefixes an inline script's specifier names a file of the target with. */
19
+ const FILE_PREFIXES = ["/", "./", "../"];
20
+ /** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
21
+ function htmlFiles(host, dir) {
22
+ let entries;
23
+ try {
24
+ entries = host.readDirectory(dir);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ const found = [];
30
+ for (const entry of entries) {
31
+ const path = joinPath(dir, entry.name);
32
+ if (entry.directory) {
33
+ if (entry.name !== SKIPPED) {
34
+ found.push(...htmlFiles(host, path));
35
+ }
36
+ }
37
+ else if (entry.name.endsWith(".html")) {
38
+ found.push(path);
39
+ }
40
+ }
41
+ return found.sort();
42
+ }
43
+ /** Each attribute of one start tag's attribute text, by its lowercase name. */
44
+ function attributes(text) {
45
+ const found = new Map();
46
+ for (const match of text.matchAll(ATTRIBUTE)) {
47
+ const [, name = "", double, single, bare] = match;
48
+ const key = name.toLowerCase();
49
+ if (!found.has(key)) {
50
+ found.set(key, double ?? single ?? bare ?? "");
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+ /** The specifiers of the import and re-export declarations one inline script holds. */
56
+ function specifiersOf(parse, fileName, content) {
57
+ return parse(fileName, content).statements.flatMap((statement) => {
58
+ const specifier = isImportDeclaration(statement) || isExportDeclaration(statement)
59
+ ? statement.moduleSpecifier
60
+ : undefined;
61
+ return specifier !== undefined && isStringLiteral(specifier) ? [specifier.text] : [];
62
+ });
63
+ }
64
+ /**
65
+ * Every value a module script of one HTML file names a file by: a `src` attribute's
66
+ * value, or each specifier an inline script imports or re-exports that starts with `/`,
67
+ * `./` or `../`.
68
+ */
69
+ function scriptValues(parse, path, text) {
70
+ const values = [];
71
+ for (const match of text.matchAll(SCRIPT_TAG)) {
72
+ const held = attributes(match[1] ?? "");
73
+ if (held.get("type") !== "module") {
74
+ continue;
75
+ }
76
+ const src = held.get("src");
77
+ if (src !== undefined) {
78
+ values.push(src);
79
+ continue;
80
+ }
81
+ if (parse === undefined) {
82
+ continue;
83
+ }
84
+ const start = match.index + match[0].length;
85
+ const rest = text.slice(start);
86
+ const end = rest.search(SCRIPT_END);
87
+ const content = end === -1 ? rest : rest.slice(0, end);
88
+ values.push(...specifiersOf(parse, `${path}.${String(start)}.ts`, content).filter((specifier) => FILE_PREFIXES.some((prefix) => specifier.startsWith(prefix))));
89
+ }
90
+ return values;
91
+ }
92
+ /** The directory of the nearest `package.json` at or above `dir`, inside `root`, else `root`. */
93
+ function manifestDirectory(host, root, dir) {
94
+ for (let at = dir; relativePath(root, at) !== undefined; at = dirnamePath(at)) {
95
+ if (host.kindOf(joinPath(at, "package.json")) === "file") {
96
+ return at;
97
+ }
98
+ if (at === root) {
99
+ break;
100
+ }
101
+ }
102
+ return root;
103
+ }
104
+ /**
105
+ * The own files the module scripts of every HTML file below the target root load. A value
106
+ * starting with a single `/` is read against the nearest manifest's directory, any other
107
+ * against the HTML file's own, and either resolves as a configuration file's relative
108
+ * specifier does. A value naming another origin or scheme, or no source file of the
109
+ * program, loads nothing here.
110
+ */
111
+ export function htmlEntries(host, targetRoot, files, parse) {
112
+ const root = resolvePath(host.workingDirectory(), targetRoot);
113
+ const found = [];
114
+ for (const path of htmlFiles(host, root)) {
115
+ let text;
116
+ try {
117
+ text = host.readFile(path);
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ const dir = dirnamePath(path);
123
+ for (const value of scriptValues(parse, path, text)) {
124
+ if (ELSEWHERE.test(value)) {
125
+ continue;
126
+ }
127
+ const reached = value.startsWith("/")
128
+ ? resolveRelativeSpecifier(host, manifestDirectory(host, root, dir), `.${value}`)
129
+ : resolveRelativeSpecifier(host, dir, value);
130
+ const file = reached === undefined ? undefined : files.byName.get(reached);
131
+ if (file !== undefined) {
132
+ found.push({ file, source: value });
133
+ }
134
+ }
135
+ }
136
+ return found;
137
+ }
@@ -213,7 +213,18 @@ function nameNodeOf(node) {
213
213
  * needs the declaration this enumeration made for it.
214
214
  */
215
215
  export function nodeKey(file, node) {
216
- return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
216
+ return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
217
+ }
218
+ /** `export default`, up to the keyword that stands where an unnamed default export's name would. */
219
+ const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
220
+ /**
221
+ * Where an unnamed declaration is placed: at `default` when it is the default export, so it
222
+ * never shares the file's own position, else at its first token.
223
+ */
224
+ function unnamedStart(node) {
225
+ const start = node.getStart();
226
+ const prefix = DEFAULT_EXPORT.exec(node.getText());
227
+ return prefix === null ? start : start + prefix[0].length;
217
228
  }
218
229
  /** The kind a class element takes: a method, or a property or accessor. */
219
230
  function classMemberKind(node) {
@@ -287,12 +298,11 @@ export function inventory(project, host, targetRoot) {
287
298
  }
288
299
  }
289
300
  function declare(node, owner, kind, name, named) {
290
- const at = name ?? node;
291
301
  const record = keep({
292
302
  file: owner.file,
293
303
  node,
294
304
  module: owner.module,
295
- position: renderPosition(owner.file, targetRoot, at.getStart()),
305
+ position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
296
306
  endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
297
307
  kind,
298
308
  parentChain: owner.kind === "file" ? [] : chainOf(owner),
package/dist/src/kinds.js CHANGED
@@ -40,7 +40,7 @@ const ROWS = [
40
40
  code: "DS1004",
41
41
  name: "test-only-use",
42
42
  languages: ["go", "ts"],
43
- rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only test code imports, is a test reference, and a declaration of test-support code is never reported under this code.",
43
+ rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only tests reach, is a test reference. A declaration of test-support code that test code references is reported under this code at the reachability class possible, as grammar/analysis.md states. Such a declaration that is an interface is reported under DS1201 instead.",
44
44
  defaultSeverity: "deny",
45
45
  maxClass: "certain",
46
46
  fixability: "deletable",
@@ -144,7 +144,7 @@ const ROWS = [
144
144
  code: "DS1301",
145
145
  name: "write-only-symbol",
146
146
  languages: ["go", "ts"],
147
- rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, or an import binding the writes alone use. The finding's message names each such declaration.",
147
+ rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
148
148
  defaultSeverity: "deny",
149
149
  maxClass: "certain",
150
150
  fixability: "deletable",
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * The entry points a package manifest names, which are roots because something
3
3
  * outside the analyzed program reaches them: a consumer importing the package, or a
4
- * shell running its command or one of its scripts.
5
- *
6
- * One call reads one manifest, the one in the directory it is given.
4
+ * shell running its command or one of its scripts. {@link readManifest} reads one
5
+ * directory's manifest, {@link packageEntries} those of the packages below the target root.
7
6
  */
8
- import { isAbsolutePath, joinPath, resolvePath } from "./paths.js";
7
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
9
8
  /** The manifest's own name. */
10
9
  const MANIFEST = "package.json";
11
10
  /** The three members that name an entry point beside `exports` and `bin`. */
@@ -140,7 +139,37 @@ export function readManifest(host, targetRoot) {
140
139
  }
141
140
  }
142
141
  return {
143
- entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
142
+ entries: found.map((entry) => ({
143
+ ...entry,
144
+ path: resolvePath(targetRoot, entry.path),
145
+ dir: targetRoot,
146
+ })),
144
147
  declaresExports: manifest["exports"] !== undefined,
145
148
  };
146
149
  }
150
+ /**
151
+ * The entry points every package manifest between the target root and each compiler
152
+ * configuration names: each directory holding a `package.json` that a configuration sits
153
+ * in or below, the target root and `known` excluded, whose manifests the run reads
154
+ * otherwise. Each entry is spelled under its manifest's path below the target root, and
155
+ * is published by its own manifest's rule, as the target's own entries are.
156
+ */
157
+ export function packageEntries(host, targetRoot, configFiles, known) {
158
+ const dirs = new Set();
159
+ for (const configFile of configFiles) {
160
+ for (let dir = dirnamePath(resolvePath(targetRoot, configFile)); dir !== targetRoot && relativePath(targetRoot, dir) !== undefined; dir = dirnamePath(dir)) {
161
+ if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
162
+ dirs.add(dir);
163
+ }
164
+ }
165
+ }
166
+ return [...dirs].sort().flatMap((dir) => {
167
+ const manifest = readManifest(host, dir);
168
+ const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
169
+ return manifest.entries.map((entry) => ({
170
+ ...entry,
171
+ member: `${where} ${entry.member}`,
172
+ published: entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
173
+ }));
174
+ });
175
+ }
@@ -4,7 +4,7 @@
4
4
  * and intersected, with the dead components computed once over every configuration.
5
5
  */
6
6
  import { componentsOf } from "./components.js";
7
- import { graphOf } from "./graph.js";
7
+ import { graphOf, OUTSIDE } from "./graph.js";
8
8
  import { byPosition } from "./position.js";
9
9
  import { sweep } from "./sweep.js";
10
10
  /** Two strings ordered bytewise. */
@@ -102,6 +102,7 @@ export function sweepMatrix(matrix, input) {
102
102
  const union = matrix.union;
103
103
  const dead = union.symbols.map(() => false);
104
104
  const testOfDeadCode = union.symbols.map(() => false);
105
+ const heldBy = union.symbols.map(() => undefined);
105
106
  const candidates = [];
106
107
  union.symbols.forEach((symbol, at) => {
107
108
  const held = matrix.heldIn[at] ?? [];
@@ -111,23 +112,28 @@ export function sweepMatrix(matrix, input) {
111
112
  }
112
113
  const all = found.filter((candidate) => candidate !== undefined);
113
114
  const admitted = all.every((candidate) => candidate.testOfDeadCode);
115
+ const testRefs = union.made[at]?.test ?? 0;
116
+ const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
117
+ (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
114
118
  dead[at] = true;
115
119
  testOfDeadCode[at] = admitted;
120
+ heldBy[at] = heldByTests;
116
121
  candidates.push({
117
122
  id: symbol.id,
118
123
  productionRefs: union.made[at]?.production ?? 0,
119
- testRefs: union.made[at]?.test ?? 0,
124
+ testRefs,
120
125
  relation: all.every((candidate) => candidate.relation === "reference-counting")
121
126
  ? "reference-counting"
122
127
  : "reachability",
123
128
  testOfDeadCode: admitted,
124
129
  configurations: held,
130
+ heldByTests,
125
131
  });
126
132
  });
127
133
  return {
128
134
  liveUnder: liveAnywhere(union.symbols, per),
129
135
  candidates,
130
- components: componentsOf(union, dead, testOfDeadCode),
136
+ components: componentsOf(union, dead.map((one, at) => one && heldBy[at] === undefined), testOfDeadCode),
131
137
  };
132
138
  }
133
139
  /**
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Module namespace objects: the binding of a namespace import, the value an `await` of a
3
+ * dynamic `import()` produces, the first parameter of the first callback passed to `then`
4
+ * on one, and a `const` binding initialized with one of these. A use reads the exports it
5
+ * names and no other: a property access by name or by a literal index, or a destructured
6
+ * property, names one export, and any other use may read every export.
7
+ */
8
+ import { isArrowFunction, isAwaitExpression, isBindingElement, isCallExpression, isElementAccessExpression, isExpressionStatement, isFunctionExpression, isIdentifier, isImportDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isObjectBindingPattern, isParenthesizedExpression, isPropertyAccessExpression, isQualifiedName, isStringLiteral, isVariableDeclaration, isVariableDeclarationList, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ function isLiteralText(node) {
10
+ return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node);
11
+ }
12
+ /** The outermost expression one node is, through the parentheses written around it. */
13
+ function parenthesized(node) {
14
+ let at = node;
15
+ while (isParenthesizedExpression(at.parent)) {
16
+ at = at.parent;
17
+ }
18
+ return at;
19
+ }
20
+ /** Whether one call is a dynamic `import()` whose argument is literal text. */
21
+ function isLiteralImport(node) {
22
+ if (!isCallExpression(node) || node.expression.kind !== SyntaxKind.ImportKeyword) {
23
+ return false;
24
+ }
25
+ const [argument] = node.arguments;
26
+ return argument !== undefined && isLiteralText(argument);
27
+ }
28
+ /** The node a `const` binding's name is visible below: the block or file its statement is in. */
29
+ function scopeOf(declaration) {
30
+ const statement = declaration.parent.parent;
31
+ return statement.parent;
32
+ }
33
+ /** Every identifier below `scope` that spells `name`, other than `declared` and a member's name. */
34
+ function occurrences(scope, name, declared) {
35
+ const found = [];
36
+ const visit = (node) => {
37
+ if (isIdentifier(node)) {
38
+ const parent = node.parent;
39
+ const member = (isPropertyAccessExpression(parent) && parent.name === node) ||
40
+ (isQualifiedName(parent) && parent.right === node) ||
41
+ (isBindingElement(parent) && parent.propertyName === node);
42
+ if (node !== declared && node.text === name && !member) {
43
+ found.push(node);
44
+ }
45
+ return;
46
+ }
47
+ node.forEachChild(visit);
48
+ };
49
+ scope.forEachChild(visit);
50
+ return found;
51
+ }
52
+ /**
53
+ * Every use of the namespace objects one file holds, keyed by the node the use is written
54
+ * at, that reads more than a name the checker resolves: an indexed read by a literal, and
55
+ * any use that may read every export. A shadowing declaration of a binding's name inside
56
+ * its scope is read as the binding, which can only keep an export live.
57
+ */
58
+ export function namespaceUses(file) {
59
+ const uses = new Map();
60
+ const binding = (name, scope, specifier, seen) => {
61
+ if (seen.has(name)) {
62
+ return;
63
+ }
64
+ seen.add(name);
65
+ for (const one of occurrences(scope, name.text, name)) {
66
+ value(one, specifier, seen);
67
+ }
68
+ };
69
+ /** One use of a namespace object's value, at `node`. */
70
+ const value = (node, specifier, seen) => {
71
+ const at = parenthesized(node);
72
+ const parent = at.parent;
73
+ if (isExpressionStatement(parent) || isVoidExpression(parent)) {
74
+ return;
75
+ }
76
+ if ((isPropertyAccessExpression(parent) && parent.expression === at) ||
77
+ (isQualifiedName(parent) && parent.left === at)) {
78
+ return;
79
+ }
80
+ if (isElementAccessExpression(parent) && parent.expression === at) {
81
+ const index = parent.argumentExpression;
82
+ uses.set(node, isLiteralText(index) ? { kind: "indexed", literal: index } : { kind: "whole", specifier });
83
+ return;
84
+ }
85
+ if (isVariableDeclaration(parent) && parent.initializer === at) {
86
+ const pattern = parent.name;
87
+ if (isObjectBindingPattern(pattern)) {
88
+ const named = pattern.elements.every((element) => element.dotDotDotToken === undefined &&
89
+ element.propertyName?.kind !== SyntaxKind.ComputedPropertyName);
90
+ if (!named) {
91
+ uses.set(node, { kind: "whole", specifier });
92
+ }
93
+ return;
94
+ }
95
+ const list = parent.parent;
96
+ if (isIdentifier(pattern) &&
97
+ isVariableDeclarationList(list) &&
98
+ (list.flags & NodeFlags.Const) !== 0) {
99
+ binding(pattern, scopeOf(parent), specifier, seen);
100
+ return;
101
+ }
102
+ }
103
+ uses.set(node, { kind: "whole", specifier });
104
+ };
105
+ /** One use of a dynamic `import()` call, at `call`. */
106
+ const imported = (call, specifier, seen) => {
107
+ const at = parenthesized(call);
108
+ const parent = at.parent;
109
+ if (isExpressionStatement(parent) || isVoidExpression(parent)) {
110
+ return;
111
+ }
112
+ if (isAwaitExpression(parent)) {
113
+ value(parent, specifier, seen);
114
+ return;
115
+ }
116
+ const then = parent.parent;
117
+ if (isPropertyAccessExpression(parent) &&
118
+ parent.expression === at &&
119
+ parent.name.text === "then" &&
120
+ isCallExpression(then) &&
121
+ then.expression === parent) {
122
+ const [callback] = then.arguments;
123
+ if (callback !== undefined && (isArrowFunction(callback) || isFunctionExpression(callback))) {
124
+ const [first] = callback.parameters;
125
+ if (first === undefined || isObjectBindingPattern(first.name)) {
126
+ return;
127
+ }
128
+ if (isIdentifier(first.name)) {
129
+ binding(first.name, callback, specifier, seen);
130
+ return;
131
+ }
132
+ }
133
+ }
134
+ uses.set(call, { kind: "whole", specifier });
135
+ };
136
+ const visit = (node) => {
137
+ if (isImportDeclaration(node)) {
138
+ const clause = node.importClause;
139
+ const bindings = clause?.namedBindings;
140
+ if (clause !== undefined &&
141
+ clause.phaseModifier !== SyntaxKind.TypeKeyword &&
142
+ bindings !== undefined &&
143
+ isNamespaceImport(bindings)) {
144
+ binding(bindings.name, file, node.moduleSpecifier, new Set());
145
+ }
146
+ return;
147
+ }
148
+ if (isLiteralImport(node) && isCallExpression(node)) {
149
+ const [argument] = node.arguments;
150
+ if (argument !== undefined) {
151
+ imported(node, argument, new Set());
152
+ }
153
+ }
154
+ node.forEachChild(visit);
155
+ };
156
+ file.forEachChild(visit);
157
+ return uses;
158
+ }