@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
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Shell command lines read as text, never run: the commands a line runs, the tokens the
3
+ * Contract splits a command into, and the words POSIX token recognition reads.
4
+ */
5
+
6
+ /**
7
+ * The shell's list and pipeline operators, each of which ends one command. An `&` beside a
8
+ * redirection's `<` or `>` (`2>&1`, and bash's `&>log`) is part of the redirection.
9
+ */
10
+ const COMMAND_OPERATORS = /&&|\|\||;|\||(?<![<>])&(?!>)/u;
11
+
12
+ /** Where a command splits into tokens: white space and the operators the Contract names. */
13
+ const TOKEN_SEPARATORS = /\s+|&&|\|\||;|\|/u;
14
+
15
+ /** A text written inside one pair of quotes. */
16
+ const QUOTED = /^(["'])(.*)\1$/su;
17
+
18
+ /** The commands one line runs, the text between two of the shell's list and pipeline operators. */
19
+ export function commandsOf(line: string): readonly string[] {
20
+ return line.split(COMMAND_OPERATORS);
21
+ }
22
+
23
+ /** One text with one pair of enclosing quotes removed. */
24
+ export function unquoted(text: string): string {
25
+ return text.replace(QUOTED, "$2");
26
+ }
27
+
28
+ /**
29
+ * Every token one script's command line holds, each with one pair of enclosing quotes
30
+ * removed. A token holding a wildcard is a pattern rather than a path, and names no
31
+ * entry point.
32
+ */
33
+ export function scriptTokens(command: string): readonly string[] {
34
+ return command
35
+ .split(TOKEN_SEPARATORS)
36
+ .map(unquoted)
37
+ .filter((token) => token !== "" && !token.includes("*"));
38
+ }
39
+
40
+ /** One shell line up to its comment: a `#` that starts a word outside quotes. */
41
+ export function withoutComment(line: string): string {
42
+ let quote = "";
43
+ for (let at = 0; at < line.length; at += 1) {
44
+ const char = line[at];
45
+ if (quote !== "") {
46
+ if (char === quote) {
47
+ quote = "";
48
+ } else if (char === "\\" && quote === '"') {
49
+ at += 1;
50
+ }
51
+ } else if (char === "'" || char === '"') {
52
+ quote = char;
53
+ } else if (char === "\\") {
54
+ at += 1;
55
+ } else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
56
+ return line.slice(0, at);
57
+ }
58
+ }
59
+ return line;
60
+ }
61
+
62
+ /** The characters that end a word outside quotes and separate nothing: blanks. */
63
+ // Stryker disable next-line Regex: its \v to \V mutant is no valid escape under the u flag, so the whole module fails to parse
64
+ const BLANK = /[ \t\r\f\v]/u;
65
+
66
+ /** The characters that end a word outside quotes and begin an operator: newline included. */
67
+ const OPERATOR = /[\n;&|()<>]/u;
68
+
69
+ /** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
70
+ const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
71
+
72
+ /**
73
+ * The words of one command line, as POSIX token recognition reads quotes, backslashes,
74
+ * line continuations, comments and operators. A word an expansion may change (an unquoted
75
+ * `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
76
+ * is computed, and so is the rest of a line whose quote is never closed. A computed word
77
+ * and each operator are undefined, so a flag's file is never read across them.
78
+ */
79
+ export function shellWords(command: string): readonly (string | undefined)[] {
80
+ const words: (string | undefined)[] = [];
81
+ let word: string | undefined;
82
+ let computed = false;
83
+ const end = (): void => {
84
+ if (word !== undefined) {
85
+ words.push(computed ? undefined : word);
86
+ }
87
+ word = undefined;
88
+ computed = false;
89
+ };
90
+ for (let at = 0; at < command.length; at += 1) {
91
+ const char = command.charAt(at);
92
+ if (char === "\\" && command.charAt(at + 1) === "\n") {
93
+ at += 1;
94
+ } else if (BLANK.test(char)) {
95
+ end();
96
+ } else if (OPERATOR.test(char)) {
97
+ end();
98
+ words.push(undefined);
99
+ } else if (char === "#" && word === undefined) {
100
+ const newline = command.indexOf("\n", at);
101
+ at = newline < 0 ? command.length : newline - 1;
102
+ } else if (char === "\\") {
103
+ at += 1;
104
+ word = (word ?? "") + command.charAt(at);
105
+ } else if (char === "'" || char === '"') {
106
+ const close = closingQuote(command, at);
107
+ if (close < 0) {
108
+ word ??= "";
109
+ computed = true;
110
+ break;
111
+ }
112
+ const quoted =
113
+ char === "'"
114
+ ? { text: command.slice(at + 1, close), computed: false }
115
+ : doubleQuoted(command.slice(at + 1, close));
116
+ word = (word ?? "") + quoted.text;
117
+ computed ||= quoted.computed;
118
+ at = close;
119
+ } else {
120
+ computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
121
+ word = (word ?? "") + char;
122
+ }
123
+ }
124
+ end();
125
+ return words;
126
+ }
127
+
128
+ /** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
129
+ function doubleQuoted(inner: string): { readonly text: string; readonly computed: boolean } {
130
+ let text = "";
131
+ let computed = false;
132
+ for (let at = 0; at < inner.length; at += 1) {
133
+ const char = inner.charAt(at);
134
+ const next = inner.charAt(at + 1);
135
+ if (char === "\\" && next === "\n") {
136
+ at += 1;
137
+ } else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
138
+ text += next;
139
+ at += 1;
140
+ } else {
141
+ computed ||= char === "$" || char === "`";
142
+ text += char;
143
+ }
144
+ }
145
+ return { text, computed };
146
+ }
147
+
148
+ /** The index of the quote that closes the one at `open`, or -1 where none does. */
149
+ function closingQuote(command: string, open: number): number {
150
+ const quote = command.charAt(open);
151
+ for (let at = open + 1; at < command.length; at += 1) {
152
+ const char = command.charAt(at);
153
+ if (char === quote) {
154
+ return at;
155
+ }
156
+ if (quote === '"' && char === "\\") {
157
+ at += 1;
158
+ }
159
+ }
160
+ return -1;
161
+ }
@@ -6,6 +6,8 @@
6
6
  * as a JSON document that does not parse has none.
7
7
  */
8
8
 
9
+ import { isRecord } from "../json-document.ts";
10
+
9
11
  /** One line of the document with its comment removed, by its indentation and its content. */
10
12
  interface Line {
11
13
  readonly indent: number;
@@ -375,3 +377,47 @@ export function yamlDocument(text: string): unknown {
375
377
  throw error;
376
378
  }
377
379
  }
380
+
381
+ /** What one top-level key of a YAML document holds, read by {@link yamlKey}. */
382
+ type YamlKey =
383
+ | { readonly kind: "value"; readonly value: unknown }
384
+ | { readonly kind: "absent" }
385
+ | { readonly kind: "refused"; readonly reason: string };
386
+
387
+ /** The key one line of the document's top-level mapping opens, or undefined where it opens none. */
388
+ function topLevelKey(raw: string): string | undefined {
389
+ const content = withoutComment(raw).trimEnd();
390
+ const end = /^\s/u.test(content) ? -1 : keyEnd(content);
391
+ if (end < 0) {
392
+ return undefined;
393
+ }
394
+ try {
395
+ const key = scalar(content.slice(0, end));
396
+ return typeof key === "object" && key !== null ? undefined : String(key);
397
+ } catch {
398
+ return undefined;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * The value of one key of a document's top-level block mapping, read from that key's lines
404
+ * alone, so a document whose other keys lie outside the subset still yields it. A key
405
+ * declared twice, or one whose own lines lie outside the subset, is refused.
406
+ */
407
+ export function yamlKey(text: string, key: string): YamlKey {
408
+ const lines = text.replace(/^\uFEFF/u, "").split(/\r?\n/u);
409
+ const starts = lines.flatMap((line, at) => (topLevelKey(line) === undefined ? [] : [at]));
410
+ const named = starts.filter((at) => topLevelKey(lines[at] ?? "") === key);
411
+ const [start] = named;
412
+ if (start === undefined) {
413
+ return { kind: "absent" };
414
+ }
415
+ if (named.length > 1) {
416
+ return { kind: "refused", reason: `the ${key} key is declared more than once` };
417
+ }
418
+ const end = starts.find((at) => at > start) ?? lines.length;
419
+ const value = yamlDocument(lines.slice(start, end).join("\n"));
420
+ return isRecord(value)
421
+ ? { kind: "value", value: value[key] }
422
+ : { kind: "refused", reason: `the value of ${key} is outside the YAML subset read here` };
423
+ }
@@ -21,12 +21,21 @@ import { packageOfSpecifier } from "./dependencies.ts";
21
21
  import type { LocatedDiagnostic } from "./discover.ts";
22
22
  import type { Host } from "./host.ts";
23
23
  import { nodeKey, type Inventory } from "./inventory.ts";
24
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
24
+ import {
25
+ ancestors,
26
+ dirnamePath,
27
+ isAbsolutePath,
28
+ joinPath,
29
+ relativePath,
30
+ resolvePath,
31
+ } from "./paths.ts";
25
32
  import { isAnswered } from "./query.ts";
26
33
  import type { TypeErrorSkip } from "./report.ts";
27
34
  import type { Engine, ProjectView } from "./session.ts";
28
35
  import type { SetupFailure } from "./setup-failure.ts";
29
36
  import { findWorkspace, type Workspace } from "./workspace.ts";
37
+ import { entriesOf } from "./fs/tree.ts";
38
+ import { INSTALLED_PACKAGES, MANIFEST, manifestAt } from "./fs/manifest.ts";
30
39
 
31
40
  /** The compiler's codes for an import whose module it cannot find. */
32
41
  const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
@@ -38,7 +47,6 @@ const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
38
47
  */
39
48
  const CONFIGURATION_LEVEL: ReadonlySet<number> = new Set([5055, 5056, 6059, 6307]);
40
49
 
41
- const MANIFEST = "package.json";
42
50
  const MANIFEST_SECTIONS = [
43
51
  "dependencies",
44
52
  "devDependencies",
@@ -156,13 +164,9 @@ function provided(host: Host, path: string, fileOnly: boolean): boolean {
156
164
  }
157
165
  const dir = dirnamePath(path);
158
166
  const stem = `${path.slice(dir.length + 1)}.`;
159
- try {
160
- return host
161
- .readDirectory(dir)
162
- .some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
163
- } catch {
164
- return false;
165
- }
167
+ return entriesOf(host, dir).some(
168
+ (entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)),
169
+ );
166
170
  }
167
171
 
168
172
  /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
@@ -172,28 +176,18 @@ function declaringManifest(
172
176
  targetRoot: string,
173
177
  name: string,
174
178
  ): string | undefined {
175
- for (let at = dir; ; at = dirnamePath(at)) {
176
- const path = joinPath(at, MANIFEST);
177
- let manifest: unknown;
178
- try {
179
- manifest = JSON.parse(host.readFile(path)) as unknown;
180
- } catch {
181
- manifest = undefined;
182
- }
183
- if (typeof manifest === "object" && manifest !== null) {
184
- const sections = manifest as Record<string, unknown>;
185
- for (const section of MANIFEST_SECTIONS) {
186
- const declared = sections[section];
187
- if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
188
- return path;
189
- }
179
+ const chain = ancestors(dir, targetRoot);
180
+ // A directory outside the target root reads its own manifest alone.
181
+ for (const at of chain.length > 0 ? chain : [dir]) {
182
+ const manifest = manifestAt(host, at);
183
+ for (const section of MANIFEST_SECTIONS) {
184
+ const declared = manifest?.[section];
185
+ if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
186
+ return joinPath(at, MANIFEST);
190
187
  }
191
188
  }
192
- // The target root is not below itself, so the walk stops after reading its manifest.
193
- if (relativePath(targetRoot, at) === undefined) {
194
- return undefined;
195
- }
196
189
  }
190
+ return undefined;
197
191
  }
198
192
 
199
193
  /**
@@ -201,14 +195,9 @@ function declaringManifest(
201
195
  * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
202
196
  */
203
197
  function installed(host: Host, dir: string, name: string): boolean {
204
- for (let at = dir; ; at = dirnamePath(at)) {
205
- if (host.kindOf(joinPath(at, "node_modules", name)) === "directory") {
206
- return true;
207
- }
208
- if (dirnamePath(at) === at) {
209
- return false;
210
- }
211
- }
198
+ return ancestors(dir).some(
199
+ (at) => host.kindOf(joinPath(at, INSTALLED_PACKAGES, name)) === "directory",
200
+ );
212
201
  }
213
202
 
214
203
  /**
@@ -6,13 +6,10 @@
6
6
  */
7
7
 
8
8
  import {
9
- isAsExpression,
10
9
  isBinaryExpression,
11
10
  isCallExpression,
12
11
  isIdentifier,
13
12
  isNoSubstitutionTemplateLiteral,
14
- isParenthesizedExpression,
15
- isSatisfiesExpression,
16
13
  isStringLiteral,
17
14
  isTemplateExpression,
18
15
  SyntaxKind,
@@ -23,6 +20,7 @@ import { renderPosition, type Position } from "./position.ts";
23
20
  import { UNANSWERED } from "./query.ts";
24
21
  import type { ProjectView } from "./session.ts";
25
22
  import { sourceFilesOf } from "./source-files.ts";
23
+ import { unwrapped } from "./unwrap.ts";
26
24
 
27
25
  /** One load the analysis cannot resolve, in the configuration whose production file writes it. */
28
26
  export interface UnseenLoad {
@@ -31,15 +29,6 @@ export interface UnseenLoad {
31
29
  readonly position: Position;
32
30
  }
33
31
 
34
- /** The expression a wrapper that changes no value holds: parentheses and type assertions. */
35
- function unwrapped(node: Node): Node {
36
- let at = node;
37
- while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
38
- at = at.expression;
39
- }
40
- return at;
41
- }
42
-
43
32
  /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
44
33
  export function leadingLiteral(node: Node): string | undefined {
45
34
  const at = unwrapped(node);
package/src/unwrap.ts ADDED
@@ -0,0 +1,34 @@
1
+ import {
2
+ isAsExpression,
3
+ isNonNullExpression,
4
+ isParenthesizedExpression,
5
+ isSatisfiesExpression,
6
+ type Expression,
7
+ type Node,
8
+ } from "@typescript/native/ast";
9
+
10
+ /** The expression inside every pair of parentheses around `node`. */
11
+ export function parenthesesRemoved<T extends Node>(node: T): T | Expression {
12
+ let at: T | Expression = node;
13
+ while (isParenthesizedExpression(at)) {
14
+ at = at.expression;
15
+ }
16
+ return at;
17
+ }
18
+
19
+ /**
20
+ * The expression inside every wrapper around `node` that changes no value it evaluates to:
21
+ * parentheses, `as` and `satisfies` assertions and the non-null assertion.
22
+ */
23
+ export function unwrapped<T extends Node>(node: T): T | Expression {
24
+ let at: T | Expression = node;
25
+ while (
26
+ isParenthesizedExpression(at) ||
27
+ isAsExpression(at) ||
28
+ isSatisfiesExpression(at) ||
29
+ isNonNullExpression(at)
30
+ ) {
31
+ at = at.expression;
32
+ }
33
+ return at;
34
+ }
@@ -1,3 +1,9 @@
1
+ import {
2
+ CONTRACT_VERSION,
3
+ EXIT_CLEAN,
4
+ EXIT_FAILURE,
5
+ type Format,
6
+ } from "../contract/contract.generated.ts";
1
7
  import {
2
8
  runAnalysis,
3
9
  runBaselineWrite,
@@ -6,7 +12,7 @@ import {
6
12
  type RunAnalysis,
7
13
  type RunProject,
8
14
  } from "../analysis.ts";
9
- import { ConfigError, kindOf, type Format } from "../config.ts";
15
+ import { ConfigError, kindOf } from "../config.ts";
10
16
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
11
17
  import { moduleIdentity } from "../consumers.ts";
12
18
  import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
@@ -17,22 +23,23 @@ import type { NotBuilt } from "../discover.ts";
17
23
  import { partialNotes } from "../partial.ts";
18
24
  import {
19
25
  buildReport,
20
- LANGUAGE,
21
26
  capFindings,
22
27
  sortFindings,
23
- type Report,
24
28
  type ReportConfiguration,
25
29
  type ReportNotBuilt,
26
30
  type ReportNote,
27
31
  } from "../report.ts";
32
+ import type { Report } from "../contract/report.generated.ts";
33
+ import { reportRefusals } from "../report-schema.ts";
28
34
  import { json } from "../reporters/json.ts";
29
35
  import { RENDERINGS } from "../reporters/reporters.ts";
30
36
  import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
31
37
  import { resolve } from "../resolve.ts";
32
38
  import { otherSchemeRows, rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
33
39
  import type { UnseenLoad } from "../unseen-loads.ts";
34
- import { CONTRACT_VERSION } from "../version.ts";
35
- import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts";
40
+ import { type Verb, type VerbOptions } from "./verb.ts";
41
+ import { bytewise } from "../text/compare.ts";
42
+ import { diagnose, LANGUAGE, message } from "../product.ts";
36
43
 
37
44
  /** The options `analyze` takes beside the ones every verb takes. */
38
45
  export const ANALYZE_OPTIONS: VerbOptions = {
@@ -54,7 +61,7 @@ class ReportPathError extends Error {
54
61
 
55
62
  /** The formats this analyzer renders, for a message that names them. */
56
63
  function formatNames(): string {
57
- return [...RENDERINGS.keys()].sort().join(", ");
64
+ return [...RENDERINGS.keys()].sort(bytewise).join(", ");
58
65
  }
59
66
 
60
67
  /** The renderings one invocation writes: the ones it named, else the configuration's. */
@@ -194,11 +201,7 @@ function templateOf(
194
201
  try {
195
202
  source = host.readFile(resolvePath(invoked, named));
196
203
  } catch (error: unknown) {
197
- throw new ConfigError(
198
- "malformed",
199
- "",
200
- `--template=${named}: ${error instanceof Error ? error.message : String(error)}`,
201
- );
204
+ throw new ConfigError("malformed", "", `--template=${named}: ${message(error)}`);
202
205
  }
203
206
  try {
204
207
  return parseTemplate(source);
@@ -262,7 +265,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
262
265
  try {
263
266
  version = host.analyzerVersion();
264
267
  } catch (error: unknown) {
265
- err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
268
+ diagnose(err, message(error));
266
269
  return EXIT_FAILURE;
267
270
  }
268
271
 
@@ -343,11 +346,17 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
343
346
  if (!(error instanceof ReportPathError)) {
344
347
  throw error;
345
348
  }
346
- err.write(`deadset-ts: ${error.message}\n`);
349
+ diagnose(err, error.message);
347
350
  return EXIT_FAILURE;
348
351
  }
349
352
 
350
353
  try {
354
+ const [refused] = reportRefusals(report);
355
+ if (refused !== undefined) {
356
+ throw new Error(
357
+ `the report breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`,
358
+ );
359
+ }
351
360
  host.writeDocument(documents.report, json(report));
352
361
  const options = {
353
362
  readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
@@ -370,38 +379,42 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
370
379
  );
371
380
  }
372
381
  } catch (error: unknown) {
373
- err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
382
+ diagnose(err, message(error));
374
383
  return EXIT_FAILURE;
375
384
  }
376
385
 
377
386
  for (const note of partial) {
378
- err.write(`deadset-ts: ${note}\n`);
387
+ diagnose(err, note);
379
388
  }
380
389
  for (const line of [...unread, ...unfound]) {
381
- err.write(`deadset-ts: ${line}\n`);
390
+ diagnose(err, line);
382
391
  }
383
392
  for (const warning of warnings) {
384
- err.write(
385
- `deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`,
393
+ diagnose(
394
+ err,
395
+ `${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}`,
386
396
  );
387
397
  }
388
398
  for (const skip of report.type_error_skips) {
389
- err.write(
390
- `deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`,
399
+ diagnose(
400
+ err,
401
+ `${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated`,
391
402
  );
392
403
  }
393
404
  if (report.totals.pending > 0) {
394
- err.write(
395
- `deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
405
+ diagnose(
406
+ err,
407
+ `${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it`,
396
408
  );
397
409
  }
398
410
  if (report.totals.stale_suppressions > 0) {
399
- err.write(`deadset-ts: ${counted(report.totals.stale_suppressions, "stale suppression")}\n`);
411
+ diagnose(err, counted(report.totals.stale_suppressions, "stale suppression"));
400
412
  }
401
413
  if (exitCode === EXIT_CODE_OFF) {
402
414
  if (verdict !== EXIT_CLEAN) {
403
- err.write(
404
- `deadset-ts: the exit code is configured off: the verdict of this run is ${String(verdict)}\n`,
415
+ diagnose(
416
+ err,
417
+ `the exit code is configured off: the verdict of this run is ${String(verdict)}`,
405
418
  );
406
419
  }
407
420
  return EXIT_CLEAN;
@@ -1,7 +1,13 @@
1
+ import {
2
+ CONTRACT_VERSION,
3
+ EXIT_CLEAN,
4
+ EXIT_FAILURE,
5
+ EXIT_USAGE,
6
+ SCHEMA_VERSIONS,
7
+ } from "../contract/contract.generated.ts";
1
8
  import { CONFORMANCE, type Conformance } from "../conformance.ts";
2
- import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSIONS_ACCEPTED } from "../report.ts";
3
- import { CONTRACT_VERSION } from "../version.ts";
4
- import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
9
+ import { type Verb } from "./verb.ts";
10
+ import { diagnose, LANGUAGE, message, PRODUCT } from "../product.ts";
5
11
 
6
12
  /**
7
13
  * The document `describe` writes: the analyzer, the Contract version it implements, the
@@ -10,10 +16,10 @@ import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
10
16
  */
11
17
  export function describeDocument(version: string, recorded: Conformance | undefined): string {
12
18
  const document = {
13
- name: ANALYZER_NAME,
19
+ name: PRODUCT,
14
20
  version,
15
21
  contract_version: CONTRACT_VERSION,
16
- schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
22
+ schema_versions_accepted: SCHEMA_VERSIONS,
17
23
  languages: [LANGUAGE],
18
24
  ...(recorded === undefined
19
25
  ? {}
@@ -31,14 +37,14 @@ export function describeDocument(version: string, recorded: Conformance | undefi
31
37
  /** Writes this analyzer's description to the output stream, and nothing to the error stream. */
32
38
  export const describeVerb: Verb = ({ out, err, host, args }) => {
33
39
  if (args.length > 0) {
34
- err.write(`deadset-ts: describe takes no argument, got ${JSON.stringify(args[0])}\n`);
40
+ diagnose(err, `describe takes no argument, got ${JSON.stringify(args[0])}`);
35
41
  return EXIT_USAGE;
36
42
  }
37
43
  let version: string;
38
44
  try {
39
45
  version = host.analyzerVersion();
40
46
  } catch (error: unknown) {
41
- err.write(`deadset-ts: describe: ${error instanceof Error ? error.message : String(error)}\n`);
47
+ diagnose(err, `describe: ${message(error)}`);
42
48
  return EXIT_FAILURE;
43
49
  }
44
50
  out.write(describeDocument(version, CONFORMANCE));
@@ -1,3 +1,4 @@
1
+ import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.ts";
1
2
  import { runAnalysis, type RunAnalysis } from "../analysis.ts";
2
3
  import { listing } from "../components.ts";
3
4
  import type { Cascade } from "../config.ts";
@@ -12,7 +13,9 @@ import { positionKey } from "../position.ts";
12
13
  import type { Reference } from "../references.ts";
13
14
  import { resolve } from "../resolve.ts";
14
15
  import type { Root } from "../roots.ts";
15
- import { EXIT_CLEAN, EXIT_USAGE, type Verb, type VerbOptions } from "./verb.ts";
16
+ import { type Verb, type VerbOptions } from "./verb.ts";
17
+ import { bytewise } from "../text/compare.ts";
18
+ import { diagnose } from "../product.ts";
16
19
 
17
20
  /**
18
21
  * The three spellings of an explanation request, each naming the symbol to explain. They are
@@ -101,7 +104,7 @@ function subjectOf(
101
104
  symbol.ref.toLowerCase().includes(folded) || symbol.name.toLowerCase().includes(folded),
102
105
  )
103
106
  .map(described)
104
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
107
+ .sort(bytewise);
105
108
  return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
106
109
  }
107
110
 
@@ -380,7 +383,7 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
380
383
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
381
384
  });
382
385
  if (subject === undefined) {
383
- err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
386
+ diagnose(err, `${JSON.stringify(named)} names no one symbol of the target`);
384
387
  for (const candidate of candidates) {
385
388
  err.write(` ${candidate}\n`);
386
389
  }
@@ -391,7 +394,7 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
391
394
  out.write(explanation(analysis, subject, config.reporters.cascade));
392
395
  }
393
396
  for (const note of notes) {
394
- err.write(`deadset-ts: ${note}\n`);
397
+ diagnose(err, note);
395
398
  }
396
399
  return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
397
400
  };
@@ -1,6 +1,7 @@
1
+ import { EXIT_CLEAN } from "../contract/contract.generated.ts";
1
2
  import { printConfig } from "../print.ts";
2
3
  import { resolve } from "../resolve.ts";
3
- import { EXIT_CLEAN, type Verb } from "./verb.ts";
4
+ import { type Verb } from "./verb.ts";
4
5
 
5
6
  /** Writes the resolved configuration with the source of every setting. */
6
7
  export const printConfigVerb: Verb = ({ out, inputs }) => {
@@ -1,8 +1,10 @@
1
+ import { EXIT_CLEAN } from "../contract/contract.generated.ts";
1
2
  import { runProjects } from "../analysis.ts";
2
3
  import { partialNotes } from "../partial.ts";
3
4
  import { resolvePath } from "../paths.ts";
4
5
  import { resolveUnkinded } from "../resolve.ts";
5
- import { EXIT_CLEAN, type Verb } from "./verb.ts";
6
+ import { type Verb } from "./verb.ts";
7
+ import { diagnose } from "../product.ts";
6
8
 
7
9
  /**
8
10
  * Writes the name of every project one run analyzes, one per line: the identifier the
@@ -23,7 +25,7 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
23
25
  unanswered: answer.unanswered,
24
26
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
25
27
  })) {
26
- err.write(`deadset-ts: ${note}\n`);
28
+ diagnose(err, note);
27
29
  }
28
30
  return EXIT_CLEAN;
29
31
  };