@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
package/src/inventory.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import {
2
+ isArrayBindingPattern,
3
+ isBindingElement,
2
4
  isClassDeclaration,
3
5
  isComputedPropertyName,
4
6
  isConstructorDeclaration,
@@ -16,6 +18,7 @@ import {
16
18
  isModuleBlock,
17
19
  isModuleDeclaration,
18
20
  isNamespaceExport,
21
+ isObjectBindingPattern,
19
22
  isParameterDeclaration,
20
23
  isParenthesizedTypeNode,
21
24
  isPrivateIdentifier,
@@ -28,7 +31,9 @@ import {
28
31
  isUnionTypeNode,
29
32
  isVariableStatement,
30
33
  ModifierFlags,
34
+ type BindingElement,
31
35
  type ClassDeclaration,
36
+ type Identifier,
32
37
  type Node,
33
38
  type SourceFile,
34
39
  } from "@typescript/native/ast";
@@ -45,6 +50,7 @@ import {
45
50
  import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
46
51
  import { isAnswered, UNANSWERED } from "./query.ts";
47
52
  import type { Handle, ProjectView } from "./session.ts";
53
+ import { manifestAt } from "./fs/manifest.ts";
48
54
 
49
55
  /**
50
56
  * The symbol inventory of one project: every declaration the target's own files
@@ -168,6 +174,12 @@ export interface Inventory {
168
174
  readonly byName: ReadonlyMap<string, readonly string[]>;
169
175
  /** Each `declare module` declaration whose name is a string literal, by id, to that name. */
170
176
  readonly modules: ReadonlyMap<string, string>;
177
+ /**
178
+ * The ids of the ambient declarations: those of a declaration file, and those a `declare`
179
+ * modifier carries or a declaration carrying one holds. They describe code the target
180
+ * does not implement.
181
+ */
182
+ readonly ambient: ReadonlySet<string>;
171
183
  /**
172
184
  * Each enum member's dependents, by id: the later members of its declaration with no
173
185
  * initializer, up to the first one with an initializer, whose values its deletion moves.
@@ -222,8 +234,6 @@ interface ExportScope {
222
234
  readonly nameNode: Node;
223
235
  }
224
236
 
225
- const MANIFEST = "package.json";
226
-
227
237
  /** One manifest that names a package, and the directory below the root that holds it. */
228
238
  interface Named {
229
239
  readonly name: string;
@@ -244,24 +254,7 @@ export function packageScope(host: Host, root: string): (path: string) => Module
244
254
  const named = new Map<string, Named | undefined>();
245
255
 
246
256
  const manifestName = (dir: string): string | undefined => {
247
- let text: string;
248
- try {
249
- text = host.readFile(joinPath(root, dir, MANIFEST));
250
- } catch {
251
- return undefined;
252
- }
253
- let value: unknown;
254
- try {
255
- value = JSON.parse(text);
256
- } catch {
257
- // A manifest this program cannot read names no package, which is the same
258
- // answer as an absent one: the file is then scoped by the target root.
259
- return undefined;
260
- }
261
- if (typeof value !== "object" || value === null) {
262
- return undefined;
263
- }
264
- const name = (value as Record<string, unknown>)["name"];
257
+ const name = manifestAt(host, joinPath(root, dir))?.["name"];
265
258
  return typeof name === "string" && name !== "" ? name : undefined;
266
259
  };
267
260
 
@@ -296,6 +289,19 @@ function modifiersOf(node: Node): number {
296
289
  return (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
297
290
  }
298
291
 
292
+ /** Whether a declaration is ambient: in a declaration file, or under a `declare` modifier. */
293
+ function isAmbient(file: SourceFile, node: Node): boolean {
294
+ if (file.isDeclarationFile) {
295
+ return true;
296
+ }
297
+ for (let at: Node | undefined = node; at !== undefined; at = at.parent as Node | undefined) {
298
+ if ((modifiersOf(at) & ModifierFlags.Ambient) !== 0) {
299
+ return true;
300
+ }
301
+ }
302
+ return false;
303
+ }
304
+
299
305
  /** The type parameters one node declares, and none for a node that declares none. */
300
306
  function typeParametersOf(node: Node): readonly Node[] {
301
307
  if (
@@ -454,6 +460,37 @@ function visibilityOf(node: Node, name: Node | undefined): Visibility {
454
460
  return "public";
455
461
  }
456
462
 
463
+ /**
464
+ * The binding elements of a destructuring declaration that each declare a name of their
465
+ * own, at any depth, but one with a default, whose removal skips the default's
466
+ * evaluation, and one beside a rest element of its object pattern, whose removal
467
+ * changes what the rest holds.
468
+ */
469
+ function patternElements(
470
+ name: Node,
471
+ ): readonly { readonly element: BindingElement; readonly name: Identifier }[] {
472
+ if (!isObjectBindingPattern(name) && !isArrayBindingPattern(name)) {
473
+ return [];
474
+ }
475
+ const rest =
476
+ isObjectBindingPattern(name) &&
477
+ name.elements.some((element) => element.dotDotDotToken !== undefined);
478
+ return name.elements.flatMap((element) => {
479
+ if (
480
+ !isBindingElement(element) ||
481
+ element.initializer !== undefined ||
482
+ (rest && element.dotDotDotToken === undefined)
483
+ ) {
484
+ return [];
485
+ }
486
+ const bound = element.name;
487
+ if (bound === undefined) {
488
+ return [];
489
+ }
490
+ return isIdentifier(bound) ? [{ element, name: bound }] : patternElements(bound);
491
+ });
492
+ }
493
+
457
494
  /** The name node of one declaration, for the kinds that carry one. */
458
495
  function nameNodeOf(node: Node): Node | undefined {
459
496
  return (node as { readonly name?: Node }).name;
@@ -781,15 +818,19 @@ export function inventory<Brand>(
781
818
  if (isVariableStatement(node)) {
782
819
  for (const declaration of node.declarationList.declarations) {
783
820
  const component = componentOf(declaration.name);
784
- if (component === undefined) {
785
- // A destructuring pattern declares binding elements, which the reference
786
- // grammar does not represent; the statement stands for them.
821
+ if (component !== undefined) {
822
+ declare(declaration, owner, "variable", declaration.name, {
823
+ component,
824
+ localName: undefined,
825
+ });
787
826
  continue;
788
827
  }
789
- declare(declaration, owner, "variable", declaration.name, {
790
- component,
791
- localName: undefined,
792
- });
828
+ for (const { element, name } of patternElements(declaration.name)) {
829
+ declare(element, owner, "variable", name, {
830
+ component: { text: name.text, computed: false },
831
+ localName: undefined,
832
+ });
833
+ }
793
834
  }
794
835
  return;
795
836
  }
@@ -1067,6 +1108,11 @@ export function inventory<Brand>(
1067
1108
  .map((symbol) => symbol.id),
1068
1109
  byName: namesOf(building),
1069
1110
  modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
1111
+ ambient: new Set(
1112
+ building
1113
+ .filter((record) => isAmbient(record.file, record.node))
1114
+ .map((record) => positionKey(record.position)),
1115
+ ),
1070
1116
  dependents,
1071
1117
  };
1072
1118
  }
@@ -0,0 +1,37 @@
1
+ export const JAVASCRIPT_FILE = /\.(?:js|jsx|mjs|cjs)$/u;
2
+
3
+ /** A minified file by its name; any other is one by its size and its mean line length. */
4
+ const MINIFIED_NAME = /\.min\.(?:js|mjs|cjs)$/u;
5
+ const MINIFIED_BYTES = 10_240;
6
+ const MINIFIED_LINE = 500;
7
+
8
+ /** Whether one name is a minified file's, which decides it without its text. */
9
+ export function minifiedByName(fileName: string): boolean {
10
+ return MINIFIED_NAME.test(fileName);
11
+ }
12
+
13
+ /** Whether one JavaScript file is minified: by its name, or by its text's size and mean line length. */
14
+ export function minified(fileName: string, text: string): boolean {
15
+ if (!JAVASCRIPT_FILE.test(fileName)) {
16
+ return false;
17
+ }
18
+ if (minifiedByName(fileName)) {
19
+ return true;
20
+ }
21
+ // A UTF-16 unit encodes to at most three UTF-8 bytes, so a shorter text is below the size.
22
+ if (text.length * 3 < MINIFIED_BYTES) {
23
+ return false;
24
+ }
25
+ const lines = text.split(/\r?\n/u).length;
26
+ return utf8Bytes(text) >= MINIFIED_BYTES && text.length / lines > MINIFIED_LINE;
27
+ }
28
+
29
+ /** The length of a text in UTF-8, a lone surrogate counting as the three bytes of U+FFFD. */
30
+ function utf8Bytes(text: string): number {
31
+ let bytes = 0;
32
+ for (const character of text) {
33
+ const point = character.codePointAt(0) ?? 0;
34
+ bytes += point < 0x80 ? 1 : point < 0x800 ? 2 : point < 0x10000 ? 3 : 4;
35
+ }
36
+ return bytes;
37
+ }
@@ -1,122 +1,276 @@
1
+ import type { Position } from "./position.ts";
2
+
3
+ /** A place inside a document: each member name a string, each array index a number. */
4
+ export type Place = readonly (string | number)[];
5
+
1
6
  /**
2
- * A read of a strict JSON document beside its parse: what a parse into a value cannot
3
- * report, the offset of each record of one array and a member written twice.
7
+ * A text that is not one strict JSON document: a syntax error, data after the value, a
8
+ * member one object writes twice, or nesting past {@link MAX_DEPTH}. `place` is the
9
+ * value the fault is in, followed by the member written twice.
4
10
  */
11
+ export class StrictJSONError extends Error {
12
+ readonly offset: number;
13
+ readonly place: Place;
5
14
 
6
- import type { Position } from "./position.ts";
15
+ constructor(reason: string, offset: number, place: Place) {
16
+ super(reason);
17
+ this.name = "StrictJSONError";
18
+ this.offset = offset;
19
+ this.place = place;
20
+ }
21
+ }
7
22
 
8
- /** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
9
- class Walk {
10
- readonly starts: number[] = [];
11
- /** The member written twice, where one is, as the offset of its name. */
12
- twice: { readonly name: string; readonly offset: number } | undefined;
13
- private readonly text: string;
14
- private readonly array: string;
23
+ /** One strict JSON document, read whole. */
24
+ interface StrictDocument {
25
+ readonly value: unknown;
26
+ /**
27
+ * The text of every number written with a fraction or an exponent, by its JSON
28
+ * Pointer: a value reads `100.0` as the integer 100, so only the text tells them apart.
29
+ */
30
+ readonly fractions: ReadonlyMap<string, string>;
31
+ /**
32
+ * The offset of each value inside the top-level member a read names, by its index or
33
+ * its member name, in document order.
34
+ */
35
+ readonly starts: ReadonlyMap<string | number, number>;
36
+ }
37
+
38
+ /**
39
+ * How deep values may nest. A document this analyzer reads nests a few levels, and a
40
+ * bound turns a document built to exhaust the stack into a refusal.
41
+ */
42
+ const MAX_DEPTH = 1000;
43
+
44
+ const SPACE = /[ \t\n\r]*/uy;
45
+ const STRING =
46
+ /"(?:[ !#$%&'()*+,\-./0-9:;<=>?@A-Z[\]-\u{10FFFF}]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"/uy;
47
+ const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/uy;
48
+ const LITERALS: ReadonlyMap<string, unknown> = new Map<string, unknown>([
49
+ ["true", true],
50
+ ["false", false],
51
+ ["null", null],
52
+ ]);
53
+
54
+ /** One member name spelled as a JSON Pointer token. */
55
+ const escaped = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
56
+
57
+ /** The member names and indexes a JSON Pointer holds, unescaped. */
58
+ export function pointerSegments(pointer: string): string[] {
59
+ return pointer === ""
60
+ ? []
61
+ : pointer
62
+ .slice(1)
63
+ .split("/")
64
+ .map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
65
+ }
66
+
67
+ /** The JSON Pointer of a place written as segments. */
68
+ export function pointerOf(segments: readonly string[]): string {
69
+ return segments.map((segment) => `/${escaped(segment)}`).join("");
70
+ }
71
+
72
+ /**
73
+ * The place a JSON Pointer names inside `value`, followed by `member` where it is not
74
+ * empty: a pointer's text does not say whether a segment of digits is an index, so the
75
+ * value it points into does.
76
+ */
77
+ export function placeIn(value: unknown, pointer: string, member = ""): (string | number)[] {
78
+ const place: (string | number)[] = [];
79
+ let held = value;
80
+ for (const segment of pointerSegments(pointer)) {
81
+ if (Array.isArray(held)) {
82
+ place.push(Number(segment));
83
+ held = held[Number(segment)];
84
+ } else {
85
+ place.push(segment);
86
+ held = isRecord(held) ? held[segment] : undefined;
87
+ }
88
+ }
89
+ return member === "" ? place : [...place, member];
90
+ }
91
+
92
+ /**
93
+ * A place as a refusal names it: member names joined by full stops and an index in
94
+ * square brackets, as `ts.serializers[0].returns`.
95
+ */
96
+ export function dotted(place: Place): string {
97
+ let path = "";
98
+ for (const segment of place) {
99
+ path +=
100
+ typeof segment === "number" ? `[${String(segment)}]` : path === "" ? segment : `.${segment}`;
101
+ }
102
+ return path;
103
+ }
104
+
105
+ class Reader {
15
106
  private at = 0;
107
+ private readonly path: (string | number)[] = [];
108
+ readonly fractions = new Map<string, string>();
109
+ readonly starts = new Map<string | number, number>();
110
+ private readonly text: string;
111
+ private readonly records: string | undefined;
16
112
 
17
- constructor(text: string, array: string) {
113
+ constructor(text: string, records: string | undefined) {
18
114
  this.text = text;
19
- this.array = array;
115
+ this.records = records;
20
116
  }
21
117
 
22
- run(): void {
23
- this.value("document");
118
+ read(): unknown {
119
+ const value = this.value(0);
120
+ this.match(SPACE);
121
+ if (this.at < this.text.length) {
122
+ this.fail("data follows the document's value");
123
+ }
124
+ return value;
24
125
  }
25
126
 
26
- private skip(): void {
27
- while (/[ \t\r\n]/u.test(this.text[this.at] ?? "")) {
28
- this.at += 1;
29
- }
127
+ private fail(reason: string, offset = this.at, member = ""): never {
128
+ throw new StrictJSONError(
129
+ reason,
130
+ offset,
131
+ member === "" ? [...this.path] : [...this.path, member],
132
+ );
30
133
  }
31
134
 
32
- private string(): string {
33
- const start = this.at;
34
- this.at += 1;
35
- while (this.text[this.at] !== '"') {
36
- this.at += this.text[this.at] === "\\" ? 2 : 1;
135
+ private match(pattern: RegExp): string | undefined {
136
+ pattern.lastIndex = this.at;
137
+ const found = pattern.exec(this.text)?.[0];
138
+ this.at = found === undefined ? this.at : pattern.lastIndex;
139
+ return found;
140
+ }
141
+
142
+ private expect(char: string, what: string): void {
143
+ this.match(SPACE);
144
+ if (this.text[this.at] !== char) {
145
+ this.fail(`${what} is expected`);
37
146
  }
38
147
  this.at += 1;
39
- return JSON.parse(this.text.slice(start, this.at)) as string;
40
148
  }
41
149
 
42
- private value(role: "document" | "array" | "record" | "other"): void {
43
- this.skip();
44
- if (role === "record") {
45
- this.starts.push(this.at);
150
+ private value(depth: number): unknown {
151
+ this.match(SPACE);
152
+ if (depth > MAX_DEPTH) {
153
+ this.fail(`values nest deeper than ${String(MAX_DEPTH)} levels`);
46
154
  }
47
155
  const char = this.text[this.at];
48
156
  if (char === "{") {
49
- this.object(role);
50
- } else if (char === "[") {
51
- this.list(role === "array" ? "record" : "other");
52
- } else if (char === '"') {
53
- this.string();
54
- } else {
55
- while (this.at < this.text.length && !/[,\]} \t\r\n]/u.test(this.text[this.at] ?? "")) {
56
- this.at += 1;
157
+ return this.object(depth);
158
+ }
159
+ if (char === "[") {
160
+ return this.array(depth);
161
+ }
162
+ const string = this.match(STRING);
163
+ if (string !== undefined) {
164
+ return JSON.parse(string) as string;
165
+ }
166
+ NUMBER.lastIndex = this.at;
167
+ const number = NUMBER.exec(this.text);
168
+ if (number !== null) {
169
+ this.at = NUMBER.lastIndex;
170
+ if (number[1] !== undefined || number[2] !== undefined) {
171
+ this.fractions.set(pointerOf(this.path.map(String)), number[0]);
57
172
  }
173
+ return Number(number[0]);
58
174
  }
175
+ for (const [literal, held] of LITERALS) {
176
+ if (this.text.startsWith(literal, this.at)) {
177
+ this.at += literal.length;
178
+ return held;
179
+ }
180
+ }
181
+ return this.fail(
182
+ char === undefined ? "the document ends where a value is expected" : "a value is expected",
183
+ );
59
184
  }
60
185
 
61
- private object(role: string): void {
62
- const seen = new Set<string>();
186
+ private object(depth: number): Record<string, unknown> {
187
+ const held: Record<string, unknown> = {};
63
188
  this.at += 1;
189
+ this.match(SPACE);
190
+ if (this.text[this.at] === "}") {
191
+ this.at += 1;
192
+ return held;
193
+ }
64
194
  for (;;) {
65
- this.skip();
66
- if (this.text[this.at] === "}") {
67
- this.at += 1;
68
- return;
195
+ this.match(SPACE);
196
+ const offset = this.at;
197
+ const spelled = this.match(STRING);
198
+ if (spelled === undefined) {
199
+ return this.fail("a member name is expected");
69
200
  }
70
- if (this.text[this.at] === ",") {
71
- this.at += 1;
72
- this.skip();
201
+ const name = JSON.parse(spelled) as string;
202
+ if (Object.hasOwn(held, name)) {
203
+ this.fail(
204
+ `the member ${JSON.stringify(name)} is written twice, so neither value is chosen`,
205
+ offset,
206
+ name,
207
+ );
73
208
  }
74
- const offset = this.at;
75
- const name = this.string();
76
- if (seen.has(name) && this.twice === undefined) {
77
- this.twice = { name, offset };
209
+ this.expect(":", "a colon after the member name");
210
+ if (depth === 1 && this.path[0] === this.records) {
211
+ this.match(SPACE);
212
+ this.starts.set(name, this.at);
78
213
  }
79
- seen.add(name);
80
- this.skip();
214
+ this.path.push(name);
215
+ Object.defineProperty(held, name, {
216
+ value: this.value(depth + 1),
217
+ enumerable: true,
218
+ writable: true,
219
+ configurable: true,
220
+ });
221
+ this.path.pop();
222
+ this.match(SPACE);
223
+ const next = this.text[this.at];
81
224
  this.at += 1;
82
- this.value(role === "document" && name === this.array ? "array" : "other");
225
+ if (next === "}") {
226
+ return held;
227
+ }
228
+ if (next !== ",") {
229
+ this.at -= 1;
230
+ this.fail("a comma or a closing brace is expected");
231
+ }
83
232
  }
84
233
  }
85
234
 
86
- private list(element: "record" | "other"): void {
235
+ private array(depth: number): unknown[] {
236
+ const held: unknown[] = [];
237
+ const records = depth === 1 && this.path[0] === this.records;
87
238
  this.at += 1;
239
+ this.match(SPACE);
240
+ if (this.text[this.at] === "]") {
241
+ this.at += 1;
242
+ return held;
243
+ }
88
244
  for (;;) {
89
- this.skip();
90
- if (this.text[this.at] === "]") {
91
- this.at += 1;
92
- return;
245
+ this.match(SPACE);
246
+ if (records) {
247
+ this.starts.set(held.length, this.at);
248
+ }
249
+ this.path.push(held.length);
250
+ held.push(this.value(depth + 1));
251
+ this.path.pop();
252
+ this.match(SPACE);
253
+ const next = this.text[this.at];
254
+ this.at += 1;
255
+ if (next === "]") {
256
+ return held;
93
257
  }
94
- if (this.text[this.at] === ",") {
95
- this.at += 1;
258
+ if (next !== ",") {
259
+ this.at -= 1;
260
+ this.fail("a comma or a closing bracket is expected");
96
261
  }
97
- this.value(element);
98
262
  }
99
263
  }
100
264
  }
101
265
 
102
- /** What a walk of one document found. */
103
- interface DocumentWalk {
104
- /** The offset of each element of the document's array member, in order: an object's `{`. */
105
- readonly starts: readonly number[];
106
- /** The first member one object writes twice, at any depth, as the offset of its name. */
107
- readonly twice: { readonly name: string; readonly offset: number } | undefined;
108
- }
109
-
110
266
  /**
111
- * Walks a document `JSON.parse` accepted, whose top-level member `array` holds the
112
- * records. A member one object writes twice reads as though it were written once, its
113
- * later value silently replacing the earlier one, so a document holding one is one a
114
- * reader names rather than reads.
267
+ * Reads `text` as one strict JSON document, or throws a {@link StrictJSONError}.
268
+ * `records` names the top-level member whose values' offsets the read keeps.
115
269
  */
116
- export function walkDocument(text: string, array: string): DocumentWalk {
117
- const walk = new Walk(text, array);
118
- walk.run();
119
- return { starts: walk.starts, twice: walk.twice };
270
+ export function readStrict(text: string, records?: string): StrictDocument {
271
+ const reader = new Reader(text, records);
272
+ const value = reader.read();
273
+ return { value, fractions: reader.fractions, starts: reader.starts };
120
274
  }
121
275
 
122
276
  /**
@@ -128,3 +282,14 @@ export function positionAt(text: string, file: string, offset: number): Position
128
282
  const lineStart = before.lastIndexOf("\n") + 1;
129
283
  return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
130
284
  }
285
+
286
+ /** A strict read's fault with the line and column it is at, as a refusal states it. */
287
+ export function locatedFault(text: string, error: StrictJSONError): string {
288
+ const { line, column } = positionAt(text, "", error.offset);
289
+ return `line ${String(line)}, column ${String(column)}: ${error.message}`;
290
+ }
291
+
292
+ /** Whether a decoded JSON value is an object: not `null` and not an array. */
293
+ export function isRecord(value: unknown): value is Record<string, unknown> {
294
+ return typeof value === "object" && value !== null && !Array.isArray(value);
295
+ }
@@ -10,7 +10,6 @@ import {
10
10
  isCaseClause,
11
11
  isElementAccessExpression,
12
12
  isIdentifier,
13
- isParenthesizedExpression,
14
13
  isPropertyAccessExpression,
15
14
  isSwitchStatement,
16
15
  isTypeOperatorNode,
@@ -27,6 +26,7 @@ import { renderPosition } from "./position.ts";
27
26
  import { UNANSWERED } from "./query.ts";
28
27
  import type { Reference } from "./references.ts";
29
28
  import type { ProjectView } from "./session.ts";
29
+ import { parenthesesRemoved } from "./unwrap.ts";
30
30
 
31
31
  /** The operators that compare their operands for equality. */
32
32
  const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
@@ -44,13 +44,9 @@ interface Comparison {
44
44
  readonly other: Expression;
45
45
  }
46
46
 
47
- function unwrapped(node: Expression): Expression {
48
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
49
- }
50
-
51
47
  /** The name node an operand's symbol is read at: an identifier, or a property access's name. */
52
48
  function nameOf(node: Expression): Node | undefined {
53
- const at = unwrapped(node);
49
+ const at = parenthesesRemoved(node);
54
50
  if (isIdentifier(at)) {
55
51
  return at;
56
52
  }
@@ -132,7 +128,7 @@ export function keyofComparisonReferences<Brand>(
132
128
  }
133
129
  const named = comparisons.flatMap((one) => {
134
130
  const name = nameOf(one.keyed);
135
- return name === undefined || isElementAccessExpression(unwrapped(one.other))
131
+ return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
136
132
  ? []
137
133
  : [{ ...one, name }];
138
134
  });