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

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 +106 -10
  2. package/dist/src/augmentations.js +21 -8
  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 +75 -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 +116 -21
  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 +21 -1
  22. package/dist/src/findings/visibility-narrowing.js +22 -3
  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 +18 -9
  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 +4 -1
  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 +7 -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 +121 -17
  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 +143 -10
  63. package/src/augmentations.ts +21 -10
  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 +103 -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 +10 -0
  78. package/src/findings/completion.ts +147 -24
  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 +25 -1
  84. package/src/findings/visibility-narrowing.ts +32 -3
  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 +32 -14
  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 +10 -1
  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 +9 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +149 -16
  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
@@ -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) => [
@@ -162,7 +162,27 @@ function brandMembers(input) {
162
162
  /** The kinds of member a brand is: a data member of a type or of a class. */
163
163
  const BRANDS = new Set(["type-member", "class-member"]);
164
164
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
165
- export const unusedDeclarations = (input) => familyFindings(input, false);
165
+ export const unusedDeclarations = (input) => [
166
+ ...familyFindings(input, false),
167
+ ...unusedImports(input),
168
+ ];
169
+ /**
170
+ * The import bindings their file never reads, under the unexported code. A generated
171
+ * file's are the generator's to remove unless the run includes generated files.
172
+ */
173
+ function unusedImports(input) {
174
+ const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
175
+ return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
176
+ ? []
177
+ : [
178
+ {
179
+ code: "DS1002",
180
+ position: binding.position,
181
+ symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
182
+ message: `import binding ${binding.name} is never read in its file`,
183
+ },
184
+ ]);
185
+ }
166
186
  /**
167
187
  * The finding each member of a dead container would carry were its container live: what
168
188
  * 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,22 @@ 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
+ * Whether the roots of a declaration's file enter it by name: a test runner's root
39
+ * enters only an export the runner calls, so another export of a test file is judged.
40
+ */
41
+ function fileEntersByName(kinds, symbol) {
42
+ const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
43
+ return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
29
44
  }
30
45
  /**
31
46
  * Folds the run's references into the reach of each declaration.
@@ -123,7 +138,7 @@ function narrowing(facts, at, symbol, world) {
123
138
  world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
124
139
  symbol.kind === "export-alias" ||
125
140
  entersByName(facts.rootedBy[at]) ||
126
- entersByName(facts.rootedBy[file]) ||
141
+ fileEntersByName(facts.rootedBy[file], symbol) ||
127
142
  (facts.named[at] ?? 0) === 0) {
128
143
  return undefined;
129
144
  }
@@ -185,7 +200,11 @@ export function narrowings(input) {
185
200
  return;
186
201
  }
187
202
  const candidate = candidates.get(symbol.id);
188
- const found = candidate === undefined
203
+ // A declaration of test code, a test file's or test-support code's, that only test code
204
+ // references is a candidate, live in the run that counts test references.
205
+ const live = candidate === undefined ||
206
+ (facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
207
+ const found = candidate === undefined || live
189
208
  ? narrowing(facts, at, symbol, world)
190
209
  : unreachable(facts, at, symbol, candidate, input);
191
210
  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
+ }