@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8

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 (193) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +24 -5
  3. package/dist/src/analysis.js +37 -56
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +6 -82
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/configured-declarations.js +15 -20
  11. package/dist/src/consumers.js +2 -2
  12. package/dist/src/contract/config.generated.js +544 -0
  13. package/dist/src/contract/contract.generated.js +13 -0
  14. package/dist/src/contract/report.generated.js +3 -2
  15. package/dist/src/conventions.js +20 -60
  16. package/dist/src/dependencies.js +25 -43
  17. package/dist/src/discover.js +14 -40
  18. package/dist/src/edges.js +38 -122
  19. package/dist/src/element-access.js +4 -6
  20. package/dist/src/entry-point-gaps.js +2 -8
  21. package/dist/src/entry-points.js +3 -10
  22. package/dist/src/enum-group.js +3 -10
  23. package/dist/src/erased-stores.js +4 -6
  24. package/dist/src/exempt-classes.js +0 -4
  25. package/dist/src/exempt.js +6 -22
  26. package/dist/src/file-facts.js +2 -25
  27. package/dist/src/findings/completion.js +5 -4
  28. package/dist/src/findings/emitters.js +1 -1
  29. package/dist/src/findings/intra-function.js +1 -1
  30. package/dist/src/findings/non-code-artifacts.js +2 -2
  31. package/dist/src/findings/reads-and-writes.js +2 -1
  32. package/dist/src/findings/self-check.js +3 -2
  33. package/dist/src/findings/unused-declarations.js +1 -1
  34. package/dist/src/findings/visibility-narrowing.js +2 -2
  35. package/dist/src/findings-pass.js +7 -12
  36. package/dist/src/framework-lifecycle.js +7 -7
  37. package/dist/src/fs/document.js +59 -0
  38. package/dist/src/fs/manifest.js +43 -0
  39. package/dist/src/fs/tree.js +88 -0
  40. package/dist/src/git-ignore.js +34 -25
  41. package/dist/src/html-entries.js +6 -33
  42. package/dist/src/in-operands.js +2 -1
  43. package/dist/src/injection-container.js +1 -1
  44. package/dist/src/intra-function-parts.js +10 -13
  45. package/dist/src/inventory.js +2 -21
  46. package/dist/src/json-document.js +198 -67
  47. package/dist/src/keyof-comparisons.js +4 -6
  48. package/dist/src/kinds.js +0 -7
  49. package/dist/src/manifest.js +5 -43
  50. package/dist/src/mdx-entries.js +3 -26
  51. package/dist/src/partial.js +2 -1
  52. package/dist/src/paths.js +26 -15
  53. package/dist/src/position.js +2 -1
  54. package/dist/src/print.js +11 -79
  55. package/dist/src/product.js +13 -0
  56. package/dist/src/references.js +14 -27
  57. package/dist/src/report-schema.js +3 -225
  58. package/dist/src/report.js +22 -31
  59. package/dist/src/reporters/sarif.js +4 -2
  60. package/dist/src/reporters/template.js +4 -19
  61. package/dist/src/resolve.js +245 -724
  62. package/dist/src/roots.js +7 -17
  63. package/dist/src/run.js +55 -61
  64. package/dist/src/schema.js +320 -343
  65. package/dist/src/scope.js +34 -103
  66. package/dist/src/serialization-contract.js +5 -9
  67. package/dist/src/session.js +2 -1
  68. package/dist/src/suppress-file.js +21 -25
  69. package/dist/src/suppress.js +3 -3
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +9 -19
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/usage.js +10 -0
  82. package/dist/src/verbs/analyze.js +25 -26
  83. package/dist/src/verbs/describe.js +4 -4
  84. package/dist/src/verbs/explain.js +8 -6
  85. package/dist/src/verbs/print-projects.js +4 -3
  86. package/dist/src/verbs/print-retained.js +2 -1
  87. package/dist/src/verbs/print-roots.js +2 -1
  88. package/dist/src/verbs/version.js +2 -1
  89. package/dist/src/workflow-steps.js +9 -46
  90. package/dist/src/workspace-manifests.js +2 -2
  91. package/dist/src/workspace-programs.js +5 -3
  92. package/dist/src/workspace-resolution.js +13 -17
  93. package/dist/src/workspace.js +28 -116
  94. package/jsr.json +1 -1
  95. package/package.json +1 -1
  96. package/src/analysis.ts +55 -76
  97. package/src/automatic-types.ts +11 -29
  98. package/src/build-scripts.ts +17 -38
  99. package/src/component-layer.ts +4 -2
  100. package/src/components.ts +2 -4
  101. package/src/config.ts +31 -296
  102. package/src/configuration-files.ts +27 -170
  103. package/src/configured-declarations.ts +18 -23
  104. package/src/consumers.ts +4 -4
  105. package/src/contract/config.generated.ts +798 -0
  106. package/src/contract/contract.generated.ts +25 -0
  107. package/src/contract/report.generated.ts +3 -2
  108. package/src/conventions.ts +26 -74
  109. package/src/dependencies.ts +32 -50
  110. package/src/discover.ts +15 -40
  111. package/src/edges.ts +41 -140
  112. package/src/element-access.ts +4 -7
  113. package/src/entry-point-gaps.ts +3 -9
  114. package/src/entry-points.ts +2 -13
  115. package/src/enum-group.ts +2 -11
  116. package/src/erased-stores.ts +3 -7
  117. package/src/exempt-classes.ts +0 -5
  118. package/src/exempt.ts +8 -30
  119. package/src/file-facts.ts +6 -24
  120. package/src/findings/completion.ts +5 -6
  121. package/src/findings/emitter.ts +2 -2
  122. package/src/findings/emitters.ts +1 -1
  123. package/src/findings/intra-function.ts +1 -1
  124. package/src/findings/non-code-artifacts.ts +4 -4
  125. package/src/findings/reads-and-writes.ts +2 -1
  126. package/src/findings/self-check.ts +3 -2
  127. package/src/findings/unused-declarations.ts +1 -1
  128. package/src/findings/visibility-narrowing.ts +2 -2
  129. package/src/findings-pass.ts +9 -15
  130. package/src/framework-lifecycle.ts +7 -7
  131. package/src/fs/document.ts +74 -0
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/host.ts +12 -5
  136. package/src/html-entries.ts +6 -33
  137. package/src/in-operands.ts +2 -1
  138. package/src/injection-container.ts +1 -1
  139. package/src/intra-function-parts.ts +10 -14
  140. package/src/inventory.ts +2 -20
  141. package/src/json-document.ts +241 -76
  142. package/src/keyof-comparisons.ts +3 -7
  143. package/src/kinds.ts +0 -8
  144. package/src/manifest.ts +5 -46
  145. package/src/mdx-entries.ts +3 -26
  146. package/src/partial.ts +2 -1
  147. package/src/paths.ts +27 -15
  148. package/src/position.ts +2 -1
  149. package/src/print.ts +12 -92
  150. package/src/product.ts +18 -0
  151. package/src/references.ts +14 -29
  152. package/src/report-schema.ts +3 -301
  153. package/src/report.ts +24 -34
  154. package/src/reporters/sarif.ts +4 -4
  155. package/src/reporters/template.ts +4 -20
  156. package/src/resolve.ts +283 -1114
  157. package/src/roots.ts +7 -20
  158. package/src/run.ts +57 -75
  159. package/src/schema.ts +391 -388
  160. package/src/scope.ts +40 -126
  161. package/src/serialization-contract.ts +5 -10
  162. package/src/session.ts +2 -1
  163. package/src/suppress-file.ts +21 -29
  164. package/src/suppress.ts +3 -3
  165. package/src/symbol-keys.ts +2 -5
  166. package/src/template-field.ts +15 -25
  167. package/src/test-globals.ts +9 -27
  168. package/src/test-joins.ts +6 -8
  169. package/src/text/compare.ts +27 -0
  170. package/src/text/glob.ts +210 -0
  171. package/src/text/shell.ts +161 -0
  172. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  173. package/src/type-errors.ts +25 -36
  174. package/src/unseen-loads.ts +1 -12
  175. package/src/unwrap.ts +34 -0
  176. package/src/usage.ts +10 -0
  177. package/src/verbs/analyze.ts +32 -42
  178. package/src/verbs/describe.ts +4 -4
  179. package/src/verbs/explain.ts +8 -10
  180. package/src/verbs/print-projects.ts +4 -3
  181. package/src/verbs/print-retained.ts +2 -1
  182. package/src/verbs/print-roots.ts +2 -1
  183. package/src/verbs/verb.ts +2 -2
  184. package/src/verbs/version.ts +2 -1
  185. package/src/workflow-steps.ts +9 -44
  186. package/src/workspace-manifests.ts +2 -2
  187. package/src/workspace-programs.ts +5 -3
  188. package/src/workspace-resolution.ts +21 -17
  189. package/src/workspace.ts +35 -121
  190. package/dist/src/glob.js +0 -65
  191. package/dist/src/pnpm-workspace.js +0 -120
  192. package/src/glob.ts +0 -67
  193. package/src/pnpm-workspace.ts +0 -135
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Which files the `.gitignore` files of a tree ignore, read as git reads them:
2
+ * Which files git ignores below a tree's root, read as git reads its ignore files:
3
3
  * https://git-scm.com/docs/gitignore#_pattern_format
4
4
  */
5
- import { globExpression } from "./glob.js";
6
- import { dirnamePath, joinPath, relativePath } from "./paths.js";
7
- /** The rules one `.gitignore` text writes, in order. */
5
+ import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.js";
6
+ import { gitignoreExpression } from "./text/glob.js";
7
+ import { repositoryOf } from "./fs/tree.js";
8
+ /** The rules one ignore file's text writes, in order. */
8
9
  function rulesOf(text) {
9
10
  const rules = [];
10
11
  for (const raw of text.split(/\r?\n/u)) {
@@ -13,7 +14,7 @@ function rulesOf(text) {
13
14
  continue;
14
15
  }
15
16
  const negated = line.startsWith("!");
16
- line = negated || line.startsWith("\\") ? line.slice(1) : line;
17
+ line = negated ? line.slice(1) : line;
17
18
  const directoryOnly = line.endsWith("/");
18
19
  line = line.replace(/\/+$/u, "");
19
20
  if (line === "") {
@@ -26,43 +27,51 @@ function rulesOf(text) {
26
27
  rules.push({
27
28
  negated,
28
29
  directoryOnly,
29
- expression: globExpression(anchored ? pattern : `**/${pattern}`),
30
+ expression: gitignoreExpression(anchored ? pattern : `**/${pattern}`),
30
31
  });
31
32
  }
32
33
  return rules;
33
34
  }
35
+ function rulesIn(host, path) {
36
+ try {
37
+ return rulesOf(host.readFile(path));
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
34
43
  /**
35
- * Whether the `.gitignore` files at and below `root` ignore one absolute path. A path
36
- * outside `root` is not ignored, and a file below an ignored directory stays ignored
37
- * whatever a later pattern says, as git cannot re-include it.
44
+ * Whether git ignores one absolute path below `root`: by the repository's `info/exclude`,
45
+ * then the `.gitignore` files from the repository's root down to the path's directory, a
46
+ * later pattern overriding an earlier one. A path outside `root` is not ignored, and a file
47
+ * below an ignored directory stays ignored whatever a later pattern says, as git cannot
48
+ * re-include it. With no repository at or above `root`, the files at and below it are read;
49
+ * with one, the directories above `root` give patterns but are not themselves tested.
38
50
  */
39
51
  export function gitIgnored(host, root) {
52
+ const found = repositoryOf(host, root);
53
+ const repository = found?.root;
54
+ const above = repository === undefined ? [] : [...ancestors(dirnamePath(root), repository)].reverse();
55
+ const excluded = found?.exclude === undefined ? [] : rulesIn(host, found.exclude);
40
56
  const read = new Map();
41
57
  const rulesAt = (dir) => {
42
58
  let rules = read.get(dir);
43
59
  if (rules === undefined) {
44
- try {
45
- rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
46
- }
47
- catch {
48
- rules = [];
49
- }
60
+ rules = rulesIn(host, joinPath(dir, ".gitignore"));
50
61
  read.set(dir, rules);
51
62
  }
52
63
  return rules;
53
64
  };
54
- /** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
55
65
  const decided = (path, directory) => {
56
- const dirs = [];
57
- for (let at = dirnamePath(path); relativePath(root, at) !== undefined; at = dirnamePath(at)) {
58
- dirs.unshift(at);
59
- }
60
- dirs.unshift(root);
66
+ const sources = [
67
+ ...(repository === undefined ? [] : [[repository, excluded]]),
68
+ ...[...above, ...[...ancestors(dirnamePath(path), root)].reverse()].map((dir) => [dir, rulesAt(dir)]),
69
+ ];
61
70
  let ignored = false;
62
- for (const dir of dirs) {
63
- const below = relativePath(dir, path);
64
- for (const rule of below === undefined ? [] : rulesAt(dir)) {
65
- if ((directory || !rule.directoryOnly) && rule.expression.test(below ?? "")) {
71
+ for (const [dir, rules] of sources) {
72
+ const below = relativePath(dir, path) ?? "";
73
+ for (const rule of rules) {
74
+ if ((directory || !rule.directoryOnly) && rule.expression.test(below)) {
66
75
  ignored = !rule.negated;
67
76
  }
68
77
  }
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import { isExportDeclaration, isImportDeclaration, isStringLiteral, } from "@typescript/native/ast";
7
7
  import { resolveRelativeSpecifier } from "./configuration-files.js";
8
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
9
- /** The directory no HTML file is read below. */
10
- const SKIPPED = "node_modules";
8
+ import { ancestors, dirnamePath, joinPath, resolvePath } from "./paths.js";
9
+ import { filesBelow } from "./fs/tree.js";
10
+ import { holdsManifest } from "./fs/manifest.js";
11
11
  /** A `<script` start tag, its attribute text captured. */
12
12
  const SCRIPT_TAG = /<script(?=[\s>/])([^>]*)>/giu;
13
13
  /** The end tag an inline module script's content runs to. */
@@ -20,29 +20,6 @@ const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
20
20
  const CLASSIC_TYPES = new Set(["text/javascript", "application/javascript"]);
21
21
  /** The prefixes an inline script's specifier names a file of the target with. */
22
22
  const FILE_PREFIXES = ["/", "./", "../"];
23
- /** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
24
- function htmlFiles(host, dir) {
25
- let entries;
26
- try {
27
- entries = host.readDirectory(dir);
28
- }
29
- catch {
30
- return [];
31
- }
32
- const found = [];
33
- for (const entry of entries) {
34
- const path = joinPath(dir, entry.name);
35
- if (entry.directory) {
36
- if (entry.name !== SKIPPED) {
37
- found.push(...htmlFiles(host, path));
38
- }
39
- }
40
- else if (entry.name.endsWith(".html")) {
41
- found.push(path);
42
- }
43
- }
44
- return found.sort();
45
- }
46
23
  /** Each attribute of one start tag's attribute text, by its lowercase name. */
47
24
  function attributes(text) {
48
25
  const found = new Map();
@@ -98,12 +75,7 @@ function scriptValues(parse, path, text) {
98
75
  }
99
76
  /** The directory of the nearest `package.json` at or above `dir`, inside `root`, else `root`. */
100
77
  function manifestDirectory(host, root, dir) {
101
- for (let at = dir; relativePath(root, at) !== undefined; at = dirnamePath(at)) {
102
- if (host.kindOf(joinPath(at, "package.json")) === "file") {
103
- return at;
104
- }
105
- }
106
- return root;
78
+ return ancestors(dir, root).find((at) => holdsManifest(host, at)) ?? root;
107
79
  }
108
80
  /**
109
81
  * The own files the scripts of every HTML file below the target root load. A value
@@ -115,7 +87,8 @@ function manifestDirectory(host, root, dir) {
115
87
  export function htmlEntries(host, targetRoot, files, parse) {
116
88
  const root = resolvePath(host.workingDirectory(), targetRoot);
117
89
  const found = [];
118
- for (const path of htmlFiles(host, root)) {
90
+ for (const below of filesBelow(host, root, (name) => name.endsWith(".html"))) {
91
+ const path = joinPath(root, below);
119
92
  let text;
120
93
  try {
121
94
  text = host.readFile(path);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { isBinaryExpression, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
7
7
  import { sourceFilesOf } from "./source-files.js";
8
+ import { bytewise } from "./text/compare.js";
8
9
  /**
9
10
  * The text of each string literal, or template literal with no substitution, written as
10
11
  * the left operand of an `in` expression in a file of the project that is no test file.
@@ -26,5 +27,5 @@ export function inOperandNames(project, targetRoot, testFiles) {
26
27
  file.forEachChild(visit);
27
28
  }
28
29
  }
29
- return [...found].sort();
30
+ return [...found].sort(bytewise);
30
31
  }
@@ -24,7 +24,7 @@ function isInjected(node) {
24
24
  /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
25
25
  function registrations(input) {
26
26
  const { project, held } = input;
27
- const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
27
+ const resolved = resolveEntries(project, held, input.ts.injection_registrations);
28
28
  if (!resolved.some(namesADeclaration)) {
29
29
  return [];
30
30
  }
@@ -14,6 +14,7 @@ import { importRef } from "./ref.js";
14
14
  import { isAnswered, UNANSWERED } from "./query.js";
15
15
  import { signatureOf } from "./signatures.js";
16
16
  import { spellsNoBinding } from "./spelled-names.js";
17
+ import { parenthesesRemoved } from "./unwrap.js";
17
18
  /** The result types that carry no value, as the checker spells them. */
18
19
  const NO_VALUE = new Set([
19
20
  "void",
@@ -29,17 +30,13 @@ function spanOf(file, root, node) {
29
30
  const endLine = file.getLineAndCharacterOfPosition(node.end).line + 1;
30
31
  return { ...start, endLine };
31
32
  }
32
- /** The parenthesized expression's content, through every pair of parentheses. */
33
- function unwrapped(node) {
34
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
35
- }
36
33
  /** The node the parentheses around one expression sit in. */
37
34
  function outerParent(node) {
38
35
  return isParenthesizedExpression(node.parent) ? outerParent(node.parent) : node.parent;
39
36
  }
40
37
  /** The name a call is made through: the identifier, or the member a property access names. */
41
38
  function calleeName(call) {
42
- const callee = unwrapped(call.expression);
39
+ const callee = parenthesesRemoved(call.expression);
43
40
  if (isIdentifier(callee)) {
44
41
  return callee;
45
42
  }
@@ -94,7 +91,7 @@ function unreachableExempt(statement) {
94
91
  }
95
92
  /** The value a case expression is a literal of, as a key two equal literals share, or none. */
96
93
  function caseKey(expression) {
97
- const node = unwrapped(expression);
94
+ const node = parenthesesRemoved(expression);
98
95
  if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
99
96
  return `s:${node.text}`;
100
97
  }
@@ -205,7 +202,7 @@ function extendedNames(files) {
205
202
  named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
206
203
  }
207
204
  else {
208
- const expression = unwrapped(type.expression);
205
+ const expression = parenthesesRemoved(type.expression);
209
206
  named = isPropertyAccessExpression(expression) ? expression.name : expression;
210
207
  }
211
208
  if (isIdentifier(named)) {
@@ -249,7 +246,7 @@ function decoratorFixes(constructor) {
249
246
  }
250
247
  /** The name an expression spells a class by: an identifier, or the member a property access names. */
251
248
  function spelledName(expression) {
252
- const node = unwrapped(expression);
249
+ const node = parenthesesRemoved(expression);
253
250
  if (isIdentifier(node)) {
254
251
  return node;
255
252
  }
@@ -442,8 +439,8 @@ export function projectParts(project, held, references, targetRoot) {
442
439
  if ((isImportClause(node) || isNamespaceImport(node)) && node.name !== undefined) {
443
440
  specifiers.add(position(node.name));
444
441
  }
445
- if (isExportAssignment(node) && isIdentifier(unwrapped(node.expression))) {
446
- specifiers.add(position(unwrapped(node.expression)));
442
+ if (isExportAssignment(node) && isIdentifier(parenthesesRemoved(node.expression))) {
443
+ specifiers.add(position(parenthesesRemoved(node.expression)));
447
444
  }
448
445
  if (isSwitchStatement(node)) {
449
446
  const holder = holderOf(node);
@@ -946,7 +943,7 @@ function isPlainWrite(use) {
946
943
  const parent = outerParent(use);
947
944
  return (isBinaryExpression(parent) &&
948
945
  parent.operatorToken.kind === SyntaxKind.EqualsToken &&
949
- unwrapped(parent.left) === use);
946
+ parenthesesRemoved(parent.left) === use);
950
947
  }
951
948
  /**
952
949
  * The writes to the locals of one block that no read reaches. A write is a declaration's
@@ -992,12 +989,12 @@ function deadStores(file, root, block, holder, locals, ids) {
992
989
  if (!isExpressionStatement(statement)) {
993
990
  return undefined;
994
991
  }
995
- const expression = unwrapped(statement.expression);
992
+ const expression = parenthesesRemoved(statement.expression);
996
993
  if (!isBinaryExpression(expression) ||
997
994
  expression.operatorToken.kind !== SyntaxKind.EqualsToken) {
998
995
  return undefined;
999
996
  }
1000
- const target = unwrapped(expression.left);
997
+ const target = parenthesesRemoved(expression.left);
1001
998
  return isIdentifier(target) && uses.includes(target)
1002
999
  ? { target, value: expression.right }
1003
1000
  : undefined;
@@ -3,7 +3,7 @@ import { dirnamePath, joinPath, relativePath } from "./paths.js";
3
3
  import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
4
4
  import { computedComponent, nameComponent, renderRef } from "./ref.js";
5
5
  import { isAnswered, UNANSWERED } from "./query.js";
6
- const MANIFEST = "package.json";
6
+ import { manifestAt } from "./fs/manifest.js";
7
7
  /**
8
8
  * Resolves each file's package scope: the name of the nearest manifest at or above
9
9
  * its directory up to the target root, and the file's path below that manifest.
@@ -17,26 +17,7 @@ const MANIFEST = "package.json";
17
17
  export function packageScope(host, root) {
18
18
  const named = new Map();
19
19
  const manifestName = (dir) => {
20
- let text;
21
- try {
22
- text = host.readFile(joinPath(root, dir, MANIFEST));
23
- }
24
- catch {
25
- return undefined;
26
- }
27
- let value;
28
- try {
29
- value = JSON.parse(text);
30
- }
31
- catch {
32
- // A manifest this program cannot read names no package, which is the same
33
- // answer as an absent one: the file is then scoped by the target root.
34
- return undefined;
35
- }
36
- if (typeof value !== "object" || value === null) {
37
- return undefined;
38
- }
39
- const name = value["name"];
20
+ const name = manifestAt(host, joinPath(root, dir))?.["name"];
40
21
  return typeof name === "string" && name !== "" ? name : undefined;
41
22
  };
42
23
  const nearest = (dir) => {
@@ -1,106 +1,228 @@
1
1
  /**
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.
2
+ * A text that is not one strict JSON document: a syntax error, data after the value, a
3
+ * member one object writes twice, or nesting past {@link MAX_DEPTH}. `place` is the
4
+ * value the fault is in, followed by the member written twice.
4
5
  */
5
- /** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
6
- class Walk {
7
- starts = [];
8
- /** The member written twice, where one is, as the offset of its name. */
9
- twice;
10
- text;
11
- array;
6
+ export class StrictJSONError extends Error {
7
+ offset;
8
+ place;
9
+ constructor(reason, offset, place) {
10
+ super(reason);
11
+ this.name = "StrictJSONError";
12
+ this.offset = offset;
13
+ this.place = place;
14
+ }
15
+ }
16
+ /**
17
+ * How deep values may nest. A document this analyzer reads nests a few levels, and a
18
+ * bound turns a document built to exhaust the stack into a refusal.
19
+ */
20
+ const MAX_DEPTH = 1000;
21
+ const SPACE = /[ \t\n\r]*/uy;
22
+ const STRING = /"(?:[ !#$%&'()*+,\-./0-9:;<=>?@A-Z[\]-\u{10FFFF}]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"/uy;
23
+ const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/uy;
24
+ const LITERALS = new Map([
25
+ ["true", true],
26
+ ["false", false],
27
+ ["null", null],
28
+ ]);
29
+ /** One member name spelled as a JSON Pointer token. */
30
+ const escaped = (name) => name.replaceAll("~", "~0").replaceAll("/", "~1");
31
+ /** The member names and indexes a JSON Pointer holds, unescaped. */
32
+ export function pointerSegments(pointer) {
33
+ return pointer === ""
34
+ ? []
35
+ : pointer
36
+ .slice(1)
37
+ .split("/")
38
+ .map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
39
+ }
40
+ /** The JSON Pointer of a place written as segments. */
41
+ export function pointerOf(segments) {
42
+ return segments.map((segment) => `/${escaped(segment)}`).join("");
43
+ }
44
+ /**
45
+ * The place a JSON Pointer names inside `value`, followed by `member` where it is not
46
+ * empty: a pointer's text does not say whether a segment of digits is an index, so the
47
+ * value it points into does.
48
+ */
49
+ export function placeIn(value, pointer, member = "") {
50
+ const place = [];
51
+ let held = value;
52
+ for (const segment of pointerSegments(pointer)) {
53
+ if (Array.isArray(held)) {
54
+ place.push(Number(segment));
55
+ held = held[Number(segment)];
56
+ }
57
+ else {
58
+ place.push(segment);
59
+ held = isRecord(held) ? held[segment] : undefined;
60
+ }
61
+ }
62
+ return member === "" ? place : [...place, member];
63
+ }
64
+ /**
65
+ * A place as a refusal names it: member names joined by full stops and an index in
66
+ * square brackets, as `ts.serializers[0].returns`.
67
+ */
68
+ export function dotted(place) {
69
+ let path = "";
70
+ for (const segment of place) {
71
+ path +=
72
+ typeof segment === "number" ? `[${String(segment)}]` : path === "" ? segment : `.${segment}`;
73
+ }
74
+ return path;
75
+ }
76
+ class Reader {
12
77
  at = 0;
13
- constructor(text, array) {
78
+ path = [];
79
+ fractions = new Map();
80
+ starts = new Map();
81
+ text;
82
+ records;
83
+ constructor(text, records) {
14
84
  this.text = text;
15
- this.array = array;
16
- }
17
- run() {
18
- this.value("document");
85
+ this.records = records;
19
86
  }
20
- skip() {
21
- while (/[ \t\r\n]/u.test(this.text[this.at] ?? "")) {
22
- this.at += 1;
87
+ read() {
88
+ const value = this.value(0);
89
+ this.match(SPACE);
90
+ if (this.at < this.text.length) {
91
+ this.fail("data follows the document's value");
23
92
  }
93
+ return value;
24
94
  }
25
- string() {
26
- const start = this.at;
27
- this.at += 1;
28
- while (this.text[this.at] !== '"') {
29
- this.at += this.text[this.at] === "\\" ? 2 : 1;
95
+ fail(reason, offset = this.at, member = "") {
96
+ throw new StrictJSONError(reason, offset, member === "" ? [...this.path] : [...this.path, member]);
97
+ }
98
+ match(pattern) {
99
+ pattern.lastIndex = this.at;
100
+ const found = pattern.exec(this.text)?.[0];
101
+ this.at = found === undefined ? this.at : pattern.lastIndex;
102
+ return found;
103
+ }
104
+ expect(char, what) {
105
+ this.match(SPACE);
106
+ if (this.text[this.at] !== char) {
107
+ this.fail(`${what} is expected`);
30
108
  }
31
109
  this.at += 1;
32
- return JSON.parse(this.text.slice(start, this.at));
33
110
  }
34
- value(role) {
35
- this.skip();
36
- if (role === "record") {
37
- this.starts.push(this.at);
111
+ value(depth) {
112
+ this.match(SPACE);
113
+ if (depth > MAX_DEPTH) {
114
+ this.fail(`values nest deeper than ${String(MAX_DEPTH)} levels`);
38
115
  }
39
116
  const char = this.text[this.at];
40
117
  if (char === "{") {
41
- this.object(role);
118
+ return this.object(depth);
42
119
  }
43
- else if (char === "[") {
44
- this.list(role === "array" ? "record" : "other");
120
+ if (char === "[") {
121
+ return this.array(depth);
45
122
  }
46
- else if (char === '"') {
47
- this.string();
123
+ const string = this.match(STRING);
124
+ if (string !== undefined) {
125
+ return JSON.parse(string);
48
126
  }
49
- else {
50
- while (this.at < this.text.length && !/[,\]} \t\r\n]/u.test(this.text[this.at] ?? "")) {
51
- this.at += 1;
127
+ NUMBER.lastIndex = this.at;
128
+ const number = NUMBER.exec(this.text);
129
+ if (number !== null) {
130
+ this.at = NUMBER.lastIndex;
131
+ if (number[1] !== undefined || number[2] !== undefined) {
132
+ this.fractions.set(pointerOf(this.path.map(String)), number[0]);
52
133
  }
134
+ return Number(number[0]);
53
135
  }
136
+ for (const [literal, held] of LITERALS) {
137
+ if (this.text.startsWith(literal, this.at)) {
138
+ this.at += literal.length;
139
+ return held;
140
+ }
141
+ }
142
+ return this.fail(char === undefined ? "the document ends where a value is expected" : "a value is expected");
54
143
  }
55
- object(role) {
56
- const seen = new Set();
144
+ object(depth) {
145
+ const held = {};
57
146
  this.at += 1;
147
+ this.match(SPACE);
148
+ if (this.text[this.at] === "}") {
149
+ this.at += 1;
150
+ return held;
151
+ }
58
152
  for (;;) {
59
- this.skip();
60
- if (this.text[this.at] === "}") {
61
- this.at += 1;
62
- return;
153
+ this.match(SPACE);
154
+ const offset = this.at;
155
+ const spelled = this.match(STRING);
156
+ if (spelled === undefined) {
157
+ return this.fail("a member name is expected");
63
158
  }
64
- if (this.text[this.at] === ",") {
65
- this.at += 1;
66
- this.skip();
159
+ const name = JSON.parse(spelled);
160
+ if (Object.hasOwn(held, name)) {
161
+ this.fail(`the member ${JSON.stringify(name)} is written twice, so neither value is chosen`, offset, name);
67
162
  }
68
- const offset = this.at;
69
- const name = this.string();
70
- if (seen.has(name) && this.twice === undefined) {
71
- this.twice = { name, offset };
163
+ this.expect(":", "a colon after the member name");
164
+ if (depth === 1 && this.path[0] === this.records) {
165
+ this.match(SPACE);
166
+ this.starts.set(name, this.at);
72
167
  }
73
- seen.add(name);
74
- this.skip();
168
+ this.path.push(name);
169
+ Object.defineProperty(held, name, {
170
+ value: this.value(depth + 1),
171
+ enumerable: true,
172
+ writable: true,
173
+ configurable: true,
174
+ });
175
+ this.path.pop();
176
+ this.match(SPACE);
177
+ const next = this.text[this.at];
75
178
  this.at += 1;
76
- this.value(role === "document" && name === this.array ? "array" : "other");
179
+ if (next === "}") {
180
+ return held;
181
+ }
182
+ if (next !== ",") {
183
+ this.at -= 1;
184
+ this.fail("a comma or a closing brace is expected");
185
+ }
77
186
  }
78
187
  }
79
- list(element) {
188
+ array(depth) {
189
+ const held = [];
190
+ const records = depth === 1 && this.path[0] === this.records;
80
191
  this.at += 1;
192
+ this.match(SPACE);
193
+ if (this.text[this.at] === "]") {
194
+ this.at += 1;
195
+ return held;
196
+ }
81
197
  for (;;) {
82
- this.skip();
83
- if (this.text[this.at] === "]") {
84
- this.at += 1;
85
- return;
198
+ this.match(SPACE);
199
+ if (records) {
200
+ this.starts.set(held.length, this.at);
86
201
  }
87
- if (this.text[this.at] === ",") {
88
- this.at += 1;
202
+ this.path.push(held.length);
203
+ held.push(this.value(depth + 1));
204
+ this.path.pop();
205
+ this.match(SPACE);
206
+ const next = this.text[this.at];
207
+ this.at += 1;
208
+ if (next === "]") {
209
+ return held;
210
+ }
211
+ if (next !== ",") {
212
+ this.at -= 1;
213
+ this.fail("a comma or a closing bracket is expected");
89
214
  }
90
- this.value(element);
91
215
  }
92
216
  }
93
217
  }
94
218
  /**
95
- * Walks a document `JSON.parse` accepted, whose top-level member `array` holds the
96
- * records. A member one object writes twice reads as though it were written once, its
97
- * later value silently replacing the earlier one, so a document holding one is one a
98
- * reader names rather than reads.
219
+ * Reads `text` as one strict JSON document, or throws a {@link StrictJSONError}.
220
+ * `records` names the top-level member whose values' offsets the read keeps.
99
221
  */
100
- export function walkDocument(text, array) {
101
- const walk = new Walk(text, array);
102
- walk.run();
103
- return { starts: walk.starts, twice: walk.twice };
222
+ export function readStrict(text, records) {
223
+ const reader = new Reader(text, records);
224
+ const value = reader.read();
225
+ return { value, fractions: reader.fractions, starts: reader.starts };
104
226
  }
105
227
  /**
106
228
  * One offset of a document as a finding carries it: the line counted from one, and the
@@ -111,3 +233,12 @@ export function positionAt(text, file, offset) {
111
233
  const lineStart = before.lastIndexOf("\n") + 1;
112
234
  return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
113
235
  }
236
+ /** A strict read's fault with the line and column it is at, as a refusal states it. */
237
+ export function locatedFault(text, error) {
238
+ const { line, column } = positionAt(text, "", error.offset);
239
+ return `line ${String(line)}, column ${String(column)}: ${error.message}`;
240
+ }
241
+ /** Whether a decoded JSON value is an object: not `null` and not an array. */
242
+ export function isRecord(value) {
243
+ return typeof value === "object" && value !== null && !Array.isArray(value);
244
+ }
@@ -4,11 +4,12 @@
4
4
  * that holds `keyof X`, the literal the other side has names the member of `X` under that
5
5
  * key, because deleting the member makes the comparison a type error.
6
6
  */
7
- import { isBinaryExpression, isCaseClause, isElementAccessExpression, isIdentifier, isParenthesizedExpression, isPropertyAccessExpression, isSwitchStatement, isTypeOperatorNode, SyntaxKind, } from "@typescript/native/ast";
7
+ import { isBinaryExpression, isCaseClause, isElementAccessExpression, isIdentifier, isPropertyAccessExpression, isSwitchStatement, isTypeOperatorNode, SyntaxKind, } from "@typescript/native/ast";
8
8
  import { SymbolFlags } from "@typescript/native/sync";
9
9
  import { nodeKey } from "./inventory.js";
10
10
  import { renderPosition } from "./position.js";
11
11
  import { UNANSWERED } from "./query.js";
12
+ import { parenthesesRemoved } from "./unwrap.js";
12
13
  /** The operators that compare their operands for equality. */
13
14
  const EQUALITIES = new Set([
14
15
  SyntaxKind.EqualsEqualsEqualsToken,
@@ -16,12 +17,9 @@ const EQUALITIES = new Set([
16
17
  SyntaxKind.EqualsEqualsToken,
17
18
  SyntaxKind.ExclamationEqualsToken,
18
19
  ]);
19
- function unwrapped(node) {
20
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
21
- }
22
20
  /** The name node an operand's symbol is read at: an identifier, or a property access's name. */
23
21
  function nameOf(node) {
24
- const at = unwrapped(node);
22
+ const at = parenthesesRemoved(node);
25
23
  if (isIdentifier(at)) {
26
24
  return at;
27
25
  }
@@ -96,7 +94,7 @@ export function keyofComparisonReferences(project, held, chains, root, testFiles
96
94
  }
97
95
  const named = comparisons.flatMap((one) => {
98
96
  const name = nameOf(one.keyed);
99
- return name === undefined || isElementAccessExpression(unwrapped(one.other))
97
+ return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
100
98
  ? []
101
99
  : [{ ...one, name }];
102
100
  });