@cplieger/deadset-ts 7.2.0-dev.4 → 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 (288) 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 +245 -309
  5. package/dist/src/augmentations.js +124 -18
  6. package/dist/src/automatic-types.js +170 -0
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +9 -9
  10. package/dist/src/config.js +6 -81
  11. package/dist/src/configuration-files.js +78 -188
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +2 -2
  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 +146 -1
  20. package/dist/src/conventions.js +136 -70
  21. package/dist/src/decorator.js +21 -18
  22. package/dist/src/dependencies.js +280 -62
  23. package/dist/src/edges.js +38 -122
  24. package/dist/src/element-access.js +4 -6
  25. package/dist/src/entry-point-gaps.js +9 -18
  26. package/dist/src/entry-points.js +106 -21
  27. package/dist/src/enum-group.js +3 -10
  28. package/dist/src/erased-stores.js +4 -6
  29. package/dist/src/exempt-classes.js +1 -63
  30. package/dist/src/exempt.js +21 -40
  31. package/dist/src/failure.js +36 -0
  32. package/dist/src/file-facts.js +2 -25
  33. package/dist/src/findings/completion.js +225 -63
  34. package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
  35. package/dist/src/findings/emitters.js +6 -6
  36. package/dist/src/findings/interfaces.js +4 -5
  37. package/dist/src/findings/intra-function.js +53 -19
  38. package/dist/src/findings/non-code-artifacts.js +71 -39
  39. package/dist/src/findings/reads-and-writes.js +16 -13
  40. package/dist/src/findings/self-check.js +10 -12
  41. package/dist/src/findings/unused-declarations.js +57 -28
  42. package/dist/src/findings/visibility-narrowing.js +40 -18
  43. package/dist/src/findings-pass.js +8 -15
  44. package/dist/src/framework-lifecycle.js +7 -7
  45. package/dist/src/fs/document.js +52 -0
  46. package/dist/src/fs/manifest.js +72 -0
  47. package/dist/src/fs/tree.js +88 -0
  48. package/dist/src/generated-file.js +18 -4
  49. package/dist/src/git-ignore.js +99 -0
  50. package/dist/src/graph.js +9 -4
  51. package/dist/src/html-entries.js +6 -33
  52. package/dist/src/import-bindings.js +97 -0
  53. package/dist/src/in-operands.js +2 -1
  54. package/dist/src/index.js +1 -1
  55. package/dist/src/indexed-access.js +130 -28
  56. package/dist/src/injection-container.js +1 -1
  57. package/dist/src/interface-satisfaction.js +30 -12
  58. package/dist/src/intra-function-parts.js +341 -69
  59. package/dist/src/inventory.js +54 -29
  60. package/dist/src/javascript-file.js +33 -0
  61. package/dist/src/json-document.js +198 -67
  62. package/dist/src/keyof-comparisons.js +4 -6
  63. package/dist/src/kinds.js +3 -366
  64. package/dist/src/load/classify.js +270 -0
  65. package/dist/src/load/diagnostics.js +80 -0
  66. package/dist/src/load/discover.js +277 -0
  67. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  68. package/dist/src/load/expected-module.js +136 -0
  69. package/dist/src/load/load.js +27 -0
  70. package/dist/src/load/module-lookup.js +37 -0
  71. package/dist/src/load/plan.js +313 -0
  72. package/dist/src/load/specifiers.js +115 -0
  73. package/dist/src/load/synthesize.js +140 -0
  74. package/dist/src/load/test-globals.js +207 -0
  75. package/dist/src/load/test-joins.js +82 -0
  76. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  77. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
  78. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
  79. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  80. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  81. package/dist/src/manifest.js +49 -54
  82. package/dist/src/matrix.js +94 -13
  83. package/dist/src/mdx-entries.js +174 -0
  84. package/dist/src/module-declarations.js +9 -13
  85. package/dist/src/outside-tests.js +1 -1
  86. package/dist/src/overrides.js +41 -10
  87. package/dist/src/partial.js +2 -1
  88. package/dist/src/paths.js +26 -15
  89. package/dist/src/position.js +2 -1
  90. package/dist/src/print.js +11 -78
  91. package/dist/src/product.js +13 -0
  92. package/dist/src/query.js +1 -0
  93. package/dist/src/ref.js +11 -0
  94. package/dist/src/references.js +52 -31
  95. package/dist/src/report-schema.js +51 -0
  96. package/dist/src/report.js +59 -41
  97. package/dist/src/reporters/annotations.js +15 -8
  98. package/dist/src/reporters/sarif.js +16 -10
  99. package/dist/src/reporters/template.js +4 -19
  100. package/dist/src/resolve.js +245 -727
  101. package/dist/src/roots.js +374 -116
  102. package/dist/src/run.js +66 -77
  103. package/dist/src/schema.js +320 -342
  104. package/dist/src/scope.js +38 -115
  105. package/dist/src/serialization-contract.js +649 -29
  106. package/dist/src/session.js +41 -29
  107. package/dist/src/source-files.js +40 -15
  108. package/dist/src/suppress-file.js +34 -29
  109. package/dist/src/suppress-inline.js +21 -4
  110. package/dist/src/suppress.js +40 -56
  111. package/dist/src/sweep.js +32 -17
  112. package/dist/src/symbol-keys.js +105 -6
  113. package/dist/src/template-field.js +9 -19
  114. package/dist/src/test-support.js +119 -18
  115. package/dist/src/text/compare.js +26 -0
  116. package/dist/src/text/glob.js +194 -0
  117. package/dist/src/text/named-files.js +54 -0
  118. package/dist/src/text/shell.js +159 -0
  119. package/dist/src/text/yaml.js +393 -0
  120. package/dist/src/type-errors.js +27 -221
  121. package/dist/src/type-parameter-reads.js +172 -0
  122. package/dist/src/unseen-loads.js +100 -0
  123. package/dist/src/unwrap.js +23 -0
  124. package/dist/src/usage.js +10 -0
  125. package/dist/src/verbs/analyze.js +181 -125
  126. package/dist/src/verbs/describe.js +9 -16
  127. package/dist/src/verbs/explain.js +14 -9
  128. package/dist/src/verbs/print-config.js +2 -1
  129. package/dist/src/verbs/print-projects.js +6 -4
  130. package/dist/src/verbs/print-retained.js +4 -2
  131. package/dist/src/verbs/print-roots.js +4 -2
  132. package/dist/src/verbs/verb.js +1 -5
  133. package/dist/src/verbs/version.js +14 -3
  134. package/dist/src/workflow-steps.js +128 -33
  135. package/jsr.json +1 -1
  136. package/package.json +5 -4
  137. package/src/analysis.ts +333 -457
  138. package/src/augmentations.ts +152 -23
  139. package/src/automatic-types.ts +228 -0
  140. package/src/build-scripts.ts +543 -0
  141. package/src/component-layer.ts +4 -2
  142. package/src/components.ts +9 -10
  143. package/src/config.ts +31 -299
  144. package/src/configuration-files.ts +100 -191
  145. package/src/configured-declarations.ts +18 -23
  146. package/src/conformance-record.ts +2 -2
  147. package/src/consumers.ts +6 -57
  148. package/src/contextual-uses.ts +64 -12
  149. package/src/contract/config.generated.ts +818 -0
  150. package/src/contract/contract.generated.ts +1308 -0
  151. package/src/contract/report.generated.ts +1237 -0
  152. package/src/convention-rows.ts +186 -1
  153. package/src/conventions.ts +166 -90
  154. package/src/decorator.ts +28 -18
  155. package/src/dependencies.ts +356 -78
  156. package/src/edges.ts +41 -140
  157. package/src/element-access.ts +4 -7
  158. package/src/entry-point-gaps.ts +11 -20
  159. package/src/entry-points.ts +124 -28
  160. package/src/enum-group.ts +2 -11
  161. package/src/erased-stores.ts +3 -7
  162. package/src/exempt-classes.ts +1 -75
  163. package/src/exempt.ts +36 -50
  164. package/src/failure.ts +63 -0
  165. package/src/file-facts.ts +9 -24
  166. package/src/finding.ts +24 -9
  167. package/src/findings/boundary.ts +2 -0
  168. package/src/findings/completion.ts +297 -77
  169. package/src/findings/dependencies-and-module-machinery.ts +90 -31
  170. package/src/findings/emitter.ts +8 -9
  171. package/src/findings/emitters.ts +8 -8
  172. package/src/findings/interfaces.ts +4 -6
  173. package/src/findings/intra-function.ts +64 -20
  174. package/src/findings/non-code-artifacts.ts +88 -50
  175. package/src/findings/reads-and-writes.ts +20 -16
  176. package/src/findings/self-check.ts +11 -17
  177. package/src/findings/unused-declarations.ts +65 -28
  178. package/src/findings/visibility-narrowing.ts +51 -19
  179. package/src/findings-pass.ts +10 -19
  180. package/src/framework-lifecycle.ts +7 -7
  181. package/src/fs/document.ts +70 -0
  182. package/src/fs/manifest.ts +92 -0
  183. package/src/fs/tree.ts +110 -0
  184. package/src/generated-file.ts +23 -4
  185. package/src/git-ignore.ts +122 -0
  186. package/src/graph.ts +11 -3
  187. package/src/host.ts +17 -14
  188. package/src/html-entries.ts +6 -33
  189. package/src/import-bindings.ts +128 -0
  190. package/src/in-operands.ts +2 -1
  191. package/src/index.ts +1 -1
  192. package/src/indexed-access.ts +152 -30
  193. package/src/injection-container.ts +1 -1
  194. package/src/interface-satisfaction.ts +33 -13
  195. package/src/intra-function-parts.ts +449 -85
  196. package/src/inventory.ts +73 -27
  197. package/src/javascript-file.ts +37 -0
  198. package/src/json-document.ts +241 -76
  199. package/src/keyof-comparisons.ts +3 -7
  200. package/src/kinds.ts +5 -413
  201. package/src/load/classify.ts +397 -0
  202. package/src/load/diagnostics.ts +144 -0
  203. package/src/load/discover.ts +455 -0
  204. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  205. package/src/load/expected-module.ts +188 -0
  206. package/src/load/load.ts +73 -0
  207. package/src/load/module-lookup.ts +50 -0
  208. package/src/load/plan.ts +504 -0
  209. package/src/load/specifiers.ts +168 -0
  210. package/src/load/synthesize.ts +211 -0
  211. package/src/load/test-globals.ts +251 -0
  212. package/src/load/test-joins.ts +98 -0
  213. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  214. package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
  215. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
  216. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  217. package/src/{workspace.ts → load/workspace.ts} +90 -134
  218. package/src/manifest.ts +81 -75
  219. package/src/matrix.ts +154 -18
  220. package/src/mdx-entries.ts +218 -0
  221. package/src/module-declarations.ts +11 -13
  222. package/src/outside-tests.ts +1 -1
  223. package/src/overrides.ts +51 -12
  224. package/src/partial.ts +3 -2
  225. package/src/paths.ts +27 -15
  226. package/src/position.ts +2 -1
  227. package/src/print.ts +12 -91
  228. package/src/product.ts +18 -0
  229. package/src/query.ts +11 -0
  230. package/src/ref.ts +13 -0
  231. package/src/references.ts +62 -32
  232. package/src/report-schema.ts +68 -0
  233. package/src/report.ts +105 -215
  234. package/src/reporters/annotations.ts +22 -11
  235. package/src/reporters/json.ts +1 -1
  236. package/src/reporters/reporter.ts +2 -4
  237. package/src/reporters/reporters.ts +1 -1
  238. package/src/reporters/sarif.ts +29 -16
  239. package/src/reporters/template.ts +4 -20
  240. package/src/reporters/text.ts +3 -3
  241. package/src/resolve.ts +283 -1111
  242. package/src/roots.ts +490 -137
  243. package/src/run.ts +75 -91
  244. package/src/schema.ts +391 -387
  245. package/src/scope.ts +46 -138
  246. package/src/serialization-contract.ts +818 -26
  247. package/src/session.ts +77 -54
  248. package/src/source-files.ts +67 -17
  249. package/src/suppress-file.ts +36 -34
  250. package/src/suppress-inline.ts +20 -3
  251. package/src/suppress.ts +52 -63
  252. package/src/sweep.ts +53 -18
  253. package/src/symbol-keys.ts +131 -7
  254. package/src/template-field.ts +15 -25
  255. package/src/test-support.ts +155 -18
  256. package/src/text/compare.ts +27 -0
  257. package/src/text/glob.ts +210 -0
  258. package/src/text/named-files.inputs.json +1522 -0
  259. package/src/text/named-files.ts +68 -0
  260. package/src/text/shell.ts +161 -0
  261. package/src/text/yaml.ts +423 -0
  262. package/src/type-errors.ts +35 -273
  263. package/src/type-parameter-reads.ts +227 -0
  264. package/src/unseen-loads.ts +134 -0
  265. package/src/unwrap.ts +34 -0
  266. package/src/usage.ts +10 -0
  267. package/src/verbs/analyze.ts +245 -161
  268. package/src/verbs/describe.ts +14 -15
  269. package/src/verbs/explain.ts +15 -13
  270. package/src/verbs/print-config.ts +2 -1
  271. package/src/verbs/print-projects.ts +6 -4
  272. package/src/verbs/print-retained.ts +4 -2
  273. package/src/verbs/print-roots.ts +4 -2
  274. package/src/verbs/verb.ts +2 -8
  275. package/src/verbs/version.ts +15 -3
  276. package/src/workflow-steps.ts +149 -34
  277. package/dist/src/discover.js +0 -347
  278. package/dist/src/glob.js +0 -65
  279. package/dist/src/pnpm-workspace.js +0 -120
  280. package/dist/src/setup-failure.js +0 -23
  281. package/dist/src/test-joins.js +0 -101
  282. package/dist/src/version.js +0 -7
  283. package/src/discover.ts +0 -510
  284. package/src/glob.ts +0 -67
  285. package/src/pnpm-workspace.ts +0 -135
  286. package/src/setup-failure.ts +0 -39
  287. package/src/test-joins.ts +0 -120
  288. package/src/version.ts +0 -7
@@ -8,15 +8,19 @@
8
8
  */
9
9
 
10
10
  import {
11
+ isArrayLiteralExpression,
11
12
  isCallExpression,
12
13
  isExportDeclaration,
14
+ isExpressionStatement,
13
15
  isExternalModuleReference,
14
16
  isIdentifier,
15
17
  isImportDeclaration,
16
18
  isImportEqualsDeclaration,
17
19
  isMetaProperty,
18
20
  isNamespaceExport,
21
+ isObjectLiteralExpression,
19
22
  isPropertyAccessExpression,
23
+ isPropertyAssignment,
20
24
  isShorthandPropertyAssignment,
21
25
  isStringLiteral,
22
26
  isNoSubstitutionTemplateLiteral,
@@ -25,29 +29,44 @@ import {
25
29
  type Node,
26
30
  type SourceFile,
27
31
  } from "@typescript/native/ast";
32
+ import { automaticTypes, type AutomaticTypes } from "./automatic-types.ts";
28
33
  import {
29
34
  configurationFiles,
30
35
  declaredIn as declaredBeside,
31
36
  dependenciesNamed,
32
37
  moduleStrings,
38
+ outsideChain,
39
+ type OutsideModule,
33
40
  type ParseFile,
34
41
  } from "./configuration-files.ts";
35
42
  import type { FindingPosition } from "./finding.ts";
36
43
  import type { Host } from "./host.ts";
37
44
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
38
- import { scriptTokens } from "./manifest.ts";
39
- 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";
40
53
  import type { DependencySection, Module } from "./ref.ts";
41
54
  import { UNANSWERED } from "./query.ts";
42
55
  import type { Handle, ProjectView } from "./session.ts";
43
- import { sourceFilesOf } from "./source-files.ts";
44
- import { workflowTokens } from "./workflow-steps.ts";
56
+ import { sourceFilesOf, type SourceFiles } from "./source-files.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";
45
63
 
46
- const MANIFEST = "package.json";
47
- const MODULES_DIR = "node_modules";
48
- const MODULES_SEGMENT = `/${MODULES_DIR}/`;
64
+ const MODULES_SEGMENT = `/${INSTALLED_PACKAGES}/`;
49
65
  const TYPES_SCOPE = "@types/";
50
66
 
67
+ /** A script token naming a JavaScript file by a path no expansion changes. */
68
+ const SCRIPT_FILE = /^[^$`{*?[~]+\.(?:js|mjs|cjs)$/u;
69
+
51
70
  /** The manifest sections a dependency is declared in, each by the selector its reference spells. */
52
71
  const SECTIONS: readonly (readonly [string, DependencySection])[] = [
53
72
  ["dependencies", "dependency"],
@@ -72,7 +91,7 @@ interface DeclaredDependency {
72
91
  export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
73
92
  return {
74
93
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
75
- at: new Map([[ANYWHERE, new Set(packages)]]),
94
+ at: new Map([[ANYWHERE, new Map(packages.map((one) => [one, new Set([PROJECT_OWNER])]))]]),
76
95
  unanswered: [],
77
96
  };
78
97
  }
@@ -98,10 +117,12 @@ export interface ProjectNeeds {
98
117
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
99
118
  /**
100
119
  * Per absolute path of the file a use is written in, the packages it names, which a
101
- * dependency only a manifest at or above that file declares serves. The empty key holds
102
- * the uses no one file holds, which every manifest's dependency of that name serves.
120
+ * dependency of the nearest manifest at or above that file declaring the name serves,
121
+ * each with the owners {@link uses} holds it by; an import whose bindings nothing in the
122
+ * file uses holds its package by none. The empty key holds the uses no one file holds,
123
+ * which every manifest's dependency of that name serves.
103
124
  */
104
- readonly at: ReadonlyMap<string, ReadonlySet<string>>;
125
+ readonly at: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
105
126
  /**
106
127
  * The files writing a module specifier the checker left unanswered, each of which may
107
128
  * use any dependency a manifest at or above it declares.
@@ -134,25 +155,18 @@ export interface Dependencies {
134
155
  * written inside it, sorted bytewise; a candidate holding no last use has no entry.
135
156
  */
136
157
  readonly lastUses: ReadonlyMap<string, readonly string[]>;
137
- }
138
-
139
- function isRecord(value: unknown): value is Record<string, unknown> {
140
- return typeof value === "object" && value !== null && !Array.isArray(value);
141
- }
142
-
143
- /** One JSON document's decoded value, or undefined where it cannot be read or parsed. */
144
- function readJSON(host: Host, path: string): { text: string; value: unknown } | undefined {
145
- let text: string;
146
- try {
147
- text = host.readFile(path);
148
- } catch {
149
- return undefined;
150
- }
151
- try {
152
- return { text, value: JSON.parse(text) };
153
- } catch {
154
- return undefined;
155
- }
158
+ /**
159
+ * Per file, by its path below the target root, the declared dependencies whose every use
160
+ * in the target is written in it, sorted bytewise, which deleting the file removes.
161
+ */
162
+ readonly fileLastUses: ReadonlyMap<string, readonly string[]>;
163
+ /**
164
+ * The needed declared dependencies, by {@link declarationKey}, each with the owners
165
+ * {@link ProjectNeeds.uses} holds its uses by, sorted bytewise: none that a command runs,
166
+ * that an unanswered specifier may need, that an import none of whose bindings is used
167
+ * names, or that a dependency so needed names as a peer.
168
+ */
169
+ readonly heldBy: ReadonlyMap<string, readonly string[]>;
156
170
  }
157
171
 
158
172
  /**
@@ -202,6 +216,34 @@ function typePackages(name: string, programPackages: ReadonlySet<string>): reado
202
216
  return [typed, named];
203
217
  }
204
218
 
219
+ /**
220
+ * The strings the `extends` member of one compiler configuration file holds, read as the
221
+ * compiler reads the file, comments and trailing commas allowed; none where it cannot be read.
222
+ */
223
+ function extendsOf(host: Host, path: string, parse: ParseFile): readonly string[] {
224
+ let text: string;
225
+ try {
226
+ text = host.readFile(path);
227
+ } catch {
228
+ return [];
229
+ }
230
+ const [statement] = parse(path, text).statements;
231
+ const document =
232
+ statement !== undefined && isExpressionStatement(statement) ? statement.expression : undefined;
233
+ if (document === undefined || !isObjectLiteralExpression(document)) {
234
+ return [];
235
+ }
236
+ const value = document.properties
237
+ .filter(isPropertyAssignment)
238
+ .findLast(
239
+ (property) => isLiteralText(property.name) && property.name.text === "extends",
240
+ )?.initializer;
241
+ const entries = value !== undefined && isArrayLiteralExpression(value) ? value.elements : [value];
242
+ return entries.flatMap((entry) =>
243
+ entry !== undefined && isLiteralText(entry) ? [entry.text] : [],
244
+ );
245
+ }
246
+
205
247
  /**
206
248
  * The packages the compiler configuration of one project names: its type packages,
207
249
  * its JSX import source, the helper library it imports, its plugins, and every
@@ -212,6 +254,7 @@ function configurationPackages(
212
254
  configFile: string,
213
255
  options: Readonly<Record<string, unknown>>,
214
256
  programPackages: ReadonlySet<string>,
257
+ parse: ParseFile,
215
258
  ): Set<string> {
216
259
  const found = new Set<string>();
217
260
  const add = (name: unknown): void => {
@@ -248,12 +291,7 @@ function configurationPackages(
248
291
  continue;
249
292
  }
250
293
  seen.add(file);
251
- const read = readJSON(host, file);
252
- const extended = isRecord(read?.value) ? read.value["extends"] : undefined;
253
- for (const entry of Array.isArray(extended) ? extended : [extended]) {
254
- if (typeof entry !== "string") {
255
- continue;
256
- }
294
+ for (const entry of extendsOf(host, file, parse)) {
257
295
  if (entry.startsWith(".") || isAbsolutePath(entry)) {
258
296
  const local = resolvePath(dirnamePath(file), entry);
259
297
  pending.push(local.endsWith(".json") ? local : `${local}.json`);
@@ -318,7 +356,7 @@ export function projectNeeds<Brand>(
318
356
  held: Inventory,
319
357
  host: Host,
320
358
  targetRoot: string,
321
- parse?: ParseFile,
359
+ parse: ParseFile,
322
360
  ): ProjectNeeds {
323
361
  const programPackages = new Set<string>();
324
362
  for (const name of project.program.getSourceFileNames()) {
@@ -327,18 +365,19 @@ export function projectNeeds<Brand>(
327
365
  programPackages.add(named);
328
366
  }
329
367
  }
330
- const packages = configurationPackages(
331
- host,
332
- project.configFile,
333
- project.program.getCompilerOptions() as Readonly<Record<string, unknown>>,
334
- programPackages,
335
- );
368
+ const options = project.program.getCompilerOptions() as Readonly<Record<string, unknown>>;
369
+ const packages = configurationPackages(host, project.configFile, options, programPackages, parse);
370
+ const automatic = automaticTypes(host, project.configFile, options, parse);
336
371
  const uses = new Map<string, Set<string>>();
337
- const at = new Map<string, Set<string>>();
372
+ const at = new Map<string, Map<string, Set<string>>>();
338
373
  const use = (owners: readonly string[], named: Iterable<string>, path: string): void => {
339
374
  for (const one of named) {
340
- at.set(path, (at.get(path) ?? new Set()).add(one));
375
+ const inFile = at.get(path) ?? new Map<string, Set<string>>();
376
+ at.set(path, inFile);
377
+ const holders = inFile.get(one) ?? new Set<string>();
378
+ inFile.set(one, holders);
341
379
  for (const owner of owners) {
380
+ holders.add(owner);
342
381
  let set = uses.get(owner);
343
382
  if (set === undefined) {
344
383
  set = new Set();
@@ -355,7 +394,7 @@ export function projectNeeds<Brand>(
355
394
  const inFile = (owners: readonly string[], named: Iterable<string>): void => {
356
395
  use(owners, named, file.fileName);
357
396
  };
358
- if (fileNeeds(project, held, file, programPackages, inFile)) {
397
+ if (fileNeeds(project, held, file, { programPackages, automatic }, inFile)) {
359
398
  unanswered.push(file.fileName);
360
399
  }
361
400
  }
@@ -370,12 +409,8 @@ export function projectNeeds<Brand>(
370
409
  }
371
410
  return names;
372
411
  };
373
- const configuration = configurationFiles(
374
- host,
375
- targetRoot,
376
- sourceFilesOf(project, targetRoot),
377
- parse,
378
- );
412
+ const files = sourceFilesOf(project, targetRoot);
413
+ const configuration = configurationFiles(host, targetRoot, files, parse);
379
414
  for (const { file, manifestDir } of configuration.modules) {
380
415
  const names = declaredAt(joinPath(manifestDir, MANIFEST));
381
416
  for (const { node, text } of moduleStrings(file)) {
@@ -384,15 +419,22 @@ export function projectNeeds<Brand>(
384
419
  }
385
420
  for (const { strings, manifestDirs } of configuration.documents) {
386
421
  for (const manifestDir of manifestDirs) {
387
- const names = declaredAt(joinPath(manifestDir, "package.json"));
422
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
388
423
  for (const text of strings) {
389
424
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
390
425
  }
391
426
  }
392
427
  }
393
- for (const { file, manifestDirs } of configuration.outside) {
428
+ const ranFiles: readonly OutsideModule[] = outsideChain(
429
+ host,
430
+ targetRoot,
431
+ files,
432
+ parse,
433
+ scriptRunFiles(host, targetRoot, files),
434
+ );
435
+ for (const { file, manifestDirs } of [...configuration.outside, ...ranFiles]) {
394
436
  for (const manifestDir of manifestDirs) {
395
- const names = declaredAt(joinPath(manifestDir, "package.json"));
437
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
396
438
  for (const { text } of moduleStrings(file)) {
397
439
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
398
440
  }
@@ -401,6 +443,56 @@ export function projectNeeds<Brand>(
401
443
  return { uses, at, unanswered };
402
444
  }
403
445
 
446
+ /**
447
+ * The JavaScript files no program holds that a script of a manifest at or above one of the
448
+ * project's files runs, each with the directory of that manifest, and those a workflow
449
+ * step or git hook runs, with the target root's: a token naming such a file below the
450
+ * target root and in no `node_modules`, read against the manifest's directory.
451
+ */
452
+ function scriptRunFiles(
453
+ host: Host,
454
+ targetRoot: string,
455
+ files: SourceFiles,
456
+ ): readonly { readonly path: string; readonly manifestDir: string }[] {
457
+ const root = resolvePath(host.workingDirectory(), targetRoot);
458
+ const dirs = new Set<string>([root]);
459
+ for (const name of files.byName.keys()) {
460
+ for (const dir of ancestors(dirnamePath(name), root)) {
461
+ if (dirs.has(dir)) {
462
+ break;
463
+ }
464
+ dirs.add(dir);
465
+ }
466
+ }
467
+ const tokens = new Map<string, ReadonlySet<string>>([
468
+ [root, new Set(workflowTokens(host, root))],
469
+ ]);
470
+ for (const dir of dirs) {
471
+ const read = readDocument(host, joinPath(dir, MANIFEST));
472
+ if (read !== undefined && isRecord(read.value)) {
473
+ const own = scriptTokensOf(read.value);
474
+ tokens.set(dir, dir === root ? new Set([...own, ...(tokens.get(root) ?? [])]) : own);
475
+ }
476
+ }
477
+ const found: { path: string; manifestDir: string }[] = [];
478
+ for (const [manifestDir, held] of tokens) {
479
+ for (const token of held) {
480
+ const path = SCRIPT_FILE.test(token) ? resolvePath(manifestDir, token) : undefined;
481
+ const below = path === undefined ? undefined : relativePath(root, path);
482
+ if (
483
+ path !== undefined &&
484
+ below !== undefined &&
485
+ !inInstalledPackages(below) &&
486
+ !files.byName.has(path) &&
487
+ host.kindOf(path) === "file"
488
+ ) {
489
+ found.push({ path, manifestDir });
490
+ }
491
+ }
492
+ }
493
+ return found;
494
+ }
495
+
404
496
  /** The declarations of the inventory a use written at one node belongs to. */
405
497
  function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly string[] {
406
498
  const parent = node.parent as Node | undefined;
@@ -425,12 +517,47 @@ function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly strin
425
517
  return [PROJECT_OWNER];
426
518
  }
427
519
 
428
- /** Records the needs of one file, and answers whether a specifier of it went unanswered. */
520
+ /**
521
+ * Whether one identifier stands for a value or type of the scope it is written in, rather
522
+ * than naming a member, a property or a label.
523
+ */
524
+ function isScopeName(node: Identifier): boolean {
525
+ const parent = node.parent as Node | undefined;
526
+ if (parent === undefined || isShorthandPropertyAssignment(parent)) {
527
+ return true;
528
+ }
529
+ const named = parent as {
530
+ readonly name?: Node;
531
+ readonly right?: Node;
532
+ readonly propertyName?: Node;
533
+ readonly label?: Node;
534
+ };
535
+ return (
536
+ named.name !== node &&
537
+ (parent.kind !== SyntaxKind.QualifiedName || named.right !== node) &&
538
+ named.propertyName !== node &&
539
+ named.label !== node
540
+ );
541
+ }
542
+
543
+ /** What the packages a project's configuration brings in hold, which a file's needs are read against. */
544
+ interface ProgramTypes {
545
+ /** The packages the program holds a file of. */
546
+ readonly programPackages: ReadonlySet<string>;
547
+ /** What the type packages a compiler would include automatically declare. */
548
+ readonly automatic: AutomaticTypes;
549
+ }
550
+
551
+ /**
552
+ * Records the needs of one file, and answers whether a specifier of it went unanswered. A
553
+ * module specifier or a name of the file's scope that resolves to nothing, and that an
554
+ * automatically included type package declares, uses that package.
555
+ */
429
556
  function fileNeeds<Brand>(
430
557
  project: ProjectView<Brand>,
431
558
  held: Inventory,
432
559
  file: SourceFile,
433
- programPackages: ReadonlySet<string>,
560
+ { programPackages, automatic }: ProgramTypes,
434
561
  use: (owners: readonly string[], named: Iterable<string>) => void,
435
562
  ): boolean {
436
563
  const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
@@ -449,17 +576,22 @@ function fileNeeds<Brand>(
449
576
  }
450
577
  const names = new Set(bindings.map(({ node }) => node.text));
451
578
  const candidates: Identifier[] = [];
579
+ const globals: Identifier[] = [];
452
580
  const visit = (node: Node): void => {
453
- if (isIdentifier(node) && names.has(node.text) && !bound.has(node)) {
454
- candidates.push(node);
581
+ if (isIdentifier(node) && !bound.has(node)) {
582
+ if (names.has(node.text)) {
583
+ candidates.push(node);
584
+ } else if (automatic.globals.has(node.text) && isScopeName(node)) {
585
+ globals.push(node);
586
+ }
455
587
  }
456
588
  node.forEachChild(visit);
457
589
  };
458
- if (names.size > 0) {
590
+ if (names.size > 0 || automatic.globals.size > 0) {
459
591
  visit(file);
460
592
  }
461
593
 
462
- const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
594
+ const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates, ...globals];
463
595
  const handles: Handle<Brand>[] = asked.map((node) => project.handle(node));
464
596
  const resolved = project.symbolsAt(handles);
465
597
 
@@ -471,6 +603,9 @@ function fileNeeds<Brand>(
471
603
  named.add(byName);
472
604
  }
473
605
  const symbol = resolved[index];
606
+ if (symbol === undefined) {
607
+ automatic.modules.get(specifier.text)?.forEach((one) => named.add(one));
608
+ }
474
609
  for (const declaration of typeof symbol === "object" ? symbol.declarations : []) {
475
610
  const byPath = packageOfPath(declaration.path);
476
611
  if (byPath !== undefined) {
@@ -508,7 +643,7 @@ function fileNeeds<Brand>(
508
643
  let symbol = resolved[offset + index];
509
644
  const parent = node.parent as Node | undefined;
510
645
  if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
511
- symbol = project.shorthandValueAt(project.handle(node));
646
+ symbol = project.shorthandValueAt(project.handle(parent));
512
647
  }
513
648
  const named =
514
649
  symbol === UNANSWERED || (unresolvedNames.has(node.text) && symbol === undefined)
@@ -521,6 +656,13 @@ function fileNeeds<Brand>(
521
656
  use(ownersOf(file, held, node), named);
522
657
  }
523
658
  });
659
+ const unresolvedAt = offset + candidates.length;
660
+ globals.forEach((node, index) => {
661
+ const symbol = resolved[unresolvedAt + index];
662
+ if (symbol === undefined || symbol === UNANSWERED) {
663
+ use(ownersOf(file, held, node), automatic.globals.get(node.text) ?? []);
664
+ }
665
+ });
524
666
 
525
667
  for (const reference of file.typeReferenceDirectives) {
526
668
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
@@ -646,8 +788,8 @@ function lineAndColumn(text: string, offset: number): { line: number; column: nu
646
788
 
647
789
  /** The installed copy of one dependency, read where the package manager puts it for the target. */
648
790
  function installedOf(host: Host, root: string, name: string): InstalledDependency | undefined {
649
- for (let dir = root; ; dir = dirnamePath(dir)) {
650
- 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));
651
793
  if (read !== undefined && isRecord(read.value)) {
652
794
  const bin = read.value["bin"];
653
795
  const peers = read.value["peerDependencies"];
@@ -664,10 +806,8 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
664
806
  : [],
665
807
  };
666
808
  }
667
- if (dirnamePath(dir) === dir) {
668
- return undefined;
669
- }
670
809
  }
810
+ return undefined;
671
811
  }
672
812
 
673
813
  /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
@@ -720,6 +860,88 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
720
860
  return found;
721
861
  }
722
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
+
723
945
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
724
946
  function isInside(path: string, dir: string): boolean {
725
947
  return dir === "" || path.startsWith(`${dir}/`);
@@ -731,6 +953,22 @@ function manifestDir(dependency: DeclaredDependency): string {
731
953
  return dir === "." ? "" : dir;
732
954
  }
733
955
 
956
+ /**
957
+ * The declarations of one name a use written at `below` needs: those of the nearest
958
+ * manifest at or above it that declares the name, or every one for a use no file holds.
959
+ */
960
+ function nearestDeclaring(
961
+ declarations: readonly DeclaredDependency[],
962
+ below: string | undefined,
963
+ ): readonly DeclaredDependency[] {
964
+ if (below === undefined) {
965
+ return declarations;
966
+ }
967
+ const enclosing = declarations.filter((dependency) => isInside(below, manifestDir(dependency)));
968
+ const nearest = Math.max(-1, ...enclosing.map((dependency) => manifestDir(dependency).length));
969
+ return enclosing.filter((dependency) => manifestDir(dependency).length === nearest);
970
+ }
971
+
734
972
  /**
735
973
  * Adds to `needed` each declared dependency that the installed manifest of a needed or
736
974
  * run declared dependency names as a peer, until no further one joins: the declaration of
@@ -799,22 +1037,29 @@ export function dependenciesOf(
799
1037
  }
800
1038
  }
801
1039
  const workflow = new Set(workflowTokens(host, targetRoot));
1040
+ const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
802
1041
  const declared: DeclaredDependency[] = [];
803
1042
  const ran = new Set<string>();
804
1043
  const peers = new Map<string, readonly string[]>();
805
1044
  const uninstalledHere = new Set(nested);
806
1045
  for (const [dir, path] of manifests) {
807
- const read = readJSON(host, joinPath(dir, MANIFEST));
1046
+ const read = readDocument(host, joinPath(dir, MANIFEST));
808
1047
  if (read === undefined || !isRecord(read.value)) {
809
1048
  continue;
810
1049
  }
811
1050
  const tokens = scriptTokensOf(read.value);
1051
+ const preloads = preloadsOf(
1052
+ manifestCommands(host, dir, read.value).map(({ command }) => command),
1053
+ );
812
1054
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
813
1055
  const found = installedOf(host, dir, dependency.name);
814
1056
  if (found === undefined && uninstalledHere.has(dir)) {
815
1057
  continue;
816
1058
  }
817
1059
  declared.push(dependency);
1060
+ if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
1061
+ ran.add(declarationKey(dependency));
1062
+ }
818
1063
  if (found === undefined) {
819
1064
  continue;
820
1065
  }
@@ -829,6 +1074,8 @@ export function dependenciesOf(
829
1074
  // A declaration several projects compile is one user, and a use any project makes is
830
1075
  // a use, so the uses are merged by declaration before anything is counted.
831
1076
  const needed = new Set<string>();
1077
+ const live = new Set<string>();
1078
+ const holders = new Map<string, Set<string>>();
832
1079
  const byName = new Map<string, DeclaredDependency[]>();
833
1080
  for (const dependency of declared) {
834
1081
  byName.set(dependency.name, [...(byName.get(dependency.name) ?? []), dependency]);
@@ -841,7 +1088,10 @@ export function dependenciesOf(
841
1088
  const unknownDeclared = declared.filter((dependency) =>
842
1089
  unanswered.some((file) => isInside(file, manifestDir(dependency))),
843
1090
  );
844
- unknownDeclared.forEach((dependency) => needed.add(declarationKey(dependency)));
1091
+ for (const dependency of unknownDeclared) {
1092
+ needed.add(declarationKey(dependency));
1093
+ live.add(declarationKey(dependency));
1094
+ }
845
1095
  const unknown = unknownDeclared.map((dependency) => dependency.name);
846
1096
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
847
1097
  for (const [id, used] of [...project.uses, ...held]) {
@@ -851,16 +1101,31 @@ export function dependenciesOf(
851
1101
  }
852
1102
  for (const [path, named] of project.at) {
853
1103
  const below = path === ANYWHERE ? undefined : relativePath(root, path);
854
- for (const one of named) {
855
- for (const dependency of byName.get(one) ?? []) {
856
- if (below === undefined || isInside(below, manifestDir(dependency))) {
857
- needed.add(declarationKey(dependency));
1104
+ for (const [one, owners] of named) {
1105
+ // An import none of whose bindings is used is no evidence about the code using it.
1106
+ const unheld = owners.size === 0;
1107
+ for (const dependency of nearestDeclaring(byName.get(one) ?? [], below)) {
1108
+ const key = declarationKey(dependency);
1109
+ needed.add(key);
1110
+ if (unheld) {
1111
+ live.add(key);
1112
+ } else {
1113
+ const held = holders.get(key) ?? new Set<string>();
1114
+ owners.forEach((owner) => held.add(owner));
1115
+ holders.set(key, held);
858
1116
  }
859
1117
  }
860
1118
  }
861
1119
  }
862
1120
  }
863
1121
  addNeededPeers(declared, peers, needed, ran);
1122
+ addNeededPeers(declared, peers, live, ran);
1123
+ const heldBy = new Map<string, readonly string[]>();
1124
+ for (const [key, held] of holders) {
1125
+ if (!live.has(key) && !ran.has(key)) {
1126
+ heldBy.set(key, [...held].sort(bytewise));
1127
+ }
1128
+ }
864
1129
  const users = new Map<string, number>();
865
1130
  for (const used of uses.values()) {
866
1131
  used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));
@@ -870,11 +1135,24 @@ export function dependenciesOf(
870
1135
  for (const id of candidates) {
871
1136
  const sole = [...(uses.get(id) ?? [])].filter((one) => users.get(one) === 1 && names.has(one));
872
1137
  if (sole.length > 0) {
873
- lastUses.set(
874
- id,
875
- sole.sort((a, b) => (a < b ? -1 : 1)),
876
- );
1138
+ lastUses.set(id, sole.sort(bytewise));
1139
+ }
1140
+ }
1141
+ const owners = new Map<string, Set<string>>();
1142
+ for (const [owner, used] of uses) {
1143
+ for (const one of used) {
1144
+ if (names.has(one)) {
1145
+ owners.set(one, (owners.get(one) ?? new Set<string>()).add(owner));
1146
+ }
1147
+ }
1148
+ }
1149
+ const fileLastUses = new Map<string, string[]>();
1150
+ for (const [one, held] of [...owners].sort(([a], [b]) => bytewise(a, b))) {
1151
+ const paths = new Set([...held].map((owner) => owner.replace(/:\d+:\d+$/u, "")));
1152
+ const [path] = paths;
1153
+ if (paths.size === 1 && path !== undefined && path !== PROJECT_OWNER) {
1154
+ fileLastUses.set(path, [...(fileLastUses.get(path) ?? []), one]);
877
1155
  }
878
1156
  }
879
- return { declared, needed, ran, unbuilt, lastUses };
1157
+ return { declared, needed, ran, unbuilt, lastUses, fileLastUses, heldBy };
880
1158
  }