@cplieger/deadset-ts 8.0.0-dev.5 → 8.0.0-dev.7

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 (201) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +3 -2
  3. package/dist/src/analysis.js +28 -46
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +3 -2
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/contract/contract.generated.js +1084 -0
  12. package/dist/src/contract/report.generated.js +796 -0
  13. package/dist/src/conventions.js +20 -60
  14. package/dist/src/dependencies.js +25 -43
  15. package/dist/src/discover.js +13 -39
  16. package/dist/src/edges.js +7 -15
  17. package/dist/src/element-access.js +4 -6
  18. package/dist/src/entry-point-gaps.js +2 -8
  19. package/dist/src/entry-points.js +3 -10
  20. package/dist/src/enum-group.js +3 -10
  21. package/dist/src/erased-stores.js +4 -6
  22. package/dist/src/exempt-classes.js +3 -63
  23. package/dist/src/exempt.js +13 -40
  24. package/dist/src/file-facts.js +2 -25
  25. package/dist/src/findings/completion.js +11 -20
  26. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  27. package/dist/src/findings/emitters.js +4 -4
  28. package/dist/src/findings/interfaces.js +4 -5
  29. package/dist/src/findings/intra-function.js +8 -7
  30. package/dist/src/findings/non-code-artifacts.js +5 -6
  31. package/dist/src/findings/reads-and-writes.js +11 -9
  32. package/dist/src/findings/self-check.js +10 -12
  33. package/dist/src/findings/unused-declarations.js +28 -27
  34. package/dist/src/findings/visibility-narrowing.js +9 -11
  35. package/dist/src/findings-pass.js +7 -14
  36. package/dist/src/fs/manifest.js +43 -0
  37. package/dist/src/fs/tree.js +88 -0
  38. package/dist/src/git-ignore.js +34 -25
  39. package/dist/src/html-entries.js +6 -33
  40. package/dist/src/in-operands.js +2 -1
  41. package/dist/src/index.js +1 -1
  42. package/dist/src/intra-function-parts.js +18 -26
  43. package/dist/src/inventory.js +2 -21
  44. package/dist/src/json-document.js +4 -0
  45. package/dist/src/keyof-comparisons.js +4 -6
  46. package/dist/src/kinds.js +4 -360
  47. package/dist/src/manifest.js +5 -43
  48. package/dist/src/mdx-entries.js +3 -26
  49. package/dist/src/partial.js +2 -1
  50. package/dist/src/paths.js +26 -15
  51. package/dist/src/position.js +2 -1
  52. package/dist/src/print.js +3 -2
  53. package/dist/src/product.js +13 -0
  54. package/dist/src/references.js +14 -27
  55. package/dist/src/report-schema.js +272 -0
  56. package/dist/src/report.js +39 -42
  57. package/dist/src/reporters/sarif.js +4 -2
  58. package/dist/src/reporters/template.js +4 -19
  59. package/dist/src/resolve.js +12 -26
  60. package/dist/src/roots.js +7 -17
  61. package/dist/src/run.js +10 -8
  62. package/dist/src/schema.js +4 -3
  63. package/dist/src/scope.js +4 -5
  64. package/dist/src/serialization-contract.js +4 -8
  65. package/dist/src/session.js +2 -1
  66. package/dist/src/setup-failure.js +2 -6
  67. package/dist/src/suppress-file.js +12 -13
  68. package/dist/src/suppress-inline.js +3 -2
  69. package/dist/src/suppress.js +29 -48
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +5 -15
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/verbs/analyze.js +22 -15
  82. package/dist/src/verbs/describe.js +7 -7
  83. package/dist/src/verbs/explain.js +7 -4
  84. package/dist/src/verbs/print-config.js +2 -1
  85. package/dist/src/verbs/print-projects.js +4 -2
  86. package/dist/src/verbs/print-retained.js +4 -2
  87. package/dist/src/verbs/print-roots.js +4 -2
  88. package/dist/src/verbs/verb.js +1 -5
  89. package/dist/src/verbs/version.js +4 -3
  90. package/dist/src/workflow-steps.js +9 -46
  91. package/dist/src/workspace-manifests.js +2 -2
  92. package/dist/src/workspace-programs.js +5 -3
  93. package/dist/src/workspace-resolution.js +13 -17
  94. package/dist/src/workspace.js +28 -116
  95. package/jsr.json +1 -1
  96. package/package.json +2 -1
  97. package/src/analysis.ts +29 -54
  98. package/src/automatic-types.ts +11 -29
  99. package/src/build-scripts.ts +17 -38
  100. package/src/component-layer.ts +4 -2
  101. package/src/components.ts +2 -4
  102. package/src/config.ts +11 -15
  103. package/src/configuration-files.ts +27 -170
  104. package/src/conformance-record.ts +1 -1
  105. package/src/contract/contract.generated.ts +1148 -0
  106. package/src/contract/report.generated.ts +1157 -0
  107. package/src/conventions.ts +26 -74
  108. package/src/dependencies.ts +32 -50
  109. package/src/discover.ts +14 -39
  110. package/src/edges.ts +7 -21
  111. package/src/element-access.ts +4 -7
  112. package/src/entry-point-gaps.ts +3 -9
  113. package/src/entry-points.ts +2 -13
  114. package/src/enum-group.ts +2 -11
  115. package/src/erased-stores.ts +3 -7
  116. package/src/exempt-classes.ts +5 -74
  117. package/src/exempt.ts +17 -47
  118. package/src/file-facts.ts +6 -24
  119. package/src/finding.ts +9 -9
  120. package/src/findings/completion.ts +22 -23
  121. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  122. package/src/findings/emitter.ts +6 -7
  123. package/src/findings/emitters.ts +6 -6
  124. package/src/findings/interfaces.ts +4 -6
  125. package/src/findings/intra-function.ts +8 -8
  126. package/src/findings/non-code-artifacts.ts +5 -7
  127. package/src/findings/reads-and-writes.ts +15 -12
  128. package/src/findings/self-check.ts +11 -17
  129. package/src/findings/unused-declarations.ts +28 -27
  130. package/src/findings/visibility-narrowing.ts +9 -12
  131. package/src/findings-pass.ts +7 -16
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/html-entries.ts +6 -33
  136. package/src/in-operands.ts +2 -1
  137. package/src/index.ts +1 -1
  138. package/src/intra-function-parts.ts +18 -28
  139. package/src/inventory.ts +2 -20
  140. package/src/json-document.ts +5 -0
  141. package/src/keyof-comparisons.ts +3 -7
  142. package/src/kinds.ts +6 -406
  143. package/src/manifest.ts +5 -46
  144. package/src/mdx-entries.ts +3 -26
  145. package/src/partial.ts +2 -1
  146. package/src/paths.ts +27 -15
  147. package/src/position.ts +2 -1
  148. package/src/print.ts +3 -2
  149. package/src/product.ts +18 -0
  150. package/src/references.ts +14 -29
  151. package/src/report-schema.ts +364 -0
  152. package/src/report.ts +66 -229
  153. package/src/reporters/annotations.ts +2 -2
  154. package/src/reporters/json.ts +1 -1
  155. package/src/reporters/reporter.ts +2 -2
  156. package/src/reporters/reporters.ts +1 -1
  157. package/src/reporters/sarif.ts +17 -12
  158. package/src/reporters/template.ts +4 -20
  159. package/src/reporters/text.ts +3 -3
  160. package/src/resolve.ts +31 -38
  161. package/src/roots.ts +7 -20
  162. package/src/run.ts +10 -14
  163. package/src/schema.ts +4 -3
  164. package/src/scope.ts +4 -10
  165. package/src/serialization-contract.ts +4 -9
  166. package/src/session.ts +2 -1
  167. package/src/setup-failure.ts +5 -7
  168. package/src/suppress-file.ts +11 -15
  169. package/src/suppress-inline.ts +2 -2
  170. package/src/suppress.ts +39 -55
  171. package/src/symbol-keys.ts +2 -5
  172. package/src/template-field.ts +5 -18
  173. package/src/test-globals.ts +9 -27
  174. package/src/test-joins.ts +6 -8
  175. package/src/text/compare.ts +27 -0
  176. package/src/text/glob.ts +210 -0
  177. package/src/text/shell.ts +161 -0
  178. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  179. package/src/type-errors.ts +25 -36
  180. package/src/unseen-loads.ts +1 -12
  181. package/src/unwrap.ts +34 -0
  182. package/src/verbs/analyze.ts +38 -25
  183. package/src/verbs/describe.ts +13 -7
  184. package/src/verbs/explain.ts +7 -4
  185. package/src/verbs/print-config.ts +2 -1
  186. package/src/verbs/print-projects.ts +4 -2
  187. package/src/verbs/print-retained.ts +4 -2
  188. package/src/verbs/print-roots.ts +4 -2
  189. package/src/verbs/verb.ts +0 -6
  190. package/src/verbs/version.ts +4 -3
  191. package/src/workflow-steps.ts +9 -44
  192. package/src/workspace-manifests.ts +2 -2
  193. package/src/workspace-programs.ts +5 -3
  194. package/src/workspace-resolution.ts +21 -17
  195. package/src/workspace.ts +35 -121
  196. package/dist/src/glob.js +0 -65
  197. package/dist/src/pnpm-workspace.js +0 -120
  198. package/dist/src/version.js +0 -7
  199. package/src/glob.ts +0 -67
  200. package/src/pnpm-workspace.ts +0 -135
  201. package/src/version.ts +0 -7
package/src/fs/tree.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { DirectoryEntry, Host } from "../host.ts";
2
+ import { ancestors, joinPath, resolvePath } from "../paths.ts";
3
+ import { LANGUAGE_ROWS } from "../contract/contract.generated.ts";
4
+ import { LANGUAGE } from "../product.ts";
5
+ import { bytewise } from "../text/compare.ts";
6
+ import { globExpression } from "../text/glob.ts";
7
+
8
+ /** The directories no walk of the tree enters, by the patterns the language's row names. */
9
+ const SKIPPED_DIRECTORIES = LANGUAGE_ROWS[LANGUAGE].skippedDirectories.map(globExpression);
10
+
11
+ function skipped(name: string): boolean {
12
+ return SKIPPED_DIRECTORIES.some((pattern) => pattern.test(name));
13
+ }
14
+
15
+ /** A git repository's working tree root, and the `info/exclude` file git reads for it. */
16
+ interface Repository {
17
+ readonly root: string;
18
+ /** Undefined where a `.git` file names no repository directory. */
19
+ readonly exclude: string | undefined;
20
+ }
21
+
22
+ /** One file's text without its line ending, or undefined where it cannot be read. */
23
+ function lineOf(host: Host, path: string): string | undefined {
24
+ try {
25
+ return host.readFile(path).replace(/\r?\n$/u, "");
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * The repository whose working tree holds `dir`: the nearest directory at or above it with a
33
+ * `.git`. A `.git` file, which a linked worktree or a submodule holds, names the repository's
34
+ * directory on its `gitdir:` line; that directory's `commondir` file, where it has one, names
35
+ * the directory shared with the main worktree, which holds `info/exclude`:
36
+ * https://git-scm.com/docs/gitrepository-layout
37
+ */
38
+ export function repositoryOf(host: Host, dir: string): Repository | undefined {
39
+ for (const root of ancestors(dir)) {
40
+ const dotGit = joinPath(root, ".git");
41
+ const kind = host.kindOf(dotGit);
42
+ if (kind === "directory") {
43
+ return { root, exclude: joinPath(dotGit, "info/exclude") };
44
+ }
45
+ if (kind === "file") {
46
+ const named = /^gitdir: (.+)$/u.exec(lineOf(host, dotGit) ?? "")?.[1];
47
+ const gitDir = named === undefined ? undefined : resolvePath(root, named);
48
+ const common = gitDir === undefined ? undefined : lineOf(host, joinPath(gitDir, "commondir"));
49
+ const shared =
50
+ gitDir === undefined || common === undefined ? gitDir : resolvePath(gitDir, common);
51
+ return { root, exclude: shared === undefined ? undefined : joinPath(shared, "info/exclude") };
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /** The entries of one directory, none where it cannot be read. */
58
+ export function entriesOf(host: Host, dir: string): readonly DirectoryEntry[] {
59
+ try {
60
+ return host.readDirectory(dir);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /** The names of the files directly in `dir` whose name `accepts`, in bytewise order. */
67
+ export function filesIn(
68
+ host: Host,
69
+ dir: string,
70
+ accepts: (name: string) => boolean,
71
+ ): readonly string[] {
72
+ return entriesOf(host, dir)
73
+ .filter((entry) => !entry.directory && accepts(entry.name))
74
+ .map((entry) => entry.name)
75
+ .sort(bytewise);
76
+ }
77
+
78
+ /** The names of the directories directly in `dir` a walk enters, in bytewise order. */
79
+ export function directoriesIn(host: Host, dir: string): readonly string[] {
80
+ return entriesOf(host, dir)
81
+ .filter((entry) => entry.directory && !skipped(entry.name))
82
+ .map((entry) => entry.name)
83
+ .sort(bytewise);
84
+ }
85
+
86
+ /**
87
+ * Every file below `root` whose name `accepts`, by its path relative to `root` in bytewise
88
+ * order, read in no skipped directory.
89
+ */
90
+ export function filesBelow(
91
+ host: Host,
92
+ root: string,
93
+ accepts: (name: string) => boolean,
94
+ ): readonly string[] {
95
+ const found: string[] = [];
96
+ const pending = [""];
97
+ for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
98
+ for (const entry of entriesOf(host, joinPath(root, dir))) {
99
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
100
+ if (!entry.directory) {
101
+ if (accepts(entry.name)) {
102
+ found.push(path);
103
+ }
104
+ } else if (!skipped(entry.name)) {
105
+ pending.push(path);
106
+ }
107
+ }
108
+ }
109
+ return found.sort(bytewise);
110
+ }
package/src/git-ignore.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Which files the `.gitignore` files of a tree ignore, read as git reads them:
2
+ * Which files git ignores below a tree's root, read as git reads its ignore files:
3
3
  * https://git-scm.com/docs/gitignore#_pattern_format
4
4
  */
5
5
 
6
- import { globExpression } from "./glob.ts";
7
6
  import type { Host } from "./host.ts";
8
- import { dirnamePath, joinPath, relativePath } from "./paths.ts";
7
+ import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.ts";
8
+ import { gitignoreExpression } from "./text/glob.ts";
9
+ import { repositoryOf } from "./fs/tree.ts";
9
10
 
10
- /** One pattern line of a `.gitignore` file. */
11
+ /** One pattern line of an ignore file. */
11
12
  interface Rule {
12
13
  readonly negated: boolean;
13
14
  /** Whether the pattern ends with `/`, so it matches a directory alone. */
@@ -16,7 +17,7 @@ interface Rule {
16
17
  readonly expression: RegExp;
17
18
  }
18
19
 
19
- /** The rules one `.gitignore` text writes, in order. */
20
+ /** The rules one ignore file's text writes, in order. */
20
21
  function rulesOf(text: string): readonly Rule[] {
21
22
  const rules: Rule[] = [];
22
23
  for (const raw of text.split(/\r?\n/u)) {
@@ -25,7 +26,7 @@ function rulesOf(text: string): readonly Rule[] {
25
26
  continue;
26
27
  }
27
28
  const negated = line.startsWith("!");
28
- line = negated || line.startsWith("\\") ? line.slice(1) : line;
29
+ line = negated ? line.slice(1) : line;
29
30
  const directoryOnly = line.endsWith("/");
30
31
  line = line.replace(/\/+$/u, "");
31
32
  if (line === "") {
@@ -38,43 +39,55 @@ function rulesOf(text: string): readonly Rule[] {
38
39
  rules.push({
39
40
  negated,
40
41
  directoryOnly,
41
- expression: globExpression(anchored ? pattern : `**/${pattern}`),
42
+ expression: gitignoreExpression(anchored ? pattern : `**/${pattern}`),
42
43
  });
43
44
  }
44
45
  return rules;
45
46
  }
46
47
 
48
+ function rulesIn(host: Host, path: string): readonly Rule[] {
49
+ try {
50
+ return rulesOf(host.readFile(path));
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
47
56
  /**
48
- * Whether the `.gitignore` files at and below `root` ignore one absolute path. A path
49
- * outside `root` is not ignored, and a file below an ignored directory stays ignored
50
- * whatever a later pattern says, as git cannot re-include it.
57
+ * Whether git ignores one absolute path below `root`: by the repository's `info/exclude`,
58
+ * then the `.gitignore` files from the repository's root down to the path's directory, a
59
+ * later pattern overriding an earlier one. A path outside `root` is not ignored, and a file
60
+ * below an ignored directory stays ignored whatever a later pattern says, as git cannot
61
+ * re-include it. With no repository at or above `root`, the files at and below it are read;
62
+ * with one, the directories above `root` give patterns but are not themselves tested.
51
63
  */
52
64
  export function gitIgnored(host: Host, root: string): (path: string) => boolean {
65
+ const found = repositoryOf(host, root);
66
+ const repository = found?.root;
67
+ const above =
68
+ repository === undefined ? [] : [...ancestors(dirnamePath(root), repository)].reverse();
69
+ const excluded = found?.exclude === undefined ? [] : rulesIn(host, found.exclude);
53
70
  const read = new Map<string, readonly Rule[]>();
54
71
  const rulesAt = (dir: string): readonly Rule[] => {
55
72
  let rules = read.get(dir);
56
73
  if (rules === undefined) {
57
- try {
58
- rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
59
- } catch {
60
- rules = [];
61
- }
74
+ rules = rulesIn(host, joinPath(dir, ".gitignore"));
62
75
  read.set(dir, rules);
63
76
  }
64
77
  return rules;
65
78
  };
66
- /** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
67
79
  const decided = (path: string, directory: boolean): boolean => {
68
- const dirs: string[] = [];
69
- for (let at = dirnamePath(path); relativePath(root, at) !== undefined; at = dirnamePath(at)) {
70
- dirs.unshift(at);
71
- }
72
- dirs.unshift(root);
80
+ const sources: [string, readonly Rule[]][] = [
81
+ ...(repository === undefined ? [] : [[repository, excluded] as [string, readonly Rule[]]]),
82
+ ...[...above, ...[...ancestors(dirnamePath(path), root)].reverse()].map(
83
+ (dir): [string, readonly Rule[]] => [dir, rulesAt(dir)],
84
+ ),
85
+ ];
73
86
  let ignored = false;
74
- for (const dir of dirs) {
75
- const below = relativePath(dir, path);
76
- for (const rule of below === undefined ? [] : rulesAt(dir)) {
77
- if ((directory || !rule.directoryOnly) && rule.expression.test(below ?? "")) {
87
+ for (const [dir, rules] of sources) {
88
+ const below = relativePath(dir, path) ?? "";
89
+ for (const rule of rules) {
90
+ if ((directory || !rule.directoryOnly) && rule.expression.test(below)) {
78
91
  ignored = !rule.negated;
79
92
  }
80
93
  }
@@ -12,8 +12,10 @@ import {
12
12
  } from "@typescript/native/ast";
13
13
  import { resolveRelativeSpecifier, type ParseFile } from "./configuration-files.ts";
14
14
  import type { Host } from "./host.ts";
15
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
15
+ import { ancestors, dirnamePath, joinPath, resolvePath } from "./paths.ts";
16
16
  import type { SourceFiles } from "./source-files.ts";
17
+ import { filesBelow } from "./fs/tree.ts";
18
+ import { holdsManifest } from "./fs/manifest.ts";
17
19
 
18
20
  /** One file a script loads, and the value or specifier that named it. */
19
21
  interface HTMLEntry {
@@ -21,9 +23,6 @@ interface HTMLEntry {
21
23
  readonly source: string;
22
24
  }
23
25
 
24
- /** The directory no HTML file is read below. */
25
- const SKIPPED = "node_modules";
26
-
27
26
  /** A `<script` start tag, its attribute text captured. */
28
27
  const SCRIPT_TAG = /<script(?=[\s>/])([^>]*)>/giu;
29
28
 
@@ -42,28 +41,6 @@ const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "applicat
42
41
  /** The prefixes an inline script's specifier names a file of the target with. */
43
42
  const FILE_PREFIXES = ["/", "./", "../"] as const;
44
43
 
45
- /** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
46
- function htmlFiles(host: Host, dir: string): readonly string[] {
47
- let entries;
48
- try {
49
- entries = host.readDirectory(dir);
50
- } catch {
51
- return [];
52
- }
53
- const found: string[] = [];
54
- for (const entry of entries) {
55
- const path = joinPath(dir, entry.name);
56
- if (entry.directory) {
57
- if (entry.name !== SKIPPED) {
58
- found.push(...htmlFiles(host, path));
59
- }
60
- } else if (entry.name.endsWith(".html")) {
61
- found.push(path);
62
- }
63
- }
64
- return found.sort();
65
- }
66
-
67
44
  /** Each attribute of one start tag's attribute text, by its lowercase name. */
68
45
  function attributes(text: string): ReadonlyMap<string, string> {
69
46
  const found = new Map<string, string>();
@@ -127,12 +104,7 @@ function scriptValues(parse: ParseFile | undefined, path: string, text: string):
127
104
 
128
105
  /** The directory of the nearest `package.json` at or above `dir`, inside `root`, else `root`. */
129
106
  function manifestDirectory(host: Host, root: string, dir: string): string {
130
- for (let at = dir; relativePath(root, at) !== undefined; at = dirnamePath(at)) {
131
- if (host.kindOf(joinPath(at, "package.json")) === "file") {
132
- return at;
133
- }
134
- }
135
- return root;
107
+ return ancestors(dir, root).find((at) => holdsManifest(host, at)) ?? root;
136
108
  }
137
109
 
138
110
  /**
@@ -150,7 +122,8 @@ export function htmlEntries(
150
122
  ): readonly HTMLEntry[] {
151
123
  const root = resolvePath(host.workingDirectory(), targetRoot);
152
124
  const found: HTMLEntry[] = [];
153
- for (const path of htmlFiles(host, root)) {
125
+ for (const below of filesBelow(host, root, (name) => name.endsWith(".html"))) {
126
+ const path = joinPath(root, below);
154
127
  let text: string;
155
128
  try {
156
129
  text = host.readFile(path);
@@ -13,6 +13,7 @@ import {
13
13
  } from "@typescript/native/ast";
14
14
  import type { ProjectView } from "./session.ts";
15
15
  import { sourceFilesOf } from "./source-files.ts";
16
+ import { bytewise } from "./text/compare.ts";
16
17
 
17
18
  /**
18
19
  * The text of each string literal, or template literal with no substitution, written as
@@ -41,5 +42,5 @@ export function inOperandNames<Brand>(
41
42
  file.forEachChild(visit);
42
43
  }
43
44
  }
44
- return [...found].sort();
45
+ return [...found].sort(bytewise);
45
46
  }
package/src/index.ts CHANGED
@@ -19,4 +19,4 @@ export {
19
19
  type Fragment,
20
20
  type Module,
21
21
  } from "./ref.ts";
22
- export { CONTRACT_VERSION } from "./version.ts";
22
+ export { CONTRACT_VERSION } from "./contract/contract.generated.ts";
@@ -5,6 +5,7 @@
5
5
  * write to a local that no read reaches, and each case an earlier case covers.
6
6
  */
7
7
 
8
+ import { CODE } from "./contract/contract.generated.ts";
8
9
  import {
9
10
  isArrowFunction,
10
11
  isAwaitExpression,
@@ -95,6 +96,7 @@ import type { Reference } from "./references.ts";
95
96
  import type { ProjectView } from "./session.ts";
96
97
  import { signatureOf, type SignatureNode } from "./signatures.ts";
97
98
  import { spellsNoBinding } from "./spelled-names.ts";
99
+ import { parenthesesRemoved } from "./unwrap.ts";
98
100
 
99
101
  /** The subject kind of each part this module reads. */
100
102
  type PartKind = "parameter" | "result" | "statement" | "store" | "case";
@@ -179,13 +181,6 @@ export interface ProjectParts {
179
181
  readonly constructions: ReadonlySet<string>;
180
182
  }
181
183
 
182
- /** The codes of the parts this module reads. */
183
- export const UNUSED_PARAMETER = "DS1801";
184
- export const UNUSED_RESULT = "DS1803";
185
- const UNREACHABLE_STATEMENT = "DS1805";
186
- const DEAD_STORE = "DS1807";
187
- const UNREACHABLE_CASE = "DS1809";
188
-
189
184
  /** The result types that carry no value, as the checker spells them. */
190
185
  const NO_VALUE: ReadonlySet<string> = new Set([
191
186
  "void",
@@ -203,11 +198,6 @@ function spanOf(file: SourceFile, root: string, node: Node): FindingPosition {
203
198
  return { ...start, endLine };
204
199
  }
205
200
 
206
- /** The parenthesized expression's content, through every pair of parentheses. */
207
- function unwrapped(node: Node): Node {
208
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
209
- }
210
-
211
201
  /** The node the parentheses around one expression sit in. */
212
202
  function outerParent(node: Node): Node {
213
203
  return isParenthesizedExpression(node.parent) ? outerParent(node.parent) : node.parent;
@@ -215,7 +205,7 @@ function outerParent(node: Node): Node {
215
205
 
216
206
  /** The name a call is made through: the identifier, or the member a property access names. */
217
207
  function calleeName(call: CallExpression): Identifier | undefined {
218
- const callee = unwrapped(call.expression);
208
+ const callee = parenthesesRemoved(call.expression);
219
209
  if (isIdentifier(callee)) {
220
210
  return callee;
221
211
  }
@@ -282,7 +272,7 @@ function unreachableExempt(statement: Statement): boolean {
282
272
 
283
273
  /** The value a case expression is a literal of, as a key two equal literals share, or none. */
284
274
  function caseKey(expression: Node): string | undefined {
285
- const node = unwrapped(expression);
275
+ const node = parenthesesRemoved(expression);
286
276
  if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
287
277
  return `s:${node.text}`;
288
278
  }
@@ -408,7 +398,7 @@ function extendedNames(files: readonly SourceFile[]): ReadonlySet<string> {
408
398
  if (isTypeReferenceNode(type)) {
409
399
  named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
410
400
  } else {
411
- const expression = unwrapped(type.expression);
401
+ const expression = parenthesesRemoved(type.expression);
412
402
  named = isPropertyAccessExpression(expression) ? expression.name : expression;
413
403
  }
414
404
  if (isIdentifier(named)) {
@@ -461,7 +451,7 @@ function decoratorFixes(constructor: ConstructorDeclaration): boolean {
461
451
 
462
452
  /** The name an expression spells a class by: an identifier, or the member a property access names. */
463
453
  function spelledName(expression: Node): Identifier | undefined {
464
- const node = unwrapped(expression);
454
+ const node = parenthesesRemoved(expression);
465
455
  if (isIdentifier(node)) {
466
456
  return node;
467
457
  }
@@ -730,8 +720,8 @@ export function projectParts<Brand>(
730
720
  if ((isImportClause(node) || isNamespaceImport(node)) && node.name !== undefined) {
731
721
  specifiers.add(position(node.name));
732
722
  }
733
- if (isExportAssignment(node) && isIdentifier(unwrapped(node.expression))) {
734
- specifiers.add(position(unwrapped(node.expression)));
723
+ if (isExportAssignment(node) && isIdentifier(parenthesesRemoved(node.expression))) {
724
+ specifiers.add(position(parenthesesRemoved(node.expression)));
735
725
  }
736
726
  if (isSwitchStatement(node)) {
737
727
  const holder = holderOf(node);
@@ -869,7 +859,7 @@ export function projectParts<Brand>(
869
859
  }
870
860
  walk.catches.forEach(({ name, holder }, at) => {
871
861
  parts.push({
872
- code: DEAD_STORE,
862
+ code: CODE.deadStore,
873
863
  declaration: holder,
874
864
  kind: "store",
875
865
  name: name.text,
@@ -881,7 +871,7 @@ export function projectParts<Brand>(
881
871
  for (const { names, uses, holder } of loopVariables) {
882
872
  for (const name of names) {
883
873
  parts.push({
884
- code: DEAD_STORE,
874
+ code: CODE.deadStore,
885
875
  declaration: holder,
886
876
  kind: "store",
887
877
  name: name.text,
@@ -899,7 +889,7 @@ export function projectParts<Brand>(
899
889
  });
900
890
  for (const { statement, holder } of walk.unreachable) {
901
891
  parts.push({
902
- code: UNREACHABLE_STATEMENT,
892
+ code: CODE.unreachableStatement,
903
893
  declaration: holder,
904
894
  kind: "statement",
905
895
  name: "statement",
@@ -910,7 +900,7 @@ export function projectParts<Brand>(
910
900
  }
911
901
  for (const { clause, text, holder } of walk.cases) {
912
902
  parts.push({
913
- code: UNREACHABLE_CASE,
903
+ code: CODE.unreachableCase,
914
904
  declaration: holder,
915
905
  kind: "case",
916
906
  name: text,
@@ -1176,7 +1166,7 @@ function parameterParts(
1176
1166
  return;
1177
1167
  }
1178
1168
  const part: PartFact = {
1179
- code: UNUSED_PARAMETER,
1169
+ code: CODE.unusedParameter,
1180
1170
  declaration,
1181
1171
  kind: "parameter",
1182
1172
  name: name.text,
@@ -1331,7 +1321,7 @@ function isPlainWrite(use: Identifier): boolean {
1331
1321
  return (
1332
1322
  isBinaryExpression(parent) &&
1333
1323
  parent.operatorToken.kind === SyntaxKind.EqualsToken &&
1334
- unwrapped(parent.left) === use
1324
+ parenthesesRemoved(parent.left) === use
1335
1325
  );
1336
1326
  }
1337
1327
 
@@ -1353,7 +1343,7 @@ function deadStores(
1353
1343
  ): PartFact[] {
1354
1344
  const found: PartFact[] = [];
1355
1345
  const deadAt = (local: Local, target: Identifier): PartFact => ({
1356
- code: DEAD_STORE,
1346
+ code: CODE.deadStore,
1357
1347
  declaration: holder,
1358
1348
  kind: "store",
1359
1349
  name: local.name.text,
@@ -1388,14 +1378,14 @@ function deadStores(
1388
1378
  if (!isExpressionStatement(statement)) {
1389
1379
  return undefined;
1390
1380
  }
1391
- const expression = unwrapped(statement.expression);
1381
+ const expression = parenthesesRemoved(statement.expression);
1392
1382
  if (
1393
1383
  !isBinaryExpression(expression) ||
1394
1384
  expression.operatorToken.kind !== SyntaxKind.EqualsToken
1395
1385
  ) {
1396
1386
  return undefined;
1397
1387
  }
1398
- const target = unwrapped(expression.left);
1388
+ const target = parenthesesRemoved(expression.left);
1399
1389
  return isIdentifier(target) && uses.includes(target)
1400
1390
  ? { target, value: expression.right }
1401
1391
  : undefined;
@@ -1482,7 +1472,7 @@ function resultParts<Brand>(
1482
1472
  const valued = project.queries.nonNullable(result);
1483
1473
  const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
1484
1474
  found.push({
1485
- code: UNUSED_RESULT,
1475
+ code: CODE.unusedResult,
1486
1476
  declaration: id,
1487
1477
  kind: "result",
1488
1478
  name: "result",
package/src/inventory.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
51
51
  import { isAnswered, UNANSWERED } from "./query.ts";
52
52
  import type { Handle, ProjectView } from "./session.ts";
53
+ import { manifestAt } from "./fs/manifest.ts";
53
54
 
54
55
  /**
55
56
  * The symbol inventory of one project: every declaration the target's own files
@@ -233,8 +234,6 @@ interface ExportScope {
233
234
  readonly nameNode: Node;
234
235
  }
235
236
 
236
- const MANIFEST = "package.json";
237
-
238
237
  /** One manifest that names a package, and the directory below the root that holds it. */
239
238
  interface Named {
240
239
  readonly name: string;
@@ -255,24 +254,7 @@ export function packageScope(host: Host, root: string): (path: string) => Module
255
254
  const named = new Map<string, Named | undefined>();
256
255
 
257
256
  const manifestName = (dir: string): string | undefined => {
258
- let text: string;
259
- try {
260
- text = host.readFile(joinPath(root, dir, MANIFEST));
261
- } catch {
262
- return undefined;
263
- }
264
- let value: unknown;
265
- try {
266
- value = JSON.parse(text);
267
- } catch {
268
- // A manifest this program cannot read names no package, which is the same
269
- // answer as an absent one: the file is then scoped by the target root.
270
- return undefined;
271
- }
272
- if (typeof value !== "object" || value === null) {
273
- return undefined;
274
- }
275
- const name = (value as Record<string, unknown>)["name"];
257
+ const name = manifestAt(host, joinPath(root, dir))?.["name"];
276
258
  return typeof name === "string" && name !== "" ? name : undefined;
277
259
  };
278
260
 
@@ -128,3 +128,8 @@ export function positionAt(text: string, file: string, offset: number): Position
128
128
  const lineStart = before.lastIndexOf("\n") + 1;
129
129
  return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
130
130
  }
131
+
132
+ /** Whether a decoded JSON value is an object: not `null` and not an array. */
133
+ export function isRecord(value: unknown): value is Record<string, unknown> {
134
+ return typeof value === "object" && value !== null && !Array.isArray(value);
135
+ }
@@ -10,7 +10,6 @@ import {
10
10
  isCaseClause,
11
11
  isElementAccessExpression,
12
12
  isIdentifier,
13
- isParenthesizedExpression,
14
13
  isPropertyAccessExpression,
15
14
  isSwitchStatement,
16
15
  isTypeOperatorNode,
@@ -27,6 +26,7 @@ import { renderPosition } from "./position.ts";
27
26
  import { UNANSWERED } from "./query.ts";
28
27
  import type { Reference } from "./references.ts";
29
28
  import type { ProjectView } from "./session.ts";
29
+ import { parenthesesRemoved } from "./unwrap.ts";
30
30
 
31
31
  /** The operators that compare their operands for equality. */
32
32
  const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
@@ -44,13 +44,9 @@ interface Comparison {
44
44
  readonly other: Expression;
45
45
  }
46
46
 
47
- function unwrapped(node: Expression): Expression {
48
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
49
- }
50
-
51
47
  /** The name node an operand's symbol is read at: an identifier, or a property access's name. */
52
48
  function nameOf(node: Expression): Node | undefined {
53
- const at = unwrapped(node);
49
+ const at = parenthesesRemoved(node);
54
50
  if (isIdentifier(at)) {
55
51
  return at;
56
52
  }
@@ -132,7 +128,7 @@ export function keyofComparisonReferences<Brand>(
132
128
  }
133
129
  const named = comparisons.flatMap((one) => {
134
130
  const name = nameOf(one.keyed);
135
- return name === undefined || isElementAccessExpression(unwrapped(one.other))
131
+ return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
136
132
  ? []
137
133
  : [{ ...one, name }];
138
134
  });