@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2

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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
@@ -1,4 +1,5 @@
1
1
  import { KINDS } from "../kinds.js";
2
+ import { parameterRef } from "../ref.js";
2
3
  import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
3
4
  import { reachedWithTheTests } from "../sweep.js";
4
5
  /** A list of rules as a sentence names them. */
@@ -8,24 +9,43 @@ function spelled(rules) {
8
9
  }
9
10
  return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
10
11
  }
11
- /** The clause a message ends with: the other rules that report the kind, or that none is known. */
12
- function overlapClause(overlap) {
13
- return overlap.length === 1 && overlap[0] === "none known"
12
+ /** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
13
+ const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
14
+ /**
15
+ * The clause a message ends with: the other rules that report the part, or that none is
16
+ * known. A parameter of a file the compiler does not check is no report of the compiler's.
17
+ */
18
+ function overlapClause(overlap, part) {
19
+ const rules = part.parameter?.checked === false
20
+ ? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
21
+ : overlap;
22
+ return rules.length === 1 && rules[0] === "none known"
14
23
  ? "no other rule is known to report it"
15
- : `also reported by ${spelled(overlap)}`;
24
+ : `also reported by ${spelled(rules)}`;
16
25
  }
17
26
  /**
18
- * Whether one parameter's name is reported: every unread one of a function whose
19
- * signature is free, and of a function used as a value only one no read parameter follows,
20
- * because a caller may pass such a function more arguments than it declares.
27
+ * Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
28
+ * caller may pass a function used as a value more arguments than it declares, so there one
29
+ * a read parameter follows is renamed to an underscore name, which the compilers read as
30
+ * declared unread, and one no read parameter follows is deleted: kept where the call's
31
+ * inference may read its annotation, at `probable` where nothing reads the annotation.
21
32
  */
22
- function parameterReported(part, free, freeButValued) {
33
+ function parameterVerdict(part, free, freeButValued) {
23
34
  const place = part.parameter;
24
35
  if (place?.inValue !== true && free.has(part.declaration)) {
25
- return true;
36
+ return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
26
37
  }
27
38
  const valued = place?.inValue === true || freeButValued.has(part.declaration);
28
- return valued && place?.beforeARead !== true;
39
+ if (!valued) {
40
+ return undefined;
41
+ }
42
+ if (place?.beforeARead === true) {
43
+ return { fix: "rename" };
44
+ }
45
+ if (place?.annotated === "steers") {
46
+ return undefined;
47
+ }
48
+ return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
29
49
  }
30
50
  /**
31
51
  * Whether one part is reported. A parameter needs its function's signature free to
@@ -41,7 +61,7 @@ function reported(input, part, callersUnknown) {
41
61
  const { free, freeButValued, discardedEverywhere } = input.intraFunction;
42
62
  switch (part.code) {
43
63
  case UNUSED_PARAMETER:
44
- return parameterReported(part, free, freeButValued);
64
+ return parameterVerdict(part, free, freeButValued) !== undefined;
45
65
  case UNUSED_RESULT:
46
66
  return (free.has(part.declaration) &&
47
67
  discardedEverywhere.has(part.declaration) &&
@@ -96,20 +116,31 @@ export const intraFunction = (input) => {
96
116
  if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
97
117
  continue;
98
118
  }
119
+ const verdict = part.code === UNUSED_PARAMETER
120
+ ? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
121
+ : undefined;
122
+ const message = verdict?.fix === "rename"
123
+ ? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
124
+ : part.message;
99
125
  found.push({
100
126
  code: part.code,
101
127
  position: part.position,
102
128
  symbol: {
103
- ref: declaration.ref,
129
+ ...(part.code === UNUSED_PARAMETER
130
+ ? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
131
+ : { ref: declaration.ref }),
104
132
  kind: part.kind,
105
133
  name: part.name,
106
134
  sizeLines: part.position.endLine - part.position.line + 1,
107
135
  },
108
- message: `${part.message}; ${overlapClause(overlap)}`,
136
+ message: `${message}; ${overlapClause(overlap, part)}`,
109
137
  details: {
110
138
  ...(part.kind === "store" ? { writePositions: [part.position] } : {}),
111
139
  overlap,
112
140
  },
141
+ ...(verdict?.fix === "delete" && verdict.confidence !== "certain"
142
+ ? { reachabilityClass: verdict.confidence }
143
+ : {}),
113
144
  });
114
145
  }
115
146
  return found;
@@ -43,41 +43,25 @@ function fileNeverBuilt({ config, swept, files }) {
43
43
  .map((file) => fileFinding(FILE_NEVER_BUILT, file, message));
44
44
  }
45
45
  /**
46
- * The paths of every file a reference written in another file reaches, naming the
47
- * file or a declaration in it, a loaded consumer's file included. An import is such a
48
- * reference, so an import from any file reaches what it names, whatever reaches the
49
- * importing file.
46
+ * The paths of the files a reference from a live declaration of another file reaches, a
47
+ * loaded consumer's file included, a root names, or a declared edge names a declaration in,
48
+ * and of every file a reference written anywhere reaches. A file holding a declaration the
49
+ * sweep judged live is kept whatever held it live, and so is a file a program holds by
50
+ * inclusion, which acts on every file of the program without an import naming it. A
51
+ * reference from a file's top level counts once the file is kept.
50
52
  */
51
- function reachedFromOutside(union) {
52
- const reached = new Set();
53
+ function keptFiles({ swept, files, boundary }) {
54
+ const union = swept.matrix.union;
55
+ const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
56
+ const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
57
+ const kept = new Set(files.heldByInclusion);
58
+ const referenced = new Set();
53
59
  for (const edge of union.consumed) {
54
60
  const to = union.symbols[edge.to];
55
61
  if (to !== undefined) {
56
- reached.add(to.position.path);
62
+ kept.add(to.position.path);
57
63
  }
58
64
  }
59
- union.out.forEach((edges, from) => {
60
- const fromPath = union.symbols[from]?.position.path;
61
- for (const edge of edges) {
62
- const to = union.symbols[edge.to];
63
- if (to !== undefined && to.position.path !== fromPath) {
64
- reached.add(to.position.path);
65
- }
66
- }
67
- });
68
- return reached;
69
- }
70
- /**
71
- * The paths of the files an import reaches, a root names, or a declared edge names a
72
- * declaration in. A file holding a declaration the sweep judged live is kept whatever
73
- * held it live, and so is a file a program holds by inclusion, which acts on every
74
- * file of the program without an import naming it.
75
- */
76
- function keptFiles({ swept, files, boundary }) {
77
- const union = swept.matrix.union;
78
- const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
79
- const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
80
- const kept = new Set([...reachedFromOutside(union), ...files.heldByInclusion]);
81
65
  for (const root of union.rooted) {
82
66
  const named = union.symbols[root.at];
83
67
  if (named !== undefined) {
@@ -89,14 +73,63 @@ function keptFiles({ swept, files, boundary }) {
89
73
  kept.add(symbol.position.path);
90
74
  }
91
75
  }
92
- return kept;
76
+ const fromTopLevel = new Map();
77
+ const pending = [];
78
+ const keep = (path) => {
79
+ if (!kept.has(path)) {
80
+ kept.add(path);
81
+ pending.push(path);
82
+ }
83
+ };
84
+ union.out.forEach((edges, from) => {
85
+ const source = union.symbols[from];
86
+ if (source === undefined) {
87
+ return;
88
+ }
89
+ for (const edge of edges) {
90
+ const to = union.symbols[edge.to]?.position.path;
91
+ if (to === undefined || to === source.position.path) {
92
+ continue;
93
+ }
94
+ referenced.add(to);
95
+ if (source.kind === FILE_SUBJECT) {
96
+ fromTopLevel.set(source.position.path, [
97
+ ...(fromTopLevel.get(source.position.path) ?? []),
98
+ to,
99
+ ]);
100
+ }
101
+ else if (!dead.has(source.id)) {
102
+ pending.push(to);
103
+ }
104
+ }
105
+ });
106
+ pending.push(...kept);
107
+ for (let path = pending.pop(); path !== undefined; path = pending.pop()) {
108
+ kept.add(path);
109
+ for (const to of fromTopLevel.get(path) ?? []) {
110
+ keep(to);
111
+ }
112
+ fromTopLevel.delete(path);
113
+ }
114
+ return { kept, referenced };
93
115
  }
94
- /** Every file of the run that no import reaches and no root or edge names. */
116
+ /**
117
+ * Every file of the run that no reference from live code reaches and no root or edge
118
+ * names. A file only dead code imports is found dead under reachability alone.
119
+ */
95
120
  function fileNeverImported(input) {
96
- const kept = keptFiles(input);
121
+ const { kept, referenced } = keptFiles(input);
97
122
  return input.swept.matrix.union.symbols
98
123
  .filter((symbol) => symbol.kind === FILE_SUBJECT && !kept.has(symbol.position.path))
99
- .map((symbol) => fileFinding(FILE_NEVER_IMPORTED, { path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine }, "no import reaches this file and no root names it or a declaration in it"));
124
+ .map((symbol) => {
125
+ const file = { path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine };
126
+ return referenced.has(symbol.position.path)
127
+ ? {
128
+ ...fileFinding(FILE_NEVER_IMPORTED, file, "only code that is itself dead imports this file, and no root names it or a declaration in it"),
129
+ unreachableOnly: true,
130
+ }
131
+ : fileFinding(FILE_NEVER_IMPORTED, file, "no import reaches this file and no root names it or a declaration in it");
132
+ });
100
133
  }
101
134
  /** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
102
135
  export const nonCodeArtifacts = (input) => [
@@ -41,8 +41,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
41
41
  * The code one candidate is reported under, or none, each symbol once under the most
42
42
  * specific code: a test of dead code first; no other test-file declaration or enum
43
43
  * member nothing names; a test-support declaration test code references as test-only
44
- * use; then deprecation, a test reference, and the unreferenced codes, an unimportable
45
- * unused export being the unreachable kind's.
44
+ * use; then deprecation, a test reference, or only dead production references beside a
45
+ * test that stays, and the unreferenced codes, an unimportable unused export being the
46
+ * unreachable kind's.
46
47
  */
47
48
  function codeOf(judged) {
48
49
  const { candidate, symbol } = judged;
@@ -65,7 +66,9 @@ function codeOf(judged) {
65
66
  if (candidate.productionRefs === 0 && judged.deprecated) {
66
67
  return "DS1006";
67
68
  }
68
- if (candidate.productionRefs === 0 && candidate.testRefs > 0) {
69
+ if (candidate.testRefs > 0 &&
70
+ (candidate.productionRefs === 0 ||
71
+ (candidate.relation === "reachability" && candidate.keptByTests))) {
69
72
  return "DS1004";
70
73
  }
71
74
  if (MEMBERS.has(symbol.kind)) {
@@ -109,7 +112,9 @@ function messageOf(code, judged) {
109
112
  case "DS1006":
110
113
  return `deprecated ${word} has no production reference`;
111
114
  case "DS1004":
112
- return `${word} is referenced only from test files and never from production code`;
115
+ return candidate.productionRefs === 0
116
+ ? `${word} is referenced only from test files and never from production code`
117
+ : `${word} is referenced only from test files and from declarations that are themselves dead`;
113
118
  }
114
119
  const subject = code === "DS1003"
115
120
  ? `${visibilityWord(symbol)}${word}`
@@ -162,7 +167,27 @@ function brandMembers(input) {
162
167
  /** The kinds of member a brand is: a data member of a type or of a class. */
163
168
  const BRANDS = new Set(["type-member", "class-member"]);
164
169
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
165
- export const unusedDeclarations = (input) => familyFindings(input, false);
170
+ export const unusedDeclarations = (input) => [
171
+ ...familyFindings(input, false),
172
+ ...unusedImports(input),
173
+ ];
174
+ /**
175
+ * The import bindings their file never reads, under the unexported code. A generated
176
+ * file's are the generator's to remove unless the run includes generated files.
177
+ */
178
+ function unusedImports(input) {
179
+ const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
180
+ return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
181
+ ? []
182
+ : [
183
+ {
184
+ code: "DS1002",
185
+ position: binding.position,
186
+ symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
187
+ message: `import binding ${binding.name} is never read in its file`,
188
+ },
189
+ ]);
190
+ }
166
191
  /**
167
192
  * The finding each member of a dead container would carry were its container live: what
168
193
  * a record on such a member is matched against, since the report carries the container's
@@ -1,5 +1,6 @@
1
1
  import { OUTSIDE } from "../graph.js";
2
2
  import { declarationKey } from "../ref.js";
3
+ import { runnerCalls } from "../sweep.js";
3
4
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
4
5
  const UNNECESSARY_EXPORT = "DS1101";
5
6
  const UNREACHABLE_EXPORT = "DS1103";
@@ -24,8 +25,27 @@ const WORDS = {
24
25
  function publishes(kinds) {
25
26
  return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
26
27
  }
28
+ /**
29
+ * The root kinds that name a file without entering it by its exports: a configuration
30
+ * string or a script token runs the file, and no tool reads a name it exports.
31
+ */
32
+ const RUN_ONLY = new Set(["configuration-string", "script"]);
33
+ /** Whether a root makes its file an entry file, or its declaration an entry, which a tool may read by name. */
27
34
  function entersByName(kinds) {
28
- return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
35
+ return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
36
+ }
37
+ /**
38
+ * The root kinds whose root of a file enters no export by name: a document's import roots
39
+ * each export it imports, so an export it does not import is judged as in any file.
40
+ */
41
+ const PER_NAME = new Set(["document-import"]);
42
+ /**
43
+ * Whether the roots of a declaration's file enter it by name: a test runner's root
44
+ * enters only an export the runner calls, so another export of a test file is judged.
45
+ */
46
+ function fileEntersByName(kinds, symbol) {
47
+ const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
48
+ return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
29
49
  }
30
50
  /**
31
51
  * Folds the run's references into the reach of each declaration.
@@ -123,7 +143,7 @@ function narrowing(facts, at, symbol, world) {
123
143
  world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
124
144
  symbol.kind === "export-alias" ||
125
145
  entersByName(facts.rootedBy[at]) ||
126
- entersByName(facts.rootedBy[file]) ||
146
+ fileEntersByName(facts.rootedBy[file], symbol) ||
127
147
  (facts.named[at] ?? 0) === 0) {
128
148
  return undefined;
129
149
  }
@@ -148,7 +168,7 @@ function narrowing(facts, at, symbol, world) {
148
168
  * export no code outside the target can import, because the manifest declares
149
169
  * `exports` and no root reaches it. Each arm yields to a more specific kind, so one
150
170
  * declaration is reported once: a test whose every subject is dead, an interface, a
151
- * declaration only a test file references, and a deprecated one.
171
+ * declaration only a test file references or a test that stays keeps, and a deprecated one.
152
172
  */
153
173
  function unreachable(facts, at, symbol, candidate, input) {
154
174
  if (!input.boundary.encapsulated ||
@@ -156,7 +176,7 @@ function unreachable(facts, at, symbol, candidate, input) {
156
176
  (facts.rootedBy[at]?.size ?? 0) > 0 ||
157
177
  candidate.testOfDeadCode ||
158
178
  symbol.kind === "interface" ||
159
- (candidate.productionRefs === 0 && candidate.testRefs > 0) ||
179
+ (candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
160
180
  (candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
161
181
  return undefined;
162
182
  }
@@ -185,7 +205,11 @@ export function narrowings(input) {
185
205
  return;
186
206
  }
187
207
  const candidate = candidates.get(symbol.id);
188
- const found = candidate === undefined
208
+ // A declaration of test code, a test file's or test-support code's, that only test code
209
+ // references is a candidate, live in the run that counts test references.
210
+ const live = candidate === undefined ||
211
+ (facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
212
+ const found = candidate === undefined || live
189
213
  ? narrowing(facts, at, symbol, world)
190
214
  : unreachable(facts, at, symbol, candidate, input);
191
215
  if (found !== undefined) {
@@ -8,6 +8,12 @@
8
8
  const HEADER_LINE = /^\/\/ Code generated .* DO NOT EDIT\.$/u;
9
9
  /** The documentation tag `@generated` as a word of a comment. */
10
10
  const GENERATED_TAG = /(?<![\w@$])@generated(?![\w$-])/u;
11
+ /** A first comment naming the tool that wrote the file. */
12
+ const GENERATED_BY = /^\/\/ Generated by \S/u;
13
+ /** The word a generator's notice holds, beside one of {@link KEEP_OUT}. */
14
+ const GENERATED_WORD = /\bgenerated\b/iu;
15
+ /** The phrases a generator's notice asks a reader not to edit the file by. */
16
+ const KEEP_OUT = /\b(?:do not edit|will be overwritten|should not make any changes)\b/iu;
11
17
  /** The comments before a text's first token, a leading hashbang line skipped. */
12
18
  function leadingComments(text) {
13
19
  const comments = [];
@@ -48,6 +54,11 @@ export function generatedByComment(text) {
48
54
  if (comments.some((one) => GENERATED_TAG.test(one))) {
49
55
  return "generated by the @generated tag its leading comments hold";
50
56
  }
57
+ const notice = comments.join("\n");
58
+ if (GENERATED_BY.test(comments[0] ?? "") ||
59
+ (GENERATED_WORD.test(notice) && KEEP_OUT.test(notice))) {
60
+ return "generated by the notice its leading comments hold";
61
+ }
51
62
  return undefined;
52
63
  }
53
64
  /**
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Which files the `.gitignore` files of a tree ignore, read as git reads them:
3
+ * https://git-scm.com/docs/gitignore#_pattern_format
4
+ */
5
+ import { globExpression } from "./glob.js";
6
+ import { dirnamePath, joinPath, relativePath } from "./paths.js";
7
+ /** The rules one `.gitignore` text writes, in order. */
8
+ function rulesOf(text) {
9
+ const rules = [];
10
+ for (const raw of text.split(/\r?\n/u)) {
11
+ let line = raw.replace(/(?<!\\)\s+$/u, "");
12
+ if (line === "" || line.startsWith("#")) {
13
+ continue;
14
+ }
15
+ const negated = line.startsWith("!");
16
+ line = negated || line.startsWith("\\") ? line.slice(1) : line;
17
+ const directoryOnly = line.endsWith("/");
18
+ line = line.replace(/\/+$/u, "");
19
+ if (line === "") {
20
+ continue;
21
+ }
22
+ // A separator at the start or in the middle anchors the pattern to the file's
23
+ // directory; with none, the pattern matches at any depth below it.
24
+ const anchored = line.includes("/");
25
+ const pattern = line.replace(/^\//u, "");
26
+ rules.push({
27
+ negated,
28
+ directoryOnly,
29
+ expression: globExpression(anchored ? pattern : `**/${pattern}`),
30
+ });
31
+ }
32
+ return rules;
33
+ }
34
+ /**
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.
38
+ */
39
+ export function gitIgnored(host, root) {
40
+ const read = new Map();
41
+ const rulesAt = (dir) => {
42
+ let rules = read.get(dir);
43
+ if (rules === undefined) {
44
+ try {
45
+ rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
46
+ }
47
+ catch {
48
+ rules = [];
49
+ }
50
+ read.set(dir, rules);
51
+ }
52
+ return rules;
53
+ };
54
+ /** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
55
+ 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);
61
+ 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 ?? "")) {
66
+ ignored = !rule.negated;
67
+ }
68
+ }
69
+ }
70
+ return ignored;
71
+ };
72
+ return (path) => {
73
+ const below = relativePath(root, path);
74
+ if (below === undefined) {
75
+ return false;
76
+ }
77
+ const parts = below.split("/");
78
+ for (let count = 1; count <= parts.length; count += 1) {
79
+ const at = joinPath(root, parts.slice(0, count).join("/"));
80
+ if (decided(at, count < parts.length)) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ };
86
+ }
package/dist/src/graph.js CHANGED
@@ -56,6 +56,8 @@ const NAMES_A_CALLER = {
56
56
  worker: true,
57
57
  "html-entry": true,
58
58
  "workflow-step": true,
59
+ "computed-import": true,
60
+ "document-import": true,
59
61
  };
60
62
  /** Whether the sweep judges a declaration of one kind. */
61
63
  export function swept(kind) {
@@ -72,7 +74,7 @@ export function namesACaller(kind) {
72
74
  * from such a declaration still counts toward its target, because reference counting
73
75
  * asks whether any declaration names the target. A root naming nothing is dropped.
74
76
  */
75
- export function graphOf(symbols, references, roots, testFiles, supportFiles = []) {
77
+ export function graphOf(symbols, references, roots, testFiles, supportFiles = [], ambient = new Set()) {
76
78
  const supports = new Set(supportFiles);
77
79
  const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
78
80
  const at = (id) => index.get(id) ?? OUTSIDE;
@@ -123,6 +125,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
123
125
  parent,
124
126
  test: symbols.map((symbol) => tests.has(symbol.position.path)),
125
127
  support: symbols.map((symbol) => supports.has(symbol.position.path)),
128
+ ambient: symbols.map((symbol) => ambient.has(symbol.id)),
126
129
  subject: symbols.map((symbol) => swept(symbol.kind)),
127
130
  exportsOf,
128
131
  rooted,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The import bindings of one file: the names its import declarations bind, which only
3
+ * the file itself can read.
4
+ */
5
+ import { isExportSpecifier, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isJsxElement, isJsxFragment, isJsxSelfClosingElement, isNamespaceImport, SyntaxKind, } from "@typescript/native/ast";
6
+ /** The extensions of the files whose import bindings are judged: no markup can read them. */
7
+ const JUDGED = /\.(?:[mc]?ts|tsx|[mc]?js|jsx)$/u;
8
+ /** The compiler's `jsx` settings whose emit calls no factory the file must hold. */
9
+ const AUTOMATIC_RUNTIME = new Set([4, 5, "react-jsx", "react-jsxdev"]);
10
+ /** A doc comment, where a JavaScript file names the types it imports. */
11
+ const DOC_COMMENT = /\/\*\*[\s\S]*?\*\//gu;
12
+ /** The parts of a doc comment the compiler resolves names in: a `{…}` type or link, a `@see` target. */
13
+ const DOC_REFERENCE = /\{[^{}]*\}|@see\s+[A-Za-z_$][\w$.]*/gu;
14
+ /** The factory a pragma comment names: `@jsx h` or `@jsxFrag Fragment`. */
15
+ const PRAGMA = /@jsx(?:Frag)?\s+([A-Za-z_$][\w$]*)/gu;
16
+ /**
17
+ * The names one file's import declarations bind: the default, named and namespace names of
18
+ * an `import` declaration, and the name of an `import x = require()` declaration that is
19
+ * not exported. None is judged in a declaration file or a component file.
20
+ */
21
+ export function importBindings(file) {
22
+ if (file.isDeclarationFile || !JUDGED.test(file.fileName)) {
23
+ return [];
24
+ }
25
+ const found = [];
26
+ for (const statement of file.statements) {
27
+ if (isImportDeclaration(statement)) {
28
+ const clause = statement.importClause;
29
+ if (clause?.name !== undefined) {
30
+ found.push(clause.name);
31
+ }
32
+ const named = clause?.namedBindings;
33
+ if (named !== undefined && isNamespaceImport(named)) {
34
+ found.push(named.name);
35
+ }
36
+ else if (named !== undefined) {
37
+ for (const element of named.elements) {
38
+ found.push(element.name);
39
+ }
40
+ }
41
+ }
42
+ else if (isImportEqualsDeclaration(statement) &&
43
+ isExternalModuleReference(statement.moduleReference) &&
44
+ isIdentifier(statement.name) &&
45
+ !(statement.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.ExportKeyword)) {
46
+ found.push(statement.name);
47
+ }
48
+ }
49
+ return found;
50
+ }
51
+ /** Whether one identifier names a binding in a local export, which reads it whatever it resolves to. */
52
+ export function exportsLocally(use) {
53
+ return isExportSpecifier(use.parent);
54
+ }
55
+ /**
56
+ * The names a file reads without an identifier the program resolves: each name a type, a
57
+ * link or a `@see` tag of its doc comments spells, and, where it writes JSX the compiler
58
+ * turns into factory calls, the factories the configuration or a pragma names.
59
+ */
60
+ export function implicitReads(file, options) {
61
+ const names = new Set();
62
+ for (const comment of file.text.match(DOC_COMMENT) ?? []) {
63
+ for (const reference of comment.match(DOC_REFERENCE) ?? []) {
64
+ for (const word of reference.match(/[A-Za-z_$][\w$]*/gu) ?? []) {
65
+ names.add(word);
66
+ }
67
+ }
68
+ }
69
+ if (AUTOMATIC_RUNTIME.has(options.jsx) || !writesJsx(file)) {
70
+ return names;
71
+ }
72
+ for (const factory of [options.jsxFactory, options.jsxFragmentFactory]) {
73
+ names.add(typeof factory === "string" ? (factory.split(".")[0] ?? factory) : "React");
74
+ }
75
+ for (const match of file.text.matchAll(PRAGMA)) {
76
+ if (match[1] !== undefined) {
77
+ names.add(match[1]);
78
+ }
79
+ }
80
+ return names;
81
+ }
82
+ /** Whether a file holds a JSX element or fragment. */
83
+ function writesJsx(file) {
84
+ let found = false;
85
+ const visit = (node) => {
86
+ if (found) {
87
+ return;
88
+ }
89
+ if (isJsxElement(node) || isJsxSelfClosingElement(node) || isJsxFragment(node)) {
90
+ found = true;
91
+ return;
92
+ }
93
+ node.forEachChild(visit);
94
+ };
95
+ file.forEachChild(visit);
96
+ return found;
97
+ }