@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
@@ -5,6 +5,7 @@
5
5
  * alias, a tag, a block scalar, a complex key or a second document, has no value here,
6
6
  * as a JSON document that does not parse has none.
7
7
  */
8
+ import { isRecord } from "../json-document.js";
8
9
  /** A failure to read the document within the subset; it ends the whole reading. */
9
10
  class Unsupported extends Error {
10
11
  }
@@ -353,3 +354,40 @@ export function yamlDocument(text) {
353
354
  throw error;
354
355
  }
355
356
  }
357
+ /** The key one line of the document's top-level mapping opens, or undefined where it opens none. */
358
+ function topLevelKey(raw) {
359
+ const content = withoutComment(raw).trimEnd();
360
+ const end = /^\s/u.test(content) ? -1 : keyEnd(content);
361
+ if (end < 0) {
362
+ return undefined;
363
+ }
364
+ try {
365
+ const key = scalar(content.slice(0, end));
366
+ return typeof key === "object" && key !== null ? undefined : String(key);
367
+ }
368
+ catch {
369
+ return undefined;
370
+ }
371
+ }
372
+ /**
373
+ * The value of one key of a document's top-level block mapping, read from that key's lines
374
+ * alone, so a document whose other keys lie outside the subset still yields it. A key
375
+ * declared twice, or one whose own lines lie outside the subset, is refused.
376
+ */
377
+ export function yamlKey(text, key) {
378
+ const lines = text.replace(/^\uFEFF/u, "").split(/\r?\n/u);
379
+ const starts = lines.flatMap((line, at) => (topLevelKey(line) === undefined ? [] : [at]));
380
+ const named = starts.filter((at) => topLevelKey(lines[at] ?? "") === key);
381
+ const [start] = named;
382
+ if (start === undefined) {
383
+ return { kind: "absent" };
384
+ }
385
+ if (named.length > 1) {
386
+ return { kind: "refused", reason: `the ${key} key is declared more than once` };
387
+ }
388
+ const end = starts.find((at) => at > start) ?? lines.length;
389
+ const value = yamlDocument(lines.slice(start, end).join("\n"));
390
+ return isRecord(value)
391
+ ? { kind: "value", value: value[key] }
392
+ : { kind: "refused", reason: `the value of ${key} is outside the YAML subset read here` };
393
+ }
@@ -8,9 +8,11 @@
8
8
  import { isClassDeclaration, isFunctionLikeDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
9
9
  import { packageOfSpecifier } from "./dependencies.js";
10
10
  import { nodeKey } from "./inventory.js";
11
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
11
+ import { ancestors, dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath, } from "./paths.js";
12
12
  import { isAnswered } from "./query.js";
13
13
  import { findWorkspace } from "./workspace.js";
14
+ import { entriesOf } from "./fs/tree.js";
15
+ import { INSTALLED_PACKAGES, MANIFEST, manifestAt } from "./fs/manifest.js";
14
16
  /** The compiler's codes for an import whose module it cannot find. */
15
17
  const MODULE_NOT_FOUND = new Set([2307, 2792]);
16
18
  /**
@@ -19,7 +21,6 @@ const MODULE_NOT_FOUND = new Set([2307, 2792]);
19
21
  * 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
20
22
  */
21
23
  const CONFIGURATION_LEVEL = new Set([5055, 5056, 6059, 6307]);
22
- const MANIFEST = "package.json";
23
24
  const MANIFEST_SECTIONS = [
24
25
  "dependencies",
25
26
  "devDependencies",
@@ -85,54 +86,29 @@ function provided(host, path, fileOnly) {
85
86
  }
86
87
  const dir = dirnamePath(path);
87
88
  const stem = `${path.slice(dir.length + 1)}.`;
88
- try {
89
- return host
90
- .readDirectory(dir)
91
- .some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
92
- }
93
- catch {
94
- return false;
95
- }
89
+ return entriesOf(host, dir).some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
96
90
  }
97
91
  /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
98
92
  function declaringManifest(host, dir, targetRoot, name) {
99
- for (let at = dir;; at = dirnamePath(at)) {
100
- const path = joinPath(at, MANIFEST);
101
- let manifest;
102
- try {
103
- manifest = JSON.parse(host.readFile(path));
104
- }
105
- catch {
106
- manifest = undefined;
107
- }
108
- if (typeof manifest === "object" && manifest !== null) {
109
- const sections = manifest;
110
- for (const section of MANIFEST_SECTIONS) {
111
- const declared = sections[section];
112
- if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
113
- return path;
114
- }
93
+ const chain = ancestors(dir, targetRoot);
94
+ // A directory outside the target root reads its own manifest alone.
95
+ for (const at of chain.length > 0 ? chain : [dir]) {
96
+ const manifest = manifestAt(host, at);
97
+ for (const section of MANIFEST_SECTIONS) {
98
+ const declared = manifest?.[section];
99
+ if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
100
+ return joinPath(at, MANIFEST);
115
101
  }
116
102
  }
117
- // The target root is not below itself, so the walk stops after reading its manifest.
118
- if (relativePath(targetRoot, at) === undefined) {
119
- return undefined;
120
- }
121
103
  }
104
+ return undefined;
122
105
  }
123
106
  /**
124
107
  * Whether a package is installed for an importer in `dir`: in the `node_modules` of
125
108
  * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
126
109
  */
127
110
  function installed(host, dir, name) {
128
- for (let at = dir;; at = dirnamePath(at)) {
129
- if (host.kindOf(joinPath(at, "node_modules", name)) === "directory") {
130
- return true;
131
- }
132
- if (dirnamePath(at) === at) {
133
- return false;
134
- }
135
- }
111
+ return ancestors(dir).some((at) => host.kindOf(joinPath(at, INSTALLED_PACKAGES, name)) === "directory");
136
112
  }
137
113
  /**
138
114
  * Why one import that the compiler cannot resolve names a module the project expects,
@@ -4,19 +4,12 @@
4
4
  * naming a directory below the calling file's. The files such a load runs are roots the
5
5
  * analysis does not see, so the configuration's roots are never complete.
6
6
  */
7
- import { isAsExpression, isBinaryExpression, isCallExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
7
+ import { isBinaryExpression, isCallExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
8
8
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
9
9
  import { renderPosition } from "./position.js";
10
10
  import { UNANSWERED } from "./query.js";
11
11
  import { sourceFilesOf } from "./source-files.js";
12
- /** The expression a wrapper that changes no value holds: parentheses and type assertions. */
13
- function unwrapped(node) {
14
- let at = node;
15
- while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
16
- at = at.expression;
17
- }
18
- return at;
19
- }
12
+ import { unwrapped } from "./unwrap.js";
20
13
  /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
21
14
  export function leadingLiteral(node) {
22
15
  const at = unwrapped(node);
@@ -0,0 +1,23 @@
1
+ import { isAsExpression, isNonNullExpression, isParenthesizedExpression, isSatisfiesExpression, } from "@typescript/native/ast";
2
+ /** The expression inside every pair of parentheses around `node`. */
3
+ export function parenthesesRemoved(node) {
4
+ let at = node;
5
+ while (isParenthesizedExpression(at)) {
6
+ at = at.expression;
7
+ }
8
+ return at;
9
+ }
10
+ /**
11
+ * The expression inside every wrapper around `node` that changes no value it evaluates to:
12
+ * parentheses, `as` and `satisfies` assertions and the non-null assertion.
13
+ */
14
+ export function unwrapped(node) {
15
+ let at = node;
16
+ while (isParenthesizedExpression(at) ||
17
+ isAsExpression(at) ||
18
+ isSatisfiesExpression(at) ||
19
+ isNonNullExpression(at)) {
20
+ at = at.expression;
21
+ }
22
+ return at;
23
+ }
@@ -1,3 +1,4 @@
1
+ import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, } from "../contract/contract.generated.js";
1
2
  import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
2
3
  import { ConfigError, kindOf } from "../config.js";
3
4
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
@@ -5,14 +6,16 @@ import { moduleIdentity } from "../consumers.js";
5
6
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
7
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
8
  import { partialNotes } from "../partial.js";
8
- import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
9
+ import { buildReport, capFindings, sortFindings, } from "../report.js";
10
+ import { reportRefusals } from "../report-schema.js";
9
11
  import { json } from "../reporters/json.js";
10
12
  import { RENDERINGS } from "../reporters/reporters.js";
11
13
  import { parseTemplate, TemplateError } from "../reporters/template.js";
12
14
  import { resolve } from "../resolve.js";
13
15
  import { otherSchemeRows, rowReason, writeBaseline } from "../suppress-file.js";
14
- import { CONTRACT_VERSION } from "../version.js";
15
- import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
16
+ import {} from "./verb.js";
17
+ import { bytewise } from "../text/compare.js";
18
+ import { diagnose, LANGUAGE, message } from "../product.js";
16
19
  /** The options `analyze` takes beside the ones every verb takes. */
17
20
  export const ANALYZE_OPTIONS = {
18
21
  plain: ["report", "baseline-write", "exit-code", "template"],
@@ -30,7 +33,7 @@ class ReportPathError extends Error {
30
33
  }
31
34
  /** The formats this analyzer renders, for a message that names them. */
32
35
  function formatNames() {
33
- return [...RENDERINGS.keys()].sort().join(", ");
36
+ return [...RENDERINGS.keys()].sort(bytewise).join(", ");
34
37
  }
35
38
  /** The renderings one invocation writes: the ones it named, else the configuration's. */
36
39
  function formatsOf(named, configured) {
@@ -136,7 +139,7 @@ function templateOf(host, invoked, named, formats) {
136
139
  source = host.readFile(resolvePath(invoked, named));
137
140
  }
138
141
  catch (error) {
139
- throw new ConfigError("malformed", "", `--template=${named}: ${error instanceof Error ? error.message : String(error)}`);
142
+ throw new ConfigError("malformed", "", `--template=${named}: ${message(error)}`);
140
143
  }
141
144
  try {
142
145
  return parseTemplate(source);
@@ -185,7 +188,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
185
188
  version = host.analyzerVersion();
186
189
  }
187
190
  catch (error) {
188
- err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
191
+ diagnose(err, message(error));
189
192
  return EXIT_FAILURE;
190
193
  }
191
194
  let report;
@@ -253,10 +256,14 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
253
256
  if (!(error instanceof ReportPathError)) {
254
257
  throw error;
255
258
  }
256
- err.write(`deadset-ts: ${error.message}\n`);
259
+ diagnose(err, error.message);
257
260
  return EXIT_FAILURE;
258
261
  }
259
262
  try {
263
+ const [refused] = reportRefusals(report);
264
+ if (refused !== undefined) {
265
+ throw new Error(`the report breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`);
266
+ }
260
267
  host.writeDocument(documents.report, json(report));
261
268
  const options = {
262
269
  readSource: (path) => host.readFile(joinPath(targetRoot, path)),
@@ -276,30 +283,30 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
276
283
  }
277
284
  }
278
285
  catch (error) {
279
- err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
286
+ diagnose(err, message(error));
280
287
  return EXIT_FAILURE;
281
288
  }
282
289
  for (const note of partial) {
283
- err.write(`deadset-ts: ${note}\n`);
290
+ diagnose(err, note);
284
291
  }
285
292
  for (const line of [...unread, ...unfound]) {
286
- err.write(`deadset-ts: ${line}\n`);
293
+ diagnose(err, line);
287
294
  }
288
295
  for (const warning of warnings) {
289
- err.write(`deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`);
296
+ diagnose(err, `${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}`);
290
297
  }
291
298
  for (const skip of report.type_error_skips) {
292
- err.write(`deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`);
299
+ diagnose(err, `${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated`);
293
300
  }
294
301
  if (report.totals.pending > 0) {
295
- err.write(`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`);
302
+ diagnose(err, `${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it`);
296
303
  }
297
304
  if (report.totals.stale_suppressions > 0) {
298
- err.write(`deadset-ts: ${counted(report.totals.stale_suppressions, "stale suppression")}\n`);
305
+ diagnose(err, counted(report.totals.stale_suppressions, "stale suppression"));
299
306
  }
300
307
  if (exitCode === EXIT_CODE_OFF) {
301
308
  if (verdict !== EXIT_CLEAN) {
302
- err.write(`deadset-ts: the exit code is configured off: the verdict of this run is ${String(verdict)}\n`);
309
+ diagnose(err, `the exit code is configured off: the verdict of this run is ${String(verdict)}`);
303
310
  }
304
311
  return EXIT_CLEAN;
305
312
  }
@@ -1,7 +1,7 @@
1
+ import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
1
2
  import { CONFORMANCE } from "../conformance.js";
2
- import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSIONS_ACCEPTED } from "../report.js";
3
- import { CONTRACT_VERSION } from "../version.js";
4
- import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
3
+ import {} from "./verb.js";
4
+ import { diagnose, LANGUAGE, message, PRODUCT } from "../product.js";
5
5
  /**
6
6
  * The document `describe` writes: the analyzer, the Contract version it implements, the
7
7
  * report schema versions it reads, the languages it claims, and its recorded result over
@@ -9,10 +9,10 @@ import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
9
9
  */
10
10
  export function describeDocument(version, recorded) {
11
11
  const document = {
12
- name: ANALYZER_NAME,
12
+ name: PRODUCT,
13
13
  version,
14
14
  contract_version: CONTRACT_VERSION,
15
- schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
15
+ schema_versions_accepted: SCHEMA_VERSIONS,
16
16
  languages: [LANGUAGE],
17
17
  ...(recorded === undefined
18
18
  ? {}
@@ -29,7 +29,7 @@ export function describeDocument(version, recorded) {
29
29
  /** Writes this analyzer's description to the output stream, and nothing to the error stream. */
30
30
  export const describeVerb = ({ out, err, host, args }) => {
31
31
  if (args.length > 0) {
32
- err.write(`deadset-ts: describe takes no argument, got ${JSON.stringify(args[0])}\n`);
32
+ diagnose(err, `describe takes no argument, got ${JSON.stringify(args[0])}`);
33
33
  return EXIT_USAGE;
34
34
  }
35
35
  let version;
@@ -37,7 +37,7 @@ export const describeVerb = ({ out, err, host, args }) => {
37
37
  version = host.analyzerVersion();
38
38
  }
39
39
  catch (error) {
40
- err.write(`deadset-ts: describe: ${error instanceof Error ? error.message : String(error)}\n`);
40
+ diagnose(err, `describe: ${message(error)}`);
41
41
  return EXIT_FAILURE;
42
42
  }
43
43
  out.write(describeDocument(version, CONFORMANCE));
@@ -1,3 +1,4 @@
1
+ import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
1
2
  import { runAnalysis } from "../analysis.js";
2
3
  import { listing } from "../components.js";
3
4
  import { ConfigError } from "../config.js";
@@ -7,7 +8,9 @@ import { partialNotes } from "../partial.js";
7
8
  import { resolvePath } from "../paths.js";
8
9
  import { positionKey } from "../position.js";
9
10
  import { resolve } from "../resolve.js";
10
- import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
11
+ import {} from "./verb.js";
12
+ import { bytewise } from "../text/compare.js";
13
+ import { diagnose } from "../product.js";
11
14
  /**
12
15
  * The three spellings of an explanation request, each naming the symbol to explain. They are
13
16
  * one request, because exactly one state holds for a symbol and the answer is that state.
@@ -73,7 +76,7 @@ function subjectOf(symbols, named) {
73
76
  const partial = symbols
74
77
  .filter((symbol) => symbol.ref.toLowerCase().includes(folded) || symbol.name.toLowerCase().includes(folded))
75
78
  .map(described)
76
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
79
+ .sort(bytewise);
77
80
  return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
78
81
  }
79
82
  /** Every reference of the run once, a reference several configurations make counted once. */
@@ -317,7 +320,7 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
317
320
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
318
321
  });
319
322
  if (subject === undefined) {
320
- err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
323
+ diagnose(err, `${JSON.stringify(named)} names no one symbol of the target`);
321
324
  for (const candidate of candidates) {
322
325
  err.write(` ${candidate}\n`);
323
326
  }
@@ -329,7 +332,7 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
329
332
  out.write(explanation(analysis, subject, config.reporters.cascade));
330
333
  }
331
334
  for (const note of notes) {
332
- err.write(`deadset-ts: ${note}\n`);
335
+ diagnose(err, note);
333
336
  }
334
337
  return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
335
338
  };
@@ -1,6 +1,7 @@
1
+ import { EXIT_CLEAN } from "../contract/contract.generated.js";
1
2
  import { printConfig } from "../print.js";
2
3
  import { resolve } from "../resolve.js";
3
- import { EXIT_CLEAN } from "./verb.js";
4
+ import {} from "./verb.js";
4
5
  /** Writes the resolved configuration with the source of every setting. */
5
6
  export const printConfigVerb = ({ out, inputs }) => {
6
7
  const { config, provenance } = resolve(inputs());
@@ -1,8 +1,10 @@
1
+ import { EXIT_CLEAN } from "../contract/contract.generated.js";
1
2
  import { runProjects } from "../analysis.js";
2
3
  import { partialNotes } from "../partial.js";
3
4
  import { resolvePath } from "../paths.js";
4
5
  import { resolveUnkinded } from "../resolve.js";
5
- import { EXIT_CLEAN } from "./verb.js";
6
+ import {} from "./verb.js";
7
+ import { diagnose } from "../product.js";
6
8
  /**
7
9
  * Writes the name of every project one run analyzes, one per line: the identifier the
8
10
  * build matrix declares for it, or, where discovery derived the project, its
@@ -22,7 +24,7 @@ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient })
22
24
  unanswered: answer.unanswered,
23
25
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
24
26
  })) {
25
- err.write(`deadset-ts: ${note}\n`);
27
+ diagnose(err, note);
26
28
  }
27
29
  return EXIT_CLEAN;
28
30
  };
@@ -1,9 +1,11 @@
1
+ import { EXIT_CLEAN } from "../contract/contract.generated.js";
1
2
  import { runSweep } from "../analysis.js";
2
3
  import { retainedLines } from "../exempt.js";
3
4
  import { partialNotes } from "../partial.js";
4
5
  import { resolvePath } from "../paths.js";
5
6
  import { resolve } from "../resolve.js";
6
- import { EXIT_CLEAN } from "./verb.js";
7
+ import {} from "./verb.js";
8
+ import { diagnose } from "../product.js";
7
9
  /**
8
10
  * Writes every declaration an exemption held back in the production sweep a report is
9
11
  * built from, one per line, and nothing where no exemption held one back. One fact
@@ -26,7 +28,7 @@ export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient })
26
28
  unanswered: swept.unanswered,
27
29
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
28
30
  })) {
29
- err.write(`deadset-ts: ${note}\n`);
31
+ diagnose(err, note);
30
32
  }
31
33
  return EXIT_CLEAN;
32
34
  };
@@ -1,8 +1,10 @@
1
+ import { EXIT_CLEAN, EXIT_FINDINGS } from "../contract/contract.generated.js";
1
2
  import { runRoots } from "../analysis.js";
2
3
  import { partialNotes } from "../partial.js";
3
4
  import { resolvePath } from "../paths.js";
4
5
  import { resolve } from "../resolve.js";
5
- import { EXIT_CLEAN, EXIT_FINDINGS } from "./verb.js";
6
+ import {} from "./verb.js";
7
+ import { diagnose } from "../product.js";
6
8
  /**
7
9
  * One root as a line: the declaration's reference, why it is a root, and the string
8
10
  * that named it, separated by tabs. A run of several configurations adds the ones
@@ -39,7 +41,7 @@ export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) =>
39
41
  unanswered: answer.unanswered,
40
42
  targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
41
43
  })) {
42
- err.write(`deadset-ts: ${note}\n`);
44
+ diagnose(err, note);
43
45
  }
44
46
  return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
45
47
  };
@@ -1,5 +1 @@
1
- /** The exit codes the Contract's exit-code table names. */
2
- export const EXIT_CLEAN = 0;
3
- export const EXIT_FINDINGS = 1;
4
- export const EXIT_USAGE = 2;
5
- export const EXIT_FAILURE = 3;
1
+ export {};
@@ -1,7 +1,8 @@
1
- import { CONTRACT_VERSION } from "../version.js";
2
- import { EXIT_CLEAN } from "./verb.js";
1
+ import { CONTRACT_VERSION, EXIT_CLEAN } from "../contract/contract.generated.js";
2
+ import {} from "./verb.js";
3
+ import { PRODUCT } from "../product.js";
3
4
  /** Writes the version the host reports and the Contract version this analyzer implements. */
4
5
  export const versionVerb = ({ out, host }) => {
5
- out.write(`deadset-ts ${host.analyzerVersion()}\ncontract ${CONTRACT_VERSION}\n`);
6
+ out.write(`${PRODUCT} ${host.analyzerVersion()}\ncontract ${CONTRACT_VERSION}\n`);
6
7
  return EXIT_CLEAN;
7
8
  };
@@ -3,8 +3,9 @@
3
3
  * path from the target root. A workflow or hook file is read as text, line by line, with no
4
4
  * YAML parsed.
5
5
  */
6
- import { scriptTokens } from "./manifest.js";
7
6
  import { joinPath, relativePath, resolvePath } from "./paths.js";
7
+ import { filesIn } from "./fs/tree.js";
8
+ import { commandsOf, scriptTokens, unquoted, withoutComment } from "./text/shell.js";
8
9
  const WORKFLOWS = ".github/workflows";
9
10
  /** A line holding a run command: indentation and one sequence marker before `run:`. */
10
11
  const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
@@ -19,26 +20,13 @@ const HOOK_MANAGERS = [
19
20
  ];
20
21
  /** The names GNU make reads a makefile under, in the order it tries them. */
21
22
  const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"];
22
- /** The operators that end one shell command of a recipe line. */
23
- const COMMAND_SEPARATORS = /&&|\|\||;|\|/u;
24
23
  const BLOCK_SCALAR = /^[|>][+-]?$/u;
25
- const QUOTED = /^(["'])(.*)\1$/su;
26
24
  /** A token holding one of these is computed or a pattern, so it names no file. */
27
25
  const COMPUTED = /[${*?[]/u;
28
26
  const INDENT = /^\s*/u;
29
- /** The files directly in one directory whose name `accepts`, in name order. */
30
- function filesIn(host, dir, accepts) {
31
- let entries;
32
- try {
33
- entries = host.readDirectory(dir);
34
- }
35
- catch {
36
- return [];
37
- }
38
- return entries
39
- .filter((entry) => !entry.directory && accepts(entry.name))
40
- .map((entry) => joinPath(dir, entry.name))
41
- .sort();
27
+ /** The files directly in one directory whose name `accepts`, by path in name order. */
28
+ function pathsIn(host, dir, accepts) {
29
+ return filesIn(host, dir, accepts).map((name) => joinPath(dir, name));
42
30
  }
43
31
  function readText(host, path) {
44
32
  try {
@@ -58,7 +46,7 @@ export function runCommands(text, key = RUN_LINE) {
58
46
  continue;
59
47
  }
60
48
  const column = (match[1] ?? "").length;
61
- const value = (match[2] ?? "").trim().replace(QUOTED, "$2");
49
+ const value = unquoted((match[2] ?? "").trim());
62
50
  if (!BLOCK_SCALAR.test(value)) {
63
51
  found.push(value);
64
52
  continue;
@@ -88,7 +76,7 @@ function recipeLines(host, root) {
88
76
  .replace(/\\\r?\n/gu, " ")
89
77
  .split(/\r?\n/u)
90
78
  .filter((line) => line.startsWith("\t"))
91
- .map((line) => withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS));
79
+ .map((line) => commandsOf(withoutComment(line.slice(1).replace(/^[@+-]+/u, ""))));
92
80
  }
93
81
  /**
94
82
  * Every token the recipe lines of the target root's makefile hold, each with the directory
@@ -113,31 +101,6 @@ function recipeTokens(host, root) {
113
101
  }
114
102
  return found;
115
103
  }
116
- /** One shell line up to its comment: a `#` that starts a word outside quotes. */
117
- function withoutComment(line) {
118
- let quote = "";
119
- for (let at = 0; at < line.length; at += 1) {
120
- const char = line[at];
121
- if (quote !== "") {
122
- if (char === quote) {
123
- quote = "";
124
- }
125
- else if (char === "\\" && quote === '"') {
126
- at += 1;
127
- }
128
- }
129
- else if (char === "'" || char === '"') {
130
- quote = char;
131
- }
132
- else if (char === "\\") {
133
- at += 1;
134
- }
135
- else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
136
- return line.slice(0, at);
137
- }
138
- }
139
- return line;
140
- }
141
104
  /**
142
105
  * Every command the target root's workflow steps, git hooks and makefile run: the `run:`
143
106
  * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
@@ -159,11 +122,11 @@ export function workflowTokens(host, targetRoot) {
159
122
  /** The commands of the workflow steps, git hooks and hook managers at one root. */
160
123
  function stepCommands(host, root) {
161
124
  const commands = [];
162
- for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
125
+ for (const path of pathsIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
163
126
  commands.push(...runCommands(readText(host, path) ?? ""));
164
127
  }
165
128
  for (const dir of HOOK_DIRS) {
166
- for (const path of filesIn(host, joinPath(root, dir), () => true)) {
129
+ for (const path of pathsIn(host, joinPath(root, dir), () => true)) {
167
130
  commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
168
131
  }
169
132
  }
@@ -8,6 +8,7 @@
8
8
  import { emittedFrom } from "./emit-map.js";
9
9
  import { readManifest } from "./manifest.js";
10
10
  import { joinPath, relativePath } from "./paths.js";
11
+ import { MANIFEST } from "./fs/manifest.js";
11
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
12
13
  const SUBPATH_WILDCARD = "*";
13
14
  /** The paths one written target stands for in its member's sources, itself included. */
@@ -53,8 +54,7 @@ export function memberEntries(host, resolver, targetRoot) {
53
54
  return [];
54
55
  }
55
56
  const manifest = readManifest(host, member.dir);
56
- const where = relativePath(targetRoot, joinPath(member.dir, "package.json")) ??
57
- joinPath(member.dir, "package.json");
57
+ const where = relativePath(targetRoot, joinPath(member.dir, MANIFEST)) ?? joinPath(member.dir, MANIFEST);
58
58
  const kept = manifest.entries.flatMap((entry) => {
59
59
  const exported = entry.member.startsWith("exports");
60
60
  if (member.private && exported) {
@@ -9,6 +9,8 @@ import { ModuleKind, ModuleResolutionKind } from "@typescript/native/sync";
9
9
  import { isStringLiteralLikeNode } from "@typescript/native/ast";
10
10
  import { baseUrlPaths } from "./base-url.js";
11
11
  import { dirnamePath, resolvePath } from "./paths.js";
12
+ import { bytewise } from "./text/compare.js";
13
+ import { PRODUCT } from "./product.js";
12
14
  /** The most questions one round trip carries. */
13
15
  const BATCH = 2048;
14
16
  /**
@@ -55,7 +57,7 @@ function unstatable(reading) {
55
57
  export function writtenPath(host, configFile) {
56
58
  const stem = configFile.replace(/\.json$/u, "");
57
59
  for (let counter = 0;; counter += 1) {
58
- const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
60
+ const path = `${stem}.${PRODUCT}${counter === 0 ? "" : `-${String(counter)}`}.json`;
59
61
  if (host.kindOf(path) === "absent") {
60
62
  return path;
61
63
  }
@@ -102,10 +104,10 @@ function configurationOf(engine, host, reading) {
102
104
  ...(exclude === undefined ? {} : { exclude }),
103
105
  });
104
106
  const file = writtenPath(host, reading.configFile);
105
- const expected = [...reading.parsed.fileNames].sort().join("\u0000");
107
+ const expected = [...reading.parsed.fileNames].sort(bytewise).join("\u0000");
106
108
  const listed = (value) => {
107
109
  try {
108
- return [...engine.parseConfigJson(value, file).fileNames].sort().join("\u0000");
110
+ return [...engine.parseConfigJson(value, file).fileNames].sort(bytewise).join("\u0000");
109
111
  }
110
112
  catch {
111
113
  return undefined;