@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.10

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 (276) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +181 -341
  5. package/dist/src/augmentations.js +106 -13
  6. package/dist/src/automatic-types.js +10 -29
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +2 -3
  10. package/dist/src/config.js +6 -82
  11. package/dist/src/configuration-files.js +28 -176
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +1 -1
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +73 -3
  20. package/dist/src/conventions.js +36 -66
  21. package/dist/src/dependencies.js +103 -40
  22. package/dist/src/edges.js +38 -122
  23. package/dist/src/element-access.js +4 -6
  24. package/dist/src/entry-point-gaps.js +2 -8
  25. package/dist/src/entry-points.js +11 -27
  26. package/dist/src/enum-group.js +3 -10
  27. package/dist/src/erased-stores.js +4 -6
  28. package/dist/src/exempt-classes.js +1 -63
  29. package/dist/src/exempt.js +21 -40
  30. package/dist/src/failure.js +36 -0
  31. package/dist/src/file-facts.js +2 -25
  32. package/dist/src/findings/completion.js +139 -72
  33. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  34. package/dist/src/findings/emitters.js +5 -5
  35. package/dist/src/findings/interfaces.js +4 -5
  36. package/dist/src/findings/intra-function.js +13 -10
  37. package/dist/src/findings/non-code-artifacts.js +7 -8
  38. package/dist/src/findings/reads-and-writes.js +16 -13
  39. package/dist/src/findings/self-check.js +10 -12
  40. package/dist/src/findings/unused-declarations.js +41 -32
  41. package/dist/src/findings/visibility-narrowing.js +19 -16
  42. package/dist/src/findings-pass.js +8 -15
  43. package/dist/src/framework-lifecycle.js +7 -7
  44. package/dist/src/fs/document.js +52 -0
  45. package/dist/src/fs/manifest.js +72 -0
  46. package/dist/src/fs/tree.js +88 -0
  47. package/dist/src/generated-file.js +7 -4
  48. package/dist/src/git-ignore.js +39 -26
  49. package/dist/src/graph.js +5 -3
  50. package/dist/src/html-entries.js +6 -33
  51. package/dist/src/in-operands.js +2 -1
  52. package/dist/src/index.js +1 -1
  53. package/dist/src/injection-container.js +1 -1
  54. package/dist/src/intra-function-parts.js +63 -43
  55. package/dist/src/inventory.js +2 -21
  56. package/dist/src/javascript-file.js +33 -0
  57. package/dist/src/json-document.js +198 -67
  58. package/dist/src/keyof-comparisons.js +4 -6
  59. package/dist/src/kinds.js +3 -366
  60. package/dist/src/load/classify.js +270 -0
  61. package/dist/src/load/diagnostics.js +80 -0
  62. package/dist/src/load/discover.js +277 -0
  63. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  64. package/dist/src/load/expected-module.js +136 -0
  65. package/dist/src/load/load.js +27 -0
  66. package/dist/src/load/module-lookup.js +37 -0
  67. package/dist/src/load/plan.js +313 -0
  68. package/dist/src/load/specifiers.js +115 -0
  69. package/dist/src/load/synthesize.js +140 -0
  70. package/dist/src/load/test-globals.js +207 -0
  71. package/dist/src/load/test-joins.js +82 -0
  72. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  73. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
  74. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
  75. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  76. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  77. package/dist/src/manifest.js +49 -54
  78. package/dist/src/matrix.js +76 -4
  79. package/dist/src/mdx-entries.js +3 -26
  80. package/dist/src/outside-tests.js +1 -1
  81. package/dist/src/partial.js +2 -1
  82. package/dist/src/paths.js +26 -15
  83. package/dist/src/position.js +2 -1
  84. package/dist/src/print.js +11 -79
  85. package/dist/src/product.js +13 -0
  86. package/dist/src/query.js +1 -0
  87. package/dist/src/ref.js +1 -0
  88. package/dist/src/references.js +48 -30
  89. package/dist/src/report-schema.js +51 -0
  90. package/dist/src/report.js +57 -41
  91. package/dist/src/reporters/annotations.js +15 -8
  92. package/dist/src/reporters/sarif.js +16 -10
  93. package/dist/src/reporters/template.js +4 -19
  94. package/dist/src/resolve.js +245 -729
  95. package/dist/src/roots.js +273 -87
  96. package/dist/src/run.js +66 -77
  97. package/dist/src/schema.js +320 -343
  98. package/dist/src/scope.js +38 -115
  99. package/dist/src/serialization-contract.js +595 -21
  100. package/dist/src/session.js +34 -29
  101. package/dist/src/source-files.js +1 -1
  102. package/dist/src/suppress-file.js +25 -28
  103. package/dist/src/suppress-inline.js +3 -2
  104. package/dist/src/suppress.js +33 -51
  105. package/dist/src/sweep.js +7 -9
  106. package/dist/src/symbol-keys.js +3 -5
  107. package/dist/src/template-field.js +9 -19
  108. package/dist/src/test-support.js +8 -11
  109. package/dist/src/text/compare.js +26 -0
  110. package/dist/src/text/glob.js +194 -0
  111. package/dist/src/text/named-files.js +54 -0
  112. package/dist/src/text/shell.js +159 -0
  113. package/dist/src/text/yaml.js +393 -0
  114. package/dist/src/type-errors.js +11 -232
  115. package/dist/src/unseen-loads.js +100 -0
  116. package/dist/src/unwrap.js +23 -0
  117. package/dist/src/usage.js +10 -0
  118. package/dist/src/verbs/analyze.js +166 -126
  119. package/dist/src/verbs/describe.js +9 -16
  120. package/dist/src/verbs/explain.js +10 -7
  121. package/dist/src/verbs/print-config.js +2 -1
  122. package/dist/src/verbs/print-projects.js +6 -4
  123. package/dist/src/verbs/print-retained.js +4 -2
  124. package/dist/src/verbs/print-roots.js +4 -2
  125. package/dist/src/verbs/verb.js +1 -5
  126. package/dist/src/verbs/version.js +14 -3
  127. package/dist/src/workflow-steps.js +62 -64
  128. package/jsr.json +1 -1
  129. package/package.json +4 -3
  130. package/src/analysis.ts +245 -502
  131. package/src/augmentations.ts +135 -17
  132. package/src/automatic-types.ts +11 -29
  133. package/src/build-scripts.ts +543 -0
  134. package/src/component-layer.ts +4 -2
  135. package/src/components.ts +2 -4
  136. package/src/config.ts +31 -302
  137. package/src/configuration-files.ts +34 -179
  138. package/src/configured-declarations.ts +18 -23
  139. package/src/conformance-record.ts +1 -1
  140. package/src/consumers.ts +6 -57
  141. package/src/contextual-uses.ts +64 -12
  142. package/src/contract/config.generated.ts +818 -0
  143. package/src/contract/contract.generated.ts +1308 -0
  144. package/src/contract/report.generated.ts +1237 -0
  145. package/src/convention-rows.ts +85 -3
  146. package/src/conventions.ts +46 -84
  147. package/src/dependencies.ts +118 -46
  148. package/src/edges.ts +41 -140
  149. package/src/element-access.ts +4 -7
  150. package/src/entry-point-gaps.ts +3 -9
  151. package/src/entry-points.ts +12 -38
  152. package/src/enum-group.ts +2 -11
  153. package/src/erased-stores.ts +3 -7
  154. package/src/exempt-classes.ts +1 -75
  155. package/src/exempt.ts +36 -50
  156. package/src/failure.ts +63 -0
  157. package/src/file-facts.ts +9 -24
  158. package/src/finding.ts +14 -9
  159. package/src/findings/boundary.ts +2 -0
  160. package/src/findings/completion.ts +181 -84
  161. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  162. package/src/findings/emitter.ts +8 -9
  163. package/src/findings/emitters.ts +7 -7
  164. package/src/findings/interfaces.ts +4 -6
  165. package/src/findings/intra-function.ts +13 -11
  166. package/src/findings/non-code-artifacts.ts +9 -11
  167. package/src/findings/reads-and-writes.ts +20 -16
  168. package/src/findings/self-check.ts +11 -17
  169. package/src/findings/unused-declarations.ts +45 -32
  170. package/src/findings/visibility-narrowing.ts +20 -17
  171. package/src/findings-pass.ts +10 -19
  172. package/src/framework-lifecycle.ts +7 -7
  173. package/src/fs/document.ts +70 -0
  174. package/src/fs/manifest.ts +92 -0
  175. package/src/fs/tree.ts +110 -0
  176. package/src/generated-file.ts +7 -4
  177. package/src/git-ignore.ts +50 -26
  178. package/src/graph.ts +5 -3
  179. package/src/host.ts +12 -5
  180. package/src/html-entries.ts +6 -33
  181. package/src/in-operands.ts +2 -1
  182. package/src/index.ts +1 -1
  183. package/src/injection-container.ts +1 -1
  184. package/src/intra-function-parts.ts +73 -44
  185. package/src/inventory.ts +2 -20
  186. package/src/javascript-file.ts +37 -0
  187. package/src/json-document.ts +241 -76
  188. package/src/keyof-comparisons.ts +3 -7
  189. package/src/kinds.ts +5 -413
  190. package/src/load/classify.ts +397 -0
  191. package/src/load/diagnostics.ts +144 -0
  192. package/src/load/discover.ts +455 -0
  193. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  194. package/src/load/expected-module.ts +188 -0
  195. package/src/load/load.ts +73 -0
  196. package/src/load/module-lookup.ts +50 -0
  197. package/src/load/plan.ts +504 -0
  198. package/src/load/specifiers.ts +168 -0
  199. package/src/load/synthesize.ts +211 -0
  200. package/src/load/test-globals.ts +251 -0
  201. package/src/load/test-joins.ts +98 -0
  202. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  203. package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
  204. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
  205. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  206. package/src/{workspace.ts → load/workspace.ts} +90 -134
  207. package/src/manifest.ts +81 -75
  208. package/src/matrix.ts +122 -4
  209. package/src/mdx-entries.ts +3 -26
  210. package/src/outside-tests.ts +1 -1
  211. package/src/partial.ts +3 -2
  212. package/src/paths.ts +27 -15
  213. package/src/position.ts +2 -1
  214. package/src/print.ts +12 -92
  215. package/src/product.ts +18 -0
  216. package/src/query.ts +11 -0
  217. package/src/ref.ts +1 -0
  218. package/src/references.ts +52 -31
  219. package/src/report-schema.ts +68 -0
  220. package/src/report.ts +101 -224
  221. package/src/reporters/annotations.ts +22 -11
  222. package/src/reporters/json.ts +1 -1
  223. package/src/reporters/reporter.ts +2 -4
  224. package/src/reporters/reporters.ts +1 -1
  225. package/src/reporters/sarif.ts +29 -16
  226. package/src/reporters/template.ts +4 -20
  227. package/src/reporters/text.ts +3 -3
  228. package/src/resolve.ts +283 -1114
  229. package/src/roots.ts +339 -104
  230. package/src/run.ts +75 -91
  231. package/src/schema.ts +391 -388
  232. package/src/scope.ts +46 -138
  233. package/src/serialization-contract.ts +763 -22
  234. package/src/session.ts +70 -54
  235. package/src/source-files.ts +1 -1
  236. package/src/suppress-file.ts +24 -33
  237. package/src/suppress-inline.ts +2 -2
  238. package/src/suppress.ts +43 -58
  239. package/src/sweep.ts +23 -9
  240. package/src/symbol-keys.ts +2 -5
  241. package/src/template-field.ts +15 -25
  242. package/src/test-support.ts +17 -13
  243. package/src/text/compare.ts +27 -0
  244. package/src/text/glob.ts +210 -0
  245. package/src/text/named-files.inputs.json +1522 -0
  246. package/src/text/named-files.ts +68 -0
  247. package/src/text/shell.ts +161 -0
  248. package/src/text/yaml.ts +423 -0
  249. package/src/type-errors.ts +18 -288
  250. package/src/unseen-loads.ts +134 -0
  251. package/src/unwrap.ts +34 -0
  252. package/src/usage.ts +10 -0
  253. package/src/verbs/analyze.ts +224 -162
  254. package/src/verbs/describe.ts +14 -15
  255. package/src/verbs/explain.ts +10 -11
  256. package/src/verbs/print-config.ts +2 -1
  257. package/src/verbs/print-projects.ts +6 -4
  258. package/src/verbs/print-retained.ts +4 -2
  259. package/src/verbs/print-roots.ts +4 -2
  260. package/src/verbs/verb.ts +2 -8
  261. package/src/verbs/version.ts +15 -3
  262. package/src/workflow-steps.ts +74 -64
  263. package/dist/src/base-url.js +0 -18
  264. package/dist/src/discover.js +0 -468
  265. package/dist/src/glob.js +0 -65
  266. package/dist/src/pnpm-workspace.js +0 -120
  267. package/dist/src/setup-failure.js +0 -23
  268. package/dist/src/test-joins.js +0 -101
  269. package/dist/src/version.js +0 -7
  270. package/src/base-url.ts +0 -24
  271. package/src/discover.ts +0 -653
  272. package/src/glob.ts +0 -67
  273. package/src/pnpm-workspace.ts +0 -135
  274. package/src/setup-failure.ts +0 -39
  275. package/src/test-joins.ts +0 -120
  276. package/src/version.ts +0 -7
@@ -42,17 +42,26 @@ import {
42
42
  import type { FindingPosition } from "./finding.ts";
43
43
  import type { Host } from "./host.ts";
44
44
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
45
- import { scriptTokens } from "./manifest.ts";
46
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
45
+ import {
46
+ ancestors,
47
+ dirnamePath,
48
+ isAbsolutePath,
49
+ joinPath,
50
+ relativePath,
51
+ resolvePath,
52
+ } from "./paths.ts";
47
53
  import type { DependencySection, Module } from "./ref.ts";
48
54
  import { UNANSWERED } from "./query.ts";
49
55
  import type { Handle, ProjectView } from "./session.ts";
50
56
  import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
51
- import { workflowTokens } from "./workflow-steps.ts";
57
+ import { manifestCommands } from "./manifest.ts";
58
+ import { workflowCommands, workflowTokens } from "./workflow-steps.ts";
59
+ import { bytewise } from "./text/compare.ts";
60
+ import { isRecord } from "./json-document.ts";
61
+ import { inInstalledPackages, INSTALLED_PACKAGES, MANIFEST, readDocument } from "./fs/manifest.ts";
62
+ import { commandsOf, scriptTokens } from "./text/shell.ts";
52
63
 
53
- const MANIFEST = "package.json";
54
- const MODULES_DIR = "node_modules";
55
- const MODULES_SEGMENT = `/${MODULES_DIR}/`;
64
+ const MODULES_SEGMENT = `/${INSTALLED_PACKAGES}/`;
56
65
  const TYPES_SCOPE = "@types/";
57
66
 
58
67
  /** A script token naming a JavaScript file by a path no expansion changes. */
@@ -160,25 +169,6 @@ export interface Dependencies {
160
169
  readonly heldBy: ReadonlyMap<string, readonly string[]>;
161
170
  }
162
171
 
163
- function isRecord(value: unknown): value is Record<string, unknown> {
164
- return typeof value === "object" && value !== null && !Array.isArray(value);
165
- }
166
-
167
- /** One JSON document's decoded value, or undefined where it cannot be read or parsed. */
168
- function readJSON(host: Host, path: string): { text: string; value: unknown } | undefined {
169
- let text: string;
170
- try {
171
- text = host.readFile(path);
172
- } catch {
173
- return undefined;
174
- }
175
- try {
176
- return { text, value: JSON.parse(text) };
177
- } catch {
178
- return undefined;
179
- }
180
- }
181
-
182
172
  /**
183
173
  * The package a bare module specifier names: its first segment, or its first two
184
174
  * where it starts with a scope. A relative, absolute or scheme-prefixed specifier and
@@ -429,7 +419,7 @@ export function projectNeeds<Brand>(
429
419
  }
430
420
  for (const { strings, manifestDirs } of configuration.documents) {
431
421
  for (const manifestDir of manifestDirs) {
432
- const names = declaredAt(joinPath(manifestDir, "package.json"));
422
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
433
423
  for (const text of strings) {
434
424
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
435
425
  }
@@ -444,7 +434,7 @@ export function projectNeeds<Brand>(
444
434
  );
445
435
  for (const { file, manifestDirs } of [...configuration.outside, ...ranFiles]) {
446
436
  for (const manifestDir of manifestDirs) {
447
- const names = declaredAt(joinPath(manifestDir, "package.json"));
437
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
448
438
  for (const { text } of moduleStrings(file)) {
449
439
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
450
440
  }
@@ -467,8 +457,8 @@ function scriptRunFiles(
467
457
  const root = resolvePath(host.workingDirectory(), targetRoot);
468
458
  const dirs = new Set<string>([root]);
469
459
  for (const name of files.byName.keys()) {
470
- for (let dir = dirnamePath(name); !dirs.has(dir); dir = dirnamePath(dir)) {
471
- if (relativePath(root, dir) === undefined) {
460
+ for (const dir of ancestors(dirnamePath(name), root)) {
461
+ if (dirs.has(dir)) {
472
462
  break;
473
463
  }
474
464
  dirs.add(dir);
@@ -478,7 +468,7 @@ function scriptRunFiles(
478
468
  [root, new Set(workflowTokens(host, root))],
479
469
  ]);
480
470
  for (const dir of dirs) {
481
- const read = readJSON(host, joinPath(dir, MANIFEST));
471
+ const read = readDocument(host, joinPath(dir, MANIFEST));
482
472
  if (read !== undefined && isRecord(read.value)) {
483
473
  const own = scriptTokensOf(read.value);
484
474
  tokens.set(dir, dir === root ? new Set([...own, ...(tokens.get(root) ?? [])]) : own);
@@ -491,7 +481,8 @@ function scriptRunFiles(
491
481
  const below = path === undefined ? undefined : relativePath(root, path);
492
482
  if (
493
483
  path !== undefined &&
494
- below?.split("/").includes(MODULES_DIR) === false &&
484
+ below !== undefined &&
485
+ !inInstalledPackages(below) &&
495
486
  !files.byName.has(path) &&
496
487
  host.kindOf(path) === "file"
497
488
  ) {
@@ -797,8 +788,8 @@ function lineAndColumn(text: string, offset: number): { line: number; column: nu
797
788
 
798
789
  /** The installed copy of one dependency, read where the package manager puts it for the target. */
799
790
  function installedOf(host: Host, root: string, name: string): InstalledDependency | undefined {
800
- for (let dir = root; ; dir = dirnamePath(dir)) {
801
- const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
791
+ for (const dir of ancestors(root)) {
792
+ const read = readDocument(host, joinPath(dir, INSTALLED_PACKAGES, name, MANIFEST));
802
793
  if (read !== undefined && isRecord(read.value)) {
803
794
  const bin = read.value["bin"];
804
795
  const peers = read.value["peerDependencies"];
@@ -815,10 +806,8 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
815
806
  : [],
816
807
  };
817
808
  }
818
- if (dirnamePath(dir) === dir) {
819
- return undefined;
820
- }
821
809
  }
810
+ return undefined;
822
811
  }
823
812
 
824
813
  /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
@@ -871,6 +860,88 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
871
860
  return found;
872
861
  }
873
862
 
863
+ /**
864
+ * The runtime flags whose value is a module loaded before the entry, as `--flag value` or
865
+ * `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
866
+ * https://nodejs.org/api/cli.html#--experimental-loadermodule,
867
+ * https://nodejs.org/api/cli.html#-r---require-module
868
+ */
869
+ const PRELOAD_FLAGS: ReadonlySet<string> = new Set([
870
+ "--experimental-loader",
871
+ "--import",
872
+ "--loader",
873
+ "--require",
874
+ "-r",
875
+ ]);
876
+
877
+ /**
878
+ * The programs that pass a preload flag to the runtime they start: Node itself, and the
879
+ * runners that forward Node's flags or read the same `--require`:
880
+ * https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
881
+ * https://typestrong.org/ts-node/docs/options#require,
882
+ * https://github.com/remy/nodemon#running-non-node-scripts
883
+ */
884
+ const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
885
+ "mocha",
886
+ "node",
887
+ "nodemon",
888
+ "ts-node",
889
+ "tsx",
890
+ ]);
891
+
892
+ /** The programs that run the command after them, and an environment assignment before it. */
893
+ const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
894
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
895
+
896
+ /**
897
+ * The packages `scripts` preload by a runtime flag's bare module specifier, a flag
898
+ * counting only in a command whose program passes it to the runtime, and for Node only
899
+ * before the script it runs, whose own arguments come after it.
900
+ */
901
+ function preloadsOf(scripts: readonly string[]): ReadonlySet<string> {
902
+ const found = new Set<string>();
903
+ for (const script of scripts) {
904
+ for (const command of commandsOf(script)) {
905
+ const tokens = scriptTokens(command);
906
+ let at = 0;
907
+ while (
908
+ at < tokens.length &&
909
+ (ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))
910
+ ) {
911
+ at += 1;
912
+ }
913
+ const path = tokens[at] ?? "";
914
+ const program = path.slice(path.lastIndexOf("/") + 1);
915
+ if (!PRELOADING_PROGRAMS.has(program)) {
916
+ continue;
917
+ }
918
+ for (at += 1; at < tokens.length; at += 1) {
919
+ const token = tokens[at] ?? "";
920
+ if (!token.startsWith("-")) {
921
+ if (program === "node") {
922
+ break;
923
+ }
924
+ continue;
925
+ }
926
+ const equals = token.indexOf("=");
927
+ const flag = equals < 0 ? token : token.slice(0, equals);
928
+ if (!PRELOAD_FLAGS.has(flag)) {
929
+ continue;
930
+ }
931
+ if (equals < 0) {
932
+ at += 1;
933
+ }
934
+ const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
935
+ const name = value === undefined ? undefined : packageOfSpecifier(value);
936
+ if (name !== undefined) {
937
+ found.add(name);
938
+ }
939
+ }
940
+ }
941
+ }
942
+ return found;
943
+ }
944
+
874
945
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
875
946
  function isInside(path: string, dir: string): boolean {
876
947
  return dir === "" || path.startsWith(`${dir}/`);
@@ -966,22 +1037,29 @@ export function dependenciesOf(
966
1037
  }
967
1038
  }
968
1039
  const workflow = new Set(workflowTokens(host, targetRoot));
1040
+ const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
969
1041
  const declared: DeclaredDependency[] = [];
970
1042
  const ran = new Set<string>();
971
1043
  const peers = new Map<string, readonly string[]>();
972
1044
  const uninstalledHere = new Set(nested);
973
1045
  for (const [dir, path] of manifests) {
974
- const read = readJSON(host, joinPath(dir, MANIFEST));
1046
+ const read = readDocument(host, joinPath(dir, MANIFEST));
975
1047
  if (read === undefined || !isRecord(read.value)) {
976
1048
  continue;
977
1049
  }
978
1050
  const tokens = scriptTokensOf(read.value);
1051
+ const preloads = preloadsOf(
1052
+ manifestCommands(host, dir, read.value).map(({ command }) => command),
1053
+ );
979
1054
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
980
1055
  const found = installedOf(host, dir, dependency.name);
981
1056
  if (found === undefined && uninstalledHere.has(dir)) {
982
1057
  continue;
983
1058
  }
984
1059
  declared.push(dependency);
1060
+ if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
1061
+ ran.add(declarationKey(dependency));
1062
+ }
985
1063
  if (found === undefined) {
986
1064
  continue;
987
1065
  }
@@ -1045,10 +1123,7 @@ export function dependenciesOf(
1045
1123
  const heldBy = new Map<string, readonly string[]>();
1046
1124
  for (const [key, held] of holders) {
1047
1125
  if (!live.has(key) && !ran.has(key)) {
1048
- heldBy.set(
1049
- key,
1050
- [...held].sort((a, b) => (a < b ? -1 : 1)),
1051
- );
1126
+ heldBy.set(key, [...held].sort(bytewise));
1052
1127
  }
1053
1128
  }
1054
1129
  const users = new Map<string, number>();
@@ -1060,10 +1135,7 @@ export function dependenciesOf(
1060
1135
  for (const id of candidates) {
1061
1136
  const sole = [...(uses.get(id) ?? [])].filter((one) => users.get(one) === 1 && names.has(one));
1062
1137
  if (sole.length > 0) {
1063
- lastUses.set(
1064
- id,
1065
- sole.sort((a, b) => (a < b ? -1 : 1)),
1066
- );
1138
+ lastUses.set(id, sole.sort(bytewise));
1067
1139
  }
1068
1140
  }
1069
1141
  const owners = new Map<string, Set<string>>();
@@ -1075,7 +1147,7 @@ export function dependenciesOf(
1075
1147
  }
1076
1148
  }
1077
1149
  const fileLastUses = new Map<string, string[]>();
1078
- for (const [one, held] of [...owners].sort(([a], [b]) => (a < b ? -1 : 1))) {
1150
+ for (const [one, held] of [...owners].sort(([a], [b]) => bytewise(a, b))) {
1079
1151
  const paths = new Set([...held].map((owner) => owner.replace(/:\d+:\d+$/u, "")));
1080
1152
  const [path] = paths;
1081
1153
  if (paths.size === 1 && path !== undefined && path !== PROJECT_OWNER) {
package/src/edges.ts CHANGED
@@ -5,15 +5,18 @@
5
5
  * pairing is a use, and the merge reads the two evaluations together.
6
6
  */
7
7
 
8
- import { ConfigError } from "./config.ts";
8
+ import { EDGES_SCHEMA } from "./contract/config.generated.ts";
9
9
  import type { Finding } from "./finding.ts";
10
10
  import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
11
+ import { readTargetDocument } from "./fs/document.ts";
11
12
  import type { Host } from "./host.ts";
12
13
  import type { InventorySymbol } from "./inventory.ts";
13
- import { positionAt, walkDocument } from "./json-document.ts";
14
- import { joinPath } from "./paths.ts";
14
+ import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.ts";
15
15
  import { positionKey } from "./position.ts";
16
+ import { firstRefusal } from "./schema.ts";
16
17
  import { isPartSubject } from "./suppress.ts";
18
+ import { bytewise } from "./text/compare.ts";
19
+ import { UsageError } from "./usage.ts";
17
20
 
18
21
  /** The name and the location the Contract fixes for the edges document. */
19
22
  export const EDGES_FILE = "deadset-edges.json";
@@ -21,155 +24,61 @@ export const EDGES_FILE = "deadset-edges.json";
21
24
  /** The language prefix of every reference this analyzer evaluates. */
22
25
  const OWN_PREFIX = "ts://";
23
26
 
24
- /** The closed key list of the document and of one edge. */
25
- const DOCUMENT_KEYS: ReadonlySet<string> = new Set(["description", "edges"]);
26
- const EDGE_KEYS: ReadonlySet<string> = new Set(["id", "because", "provides", "used_by"]);
27
-
28
- /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
29
- const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
30
-
31
- /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
32
- const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
33
-
34
- const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
35
- const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
36
- const ID_WANT =
37
- "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
38
- const REFERENCE_WANT =
39
- "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
40
-
41
27
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
42
- class EdgesError extends ConfigError {
43
- constructor(at: string, text: string, want: string) {
44
- super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
28
+ class EdgesError extends UsageError {
29
+ constructor(at: string, reason: string) {
30
+ super(`${at}: ${reason}`);
45
31
  this.name = "EdgesError";
46
32
  }
47
33
  }
48
34
 
49
- /**
50
- * Whether a reference carries a wildcard outside a quoted or a computed component, which
51
- * would name a set of symbols rather than one.
52
- */
53
- function carriesWildcard(ref: string): boolean {
54
- let quoted = false;
55
- let computed = false;
56
- let escaped = false;
57
- for (const char of ref) {
58
- if (escaped) {
59
- escaped = false;
60
- } else if (quoted && char === "\\") {
61
- escaped = true;
62
- } else if (quoted) {
63
- quoted = char !== "'";
64
- } else if (computed) {
65
- computed = char !== "]";
66
- } else if (char === "'") {
67
- quoted = true;
68
- } else if (char === "[") {
69
- computed = true;
70
- } else if (char === "*") {
71
- return true;
72
- }
73
- }
74
- return false;
75
- }
76
-
77
- function isObject(value: unknown): value is Record<string, unknown> {
78
- return typeof value === "object" && value !== null && !Array.isArray(value);
79
- }
80
-
81
- /** One edge, checked: the closed key list, the value types and the value forms. */
82
- function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
83
- const refuse = (text: string, want: string): never => {
84
- throw new EdgesError(at, text, want);
85
- };
86
- if (!isObject(value)) {
87
- return refuse(JSON.stringify(value), EDGE_WANT);
88
- }
89
- for (const [key, held] of Object.entries(value)) {
90
- if (!EDGE_KEYS.has(key) || typeof held !== "string") {
91
- refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
92
- }
93
- }
94
- const edge = value as Readonly<Record<string, string | undefined>>;
95
- const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
96
- const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
97
- const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
98
- if (!ID_FORM.test(id)) {
99
- refuse(JSON.stringify(id), ID_WANT);
100
- }
101
- for (const ref of [provides, usedBy]) {
102
- if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
103
- refuse(JSON.stringify(ref), REFERENCE_WANT);
104
- }
105
- }
106
- return { id, provides, usedBy };
107
- }
35
+ /** The sides of one edge, in the order a reader yields them. */
36
+ const SIDE_NAMES: readonly EdgeSideName[] = ["provides", "used_by"];
108
37
 
109
38
  /**
110
39
  * Every side of a declared edge that names a symbol of this language, in document order,
111
40
  * `provides` before `used_by` within an edge. An absent document declares no edge. The
112
- * document is strict JSON with a closed key list at both levels, and a defect in it is an
113
- * {@link EdgesError}.
41
+ * document is strict JSON held to the Contract's edges schema, and a defect in it is an
42
+ * {@link EdgesError} at the line and column of the edge it is in, where it is in one.
114
43
  */
115
44
  export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide[] {
116
- const file = joinPath(targetRoot, EDGES_FILE);
117
- if (host.kindOf(file) === "absent") {
45
+ const text = readTargetDocument(host, targetRoot, EDGES_FILE);
46
+ if (text === undefined) {
118
47
  return [];
119
48
  }
120
- const text = host.readFile(file);
121
- let parsed: unknown;
49
+ let read;
122
50
  try {
123
- parsed = JSON.parse(text);
51
+ read = readStrict(text, "edges");
124
52
  } catch (error: unknown) {
125
- throw new EdgesError(
126
- EDGES_FILE,
127
- error instanceof Error ? error.message : String(error),
128
- DOCUMENT_WANT,
129
- );
130
- }
131
- if (!isObject(parsed)) {
132
- throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
133
- }
134
- const walk = walkDocument(text, "edges");
135
- if (walk.twice !== undefined) {
136
- throw new EdgesError(
137
- positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)),
138
- `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
139
- DOCUMENT_WANT,
140
- );
53
+ if (!(error instanceof StrictJSONError)) {
54
+ throw error;
55
+ }
56
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
141
57
  }
142
- const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
143
- const description = parsed["description"];
144
- const edges = parsed["edges"];
145
- if (
146
- unknown !== undefined ||
147
- (description !== undefined && typeof description !== "string") ||
148
- !Array.isArray(edges)
149
- ) {
150
- throw new EdgesError(
151
- EDGES_FILE,
152
- unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`,
153
- DOCUMENT_WANT,
154
- );
58
+ const value = read.value;
59
+ const refused = firstRefusal(EDGES_SCHEMA, value);
60
+ if (refused !== undefined) {
61
+ const place = placeIn(value, refused.pointer, refused.member);
62
+ const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
63
+ const at =
64
+ offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
65
+ const where = dotted(place);
66
+ throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
155
67
  }
68
+ const edges = (
69
+ value as { readonly edges: Readonly<Record<string, Record<EdgeSideName, string>>> }
70
+ ).edges;
156
71
 
157
72
  const sides: EdgeSide[] = [];
158
- (edges as readonly unknown[]).forEach((value, index) => {
159
- const offset = walk.starts[index];
160
- const at =
161
- offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
162
- const edge = readEdge(value, at);
163
- const named: readonly [EdgeSideName, string][] = [
164
- ["provides", edge.provides],
165
- ["used_by", edge.usedBy],
166
- ];
167
- for (const [side, symbol] of named) {
168
- if (symbol.startsWith(OWN_PREFIX)) {
169
- sides.push({ edge: edge.id, side, symbol });
73
+ for (const id of read.starts.keys()) {
74
+ const edge = String(id);
75
+ for (const side of SIDE_NAMES) {
76
+ const symbol = edges[edge]?.[side];
77
+ if (symbol?.startsWith(OWN_PREFIX) === true) {
78
+ sides.push({ edge, side, symbol });
170
79
  }
171
80
  }
172
- });
81
+ }
173
82
  return sides;
174
83
  }
175
84
 
@@ -190,14 +99,6 @@ interface Evaluated<F extends Finding> {
190
99
  readonly evaluations: readonly EdgeEvaluation<F>[];
191
100
  }
192
101
 
193
- /** Two strings ordered bytewise. */
194
- function compare(a: string, b: string): number {
195
- if (a === b) {
196
- return 0;
197
- }
198
- return a < b ? -1 : 1;
199
- }
200
-
201
102
  /**
202
103
  * Evaluates every declared side of this language over the run's findings. A side naming a
203
104
  * symbol the run enumerates nothing under is `absent`; otherwise it is evaluated once per
@@ -242,6 +143,6 @@ export function evaluateEdges<F extends Finding>(
242
143
  return { ...side, state: "dead", finding };
243
144
  });
244
145
  });
245
- evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
146
+ evaluations.sort((a, b) => bytewise(a.edge, b.edge) || bytewise(a.side, b.side));
246
147
  return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
247
148
  }
@@ -47,6 +47,7 @@ import { renderPosition } from "./position.ts";
47
47
  import { UNANSWERED } from "./query.ts";
48
48
  import type { Reference } from "./references.ts";
49
49
  import type { ProjectView } from "./session.ts";
50
+ import { parenthesesRemoved } from "./unwrap.ts";
50
51
 
51
52
  type Use = "read" | "write" | "both";
52
53
 
@@ -93,13 +94,9 @@ function useOf(node: Node): Use {
93
94
  return "read";
94
95
  }
95
96
 
96
- function unwrapped(node: Expression): Expression {
97
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
98
- }
99
-
100
97
  /** Whether an index is a literal the checker resolves to a member by its own text. */
101
98
  function isLiteralIndex(node: Expression): boolean {
102
- const at = unwrapped(node);
99
+ const at = parenthesesRemoved(node);
103
100
  return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
104
101
  }
105
102
 
@@ -293,7 +290,7 @@ export function elementAccessReferences<Brand>(
293
290
  const parameterOf = new Map<Candidate, { callable: Callable; at: number }>();
294
291
  const byCallable = new Map<Identifier, Candidate[]>();
295
292
  for (const candidate of candidates) {
296
- const index = unwrapped(candidate.access.argumentExpression);
293
+ const index = parenthesesRemoved(candidate.access.argumentExpression);
297
294
  const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
298
295
  const callable = fn === undefined ? undefined : callableOf(fn);
299
296
  if (callable === undefined || !isIdentifier(index)) {
@@ -353,7 +350,7 @@ export function elementAccessReferences<Brand>(
353
350
  continue;
354
351
  }
355
352
  // An object that is a parameter too is read as the argument each call passes there.
356
- const object = unwrapped(candidate.access.expression);
353
+ const object = parenthesesRemoved(candidate.access.expression);
357
354
  const objectAt = isIdentifier(object)
358
355
  ? deferred.callable.parameters.findIndex(
359
356
  (parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text,
@@ -8,6 +8,8 @@
8
8
  * one at a time: beside a finding, or in a report's declared gaps.
9
9
  */
10
10
 
11
+ import { bytewise } from "./text/compare.ts";
12
+
11
13
  /** One declined entry-point convention. */
12
14
  export interface DeclinedConvention {
13
15
  /** The tool the convention belongs to, by the name of the package or service. */
@@ -323,16 +325,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
323
325
  },
324
326
  ];
325
327
 
326
- /** Two strings ordered bytewise. */
327
- function compare(a: string, b: string): number {
328
- if (a === b) {
329
- return 0;
330
- }
331
- return a < b ? -1 : 1;
332
- }
333
-
334
328
  /** Every declined entry-point convention, ordered by tool and then by convention. */
335
329
  export const DECLINED_CONVENTIONS: readonly DeclinedConvention[] = [
336
330
  ...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
337
331
  ...PARTLY_ENTERED,
338
- ].sort((a, b) => compare(a.tool, b.tool) || compare(a.convention, b.convention));
332
+ ].sort((a, b) => bytewise(a.tool, b.tool) || bytewise(a.convention, b.convention));