@cplieger/deadset-ts 8.0.0-dev.7 → 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 (78) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +13 -14
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configured-declarations.js +15 -20
  5. package/dist/src/consumers.js +2 -2
  6. package/dist/src/contract/config.generated.js +544 -0
  7. package/dist/src/contract/report.generated.js +3 -2
  8. package/dist/src/discover.js +1 -1
  9. package/dist/src/edges.js +36 -112
  10. package/dist/src/exempt-classes.js +0 -2
  11. package/dist/src/exempt.js +1 -1
  12. package/dist/src/findings/completion.js +5 -4
  13. package/dist/src/findings/emitters.js +1 -1
  14. package/dist/src/findings/intra-function.js +1 -1
  15. package/dist/src/findings/non-code-artifacts.js +2 -2
  16. package/dist/src/findings/unused-declarations.js +1 -1
  17. package/dist/src/findings/visibility-narrowing.js +2 -2
  18. package/dist/src/findings-pass.js +1 -1
  19. package/dist/src/framework-lifecycle.js +7 -7
  20. package/dist/src/fs/document.js +59 -0
  21. package/dist/src/injection-container.js +1 -1
  22. package/dist/src/json-document.js +194 -67
  23. package/dist/src/kinds.js +0 -7
  24. package/dist/src/print.js +10 -79
  25. package/dist/src/report-schema.js +3 -224
  26. package/dist/src/resolve.js +245 -715
  27. package/dist/src/run.js +48 -55
  28. package/dist/src/schema.js +320 -344
  29. package/dist/src/scope.js +33 -101
  30. package/dist/src/serialization-contract.js +1 -1
  31. package/dist/src/suppress-file.js +15 -17
  32. package/dist/src/suppress.js +3 -3
  33. package/dist/src/template-field.js +4 -4
  34. package/dist/src/usage.js +10 -0
  35. package/dist/src/verbs/analyze.js +12 -15
  36. package/dist/src/verbs/explain.js +3 -3
  37. package/dist/src/verbs/print-projects.js +2 -2
  38. package/jsr.json +1 -1
  39. package/package.json +1 -1
  40. package/src/analysis.ts +30 -26
  41. package/src/config.ts +31 -298
  42. package/src/configured-declarations.ts +18 -23
  43. package/src/consumers.ts +4 -4
  44. package/src/contract/config.generated.ts +798 -0
  45. package/src/contract/report.generated.ts +3 -2
  46. package/src/discover.ts +1 -1
  47. package/src/edges.ts +39 -124
  48. package/src/exempt-classes.ts +0 -5
  49. package/src/exempt.ts +4 -4
  50. package/src/findings/completion.ts +5 -6
  51. package/src/findings/emitter.ts +2 -2
  52. package/src/findings/emitters.ts +1 -1
  53. package/src/findings/intra-function.ts +1 -1
  54. package/src/findings/non-code-artifacts.ts +4 -4
  55. package/src/findings/unused-declarations.ts +1 -1
  56. package/src/findings/visibility-narrowing.ts +2 -2
  57. package/src/findings-pass.ts +3 -3
  58. package/src/framework-lifecycle.ts +7 -7
  59. package/src/fs/document.ts +74 -0
  60. package/src/host.ts +12 -5
  61. package/src/injection-container.ts +1 -1
  62. package/src/json-document.ts +236 -76
  63. package/src/kinds.ts +0 -8
  64. package/src/print.ts +11 -92
  65. package/src/report-schema.ts +3 -299
  66. package/src/resolve.ts +281 -1105
  67. package/src/run.ts +50 -63
  68. package/src/schema.ts +391 -389
  69. package/src/scope.ts +39 -119
  70. package/src/serialization-contract.ts +1 -1
  71. package/src/suppress-file.ts +15 -20
  72. package/src/suppress.ts +3 -3
  73. package/src/template-field.ts +10 -7
  74. package/src/usage.ts +10 -0
  75. package/src/verbs/analyze.ts +12 -23
  76. package/src/verbs/explain.ts +3 -7
  77. package/src/verbs/print-projects.ts +2 -2
  78. package/src/verbs/verb.ts +2 -2
package/dist/src/edges.js CHANGED
@@ -4,144 +4,68 @@
4
4
  * never resolves the paired symbol: the other language's analyzer decides whether the
5
5
  * pairing is a use, and the merge reads the two evaluations together.
6
6
  */
7
- import { ConfigError } from "./config.js";
8
- import { isRecord, positionAt, walkDocument } from "./json-document.js";
9
- import { joinPath } from "./paths.js";
7
+ import { EDGES_SCHEMA } from "./contract/config.generated.js";
8
+ import { readTargetDocument } from "./fs/document.js";
9
+ import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.js";
10
10
  import { positionKey } from "./position.js";
11
+ import { firstRefusal } from "./schema.js";
11
12
  import { isPartSubject } from "./suppress.js";
12
13
  import { bytewise } from "./text/compare.js";
13
- import { message } from "./product.js";
14
+ import { UsageError } from "./usage.js";
14
15
  /** The name and the location the Contract fixes for the edges document. */
15
16
  export const EDGES_FILE = "deadset-edges.json";
16
17
  /** The language prefix of every reference this analyzer evaluates. */
17
18
  const OWN_PREFIX = "ts://";
18
- /** The closed key list of the document and of one edge. */
19
- const DOCUMENT_KEYS = new Set(["description", "edges"]);
20
- const EDGE_KEYS = new Set(["id", "because", "provides", "used_by"]);
21
- /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
22
- const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
23
- /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
24
- const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
25
- const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
26
- const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
27
- const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
28
- const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
29
19
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
30
- class EdgesError extends ConfigError {
31
- constructor(at, text, want) {
32
- super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
20
+ class EdgesError extends UsageError {
21
+ constructor(at, reason) {
22
+ super(`${at}: ${reason}`);
33
23
  this.name = "EdgesError";
34
24
  }
35
25
  }
36
- /**
37
- * Whether a reference carries a wildcard outside a quoted or a computed component, which
38
- * would name a set of symbols rather than one.
39
- */
40
- function carriesWildcard(ref) {
41
- let quoted = false;
42
- let computed = false;
43
- let escaped = false;
44
- for (const char of ref) {
45
- if (escaped) {
46
- escaped = false;
47
- }
48
- else if (quoted && char === "\\") {
49
- escaped = true;
50
- }
51
- else if (quoted) {
52
- quoted = char !== "'";
53
- }
54
- else if (computed) {
55
- computed = char !== "]";
56
- }
57
- else if (char === "'") {
58
- quoted = true;
59
- }
60
- else if (char === "[") {
61
- computed = true;
62
- }
63
- else if (char === "*") {
64
- return true;
65
- }
66
- }
67
- return false;
68
- }
69
- /** One edge, checked: the closed key list, the value types and the value forms. */
70
- function readEdge(value, at) {
71
- const refuse = (text, want) => {
72
- throw new EdgesError(at, text, want);
73
- };
74
- if (!isRecord(value)) {
75
- return refuse(JSON.stringify(value), EDGE_WANT);
76
- }
77
- for (const [key, held] of Object.entries(value)) {
78
- if (!EDGE_KEYS.has(key) || typeof held !== "string") {
79
- refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
80
- }
81
- }
82
- const edge = value;
83
- const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
84
- const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
85
- const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
86
- if (!ID_FORM.test(id)) {
87
- refuse(JSON.stringify(id), ID_WANT);
88
- }
89
- for (const ref of [provides, usedBy]) {
90
- if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
91
- refuse(JSON.stringify(ref), REFERENCE_WANT);
92
- }
93
- }
94
- return { id, provides, usedBy };
95
- }
26
+ /** The sides of one edge, in the order a reader yields them. */
27
+ const SIDE_NAMES = ["provides", "used_by"];
96
28
  /**
97
29
  * Every side of a declared edge that names a symbol of this language, in document order,
98
30
  * `provides` before `used_by` within an edge. An absent document declares no edge. The
99
- * document is strict JSON with a closed key list at both levels, and a defect in it is an
100
- * {@link EdgesError}.
31
+ * document is strict JSON held to the Contract's edges schema, and a defect in it is an
32
+ * {@link EdgesError} at the line and column of the edge it is in, where it is in one.
101
33
  */
102
34
  export function readEdgeSides(host, targetRoot) {
103
- const file = joinPath(targetRoot, EDGES_FILE);
104
- if (host.kindOf(file) === "absent") {
35
+ const text = readTargetDocument(host, targetRoot, EDGES_FILE);
36
+ if (text === undefined) {
105
37
  return [];
106
38
  }
107
- const text = host.readFile(file);
108
- let parsed;
39
+ let read;
109
40
  try {
110
- parsed = JSON.parse(text);
41
+ read = readStrict(text, "edges");
111
42
  }
112
43
  catch (error) {
113
- throw new EdgesError(EDGES_FILE, message(error), DOCUMENT_WANT);
114
- }
115
- if (!isRecord(parsed)) {
116
- throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
117
- }
118
- const walk = walkDocument(text, "edges");
119
- if (walk.twice !== undefined) {
120
- throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)), `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`, DOCUMENT_WANT);
44
+ if (!(error instanceof StrictJSONError)) {
45
+ throw error;
46
+ }
47
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
121
48
  }
122
- const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
123
- const description = parsed["description"];
124
- const edges = parsed["edges"];
125
- if (unknown !== undefined ||
126
- (description !== undefined && typeof description !== "string") ||
127
- !Array.isArray(edges)) {
128
- throw new EdgesError(EDGES_FILE, unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`, DOCUMENT_WANT);
49
+ const value = read.value;
50
+ const refused = firstRefusal(EDGES_SCHEMA, value);
51
+ if (refused !== undefined) {
52
+ const place = placeIn(value, refused.pointer, refused.member);
53
+ const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
54
+ const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
55
+ const where = dotted(place);
56
+ throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
129
57
  }
58
+ const edges = value.edges;
130
59
  const sides = [];
131
- edges.forEach((value, index) => {
132
- const offset = walk.starts[index];
133
- const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
134
- const edge = readEdge(value, at);
135
- const named = [
136
- ["provides", edge.provides],
137
- ["used_by", edge.usedBy],
138
- ];
139
- for (const [side, symbol] of named) {
140
- if (symbol.startsWith(OWN_PREFIX)) {
141
- sides.push({ edge: edge.id, side, symbol });
60
+ for (const id of read.starts.keys()) {
61
+ const edge = String(id);
62
+ for (const side of SIDE_NAMES) {
63
+ const symbol = edges[edge]?.[side];
64
+ if (symbol?.startsWith(OWN_PREFIX) === true) {
65
+ sides.push({ edge, side, symbol });
142
66
  }
143
67
  }
144
- });
68
+ }
145
69
  return sides;
146
70
  }
147
71
  /**
@@ -11,5 +11,3 @@ const VISIBILITY = new Map(TS_ROWS.map((row) => [row.class, row.typescriptVisibi
11
11
  export function typescriptVisibilityOf(exemptionClass) {
12
12
  return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
13
13
  }
14
- /** Every class of the vocabulary, whichever language it runs on, in the vocabulary's order. */
15
- export const EXEMPTION_CLASS_NAMES = EXEMPTION_CLASSES.map((row) => row.class);
@@ -17,7 +17,7 @@ import { bytewise } from "./text/compare.js";
17
17
  * only switches nothing off here, so one configuration serves every analyzer.
18
18
  */
19
19
  export function disabledClasses(config) {
20
- return new Set(config.exemptionsDisabled);
20
+ return new Set(config.exemptions.disabled);
21
21
  }
22
22
  /**
23
23
  * Whether a class whose row states `allowed` may retain a member of one visibility. A
@@ -41,8 +41,9 @@ function everyDeclaredLoaded(boundary) {
41
41
  * naming its family, else the code's default.
42
42
  */
43
43
  function severityOf(input, code, fallback) {
44
- const { config } = input;
45
- return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
44
+ const { severity } = input.config;
45
+ const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
46
+ return key === undefined ? fallback : (severity[key] ?? fallback);
46
47
  }
47
48
  /**
48
49
  * The declarations the roots name as a library's published API, each public member of a
@@ -165,7 +166,7 @@ function testUsedSubjects(input) {
165
166
  function rootsCeiling(input, finding, candidate, configurations) {
166
167
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
167
168
  const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
168
- const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
169
+ const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
169
170
  return unreachable && !complete ? "probable" : "certain";
170
171
  }
171
172
  /**
@@ -460,7 +461,7 @@ function dialed(finding, minConfidence) {
460
461
  * root withholds itself alone.
461
462
  */
462
463
  export function dialsOf(input, findings) {
463
- const minConfidence = input.config.analysis.minConfidence;
464
+ const minConfidence = input.config.analysis.min_confidence;
464
465
  const components = new Set(findings
465
466
  .filter((finding) => finding.component.root &&
466
467
  dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
@@ -86,7 +86,7 @@ function atPossible(input) {
86
86
  const { config } = input;
87
87
  return {
88
88
  ...input,
89
- config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
89
+ config: { ...config, analysis: { ...config.analysis, min_confidence: "possible" } },
90
90
  };
91
91
  }
92
92
  /**
@@ -98,7 +98,7 @@ export const intraFunction = (input) => {
98
98
  return symbol === undefined ? [] : [symbol.id];
99
99
  }));
100
100
  const { declared, loaded } = input.boundary.consumers;
101
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
101
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
102
102
  const published = (id) => {
103
103
  const symbol = byId.get(id);
104
104
  return (symbol !== undefined &&
@@ -10,8 +10,8 @@ const FILE_START = 1;
10
10
  * derived matrix is never complete.
11
11
  */
12
12
  function completeMatrix(config) {
13
- return (config.analysis.matrixComplete &&
14
- config.analysis.configurations.some((entry) => entry.shape === "project"));
13
+ return (config.analysis.matrix.complete &&
14
+ config.analysis.configurations.some((entry) => "project" in entry));
15
15
  }
16
16
  /** One finding about a whole file. */
17
17
  function fileFinding(code, file, message) {
@@ -178,7 +178,7 @@ export const unusedDeclarations = (input) => [
178
178
  * remove unless the run includes generated files.
179
179
  */
180
180
  function unusedImports(input) {
181
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
181
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
182
182
  return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
183
183
  ? []
184
184
  : [
@@ -193,10 +193,10 @@ function unreachable(facts, at, symbol, candidate, input) {
193
193
  export function narrowings(input) {
194
194
  const facts = factsOf(input.swept.matrix.union);
195
195
  const { declared, loaded } = input.boundary.consumers;
196
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
196
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
197
197
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
198
198
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
199
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
199
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
200
200
  const findings = [];
201
201
  facts.graph.symbols.forEach((symbol, at) => {
202
202
  if (generated?.has(symbol.position.path) === true) {
@@ -66,7 +66,7 @@ export function verdictOf(result, config) {
66
66
  if (result.totals.staleSuppressions > 0) {
67
67
  return EXIT_FINDINGS;
68
68
  }
69
- const failing = result.findings.some((finding) => fails(finding.severity, config.reporters.failOn));
69
+ const failing = result.findings.some((finding) => fails(finding.severity, config.reporters.fail_on));
70
70
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
71
71
  }
72
72
  /**
@@ -16,8 +16,8 @@ import { must } from "./query.js";
16
16
  /** Every contract's components and bases, resolved in one pass over the entries. */
17
17
  function resolveContracts(project, held, contracts) {
18
18
  const entries = contracts.flatMap((contract) => [
19
- ...contract.components,
20
- ...contract.bases,
19
+ ...(contract.components ?? []),
20
+ ...(contract.bases ?? []),
21
21
  ]);
22
22
  const resolved = resolveEntries(project, held, entries);
23
23
  let next = 0;
@@ -28,8 +28,8 @@ function resolveContracts(project, held, contracts) {
28
28
  };
29
29
  return contracts.map((contract) => ({
30
30
  contract,
31
- components: take(contract.components.length),
32
- bases: take(contract.bases.length),
31
+ components: take(contract.components?.length ?? 0),
32
+ bases: take(contract.bases?.length ?? 0),
33
33
  }));
34
34
  }
35
35
  /** The classes a decorator or a call the contracts' components name makes a component. */
@@ -160,11 +160,11 @@ function byBases(project, held, chains, contracts) {
160
160
  */
161
161
  export function frameworkLifecycle(input) {
162
162
  const { project, held, targetRoot } = input;
163
- if (input.ts.lifecycleContracts.length === 0) {
163
+ if (input.ts.lifecycle_contracts.length === 0) {
164
164
  return [];
165
165
  }
166
166
  const chains = aliasChains(project, held);
167
- const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
167
+ const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
168
168
  const components = [
169
169
  ...byComponents(project, held, chains, contracts),
170
170
  ...byBases(project, held, chains, contracts),
@@ -173,7 +173,7 @@ export function frameworkLifecycle(input) {
173
173
  return [];
174
174
  }
175
175
  const ids = new Set(components.map((one) => one.id));
176
- const sides = input.ts.lifecycleContracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
176
+ const sides = input.ts.lifecycle_contracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
177
177
  ? ["instance", "static"]
178
178
  : ["instance"];
179
179
  const members = sides.map((side) => classMembers(project, held, ids, side));
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The one read of a document the target holds at its root, which a checkout under analysis
3
+ * controls: the repository configuration, the ignore file, the baseline and the edges
4
+ * document. The read stays inside the target root, takes a regular file alone, and holds no
5
+ * more of it than the Contract's document bound.
6
+ */
7
+ import { CONFIG_MAX_BYTES } from "../contract/config.generated.js";
8
+ import { joinPath, relativePath } from "../paths.js";
9
+ import { message } from "../product.js";
10
+ import { utf8Bytes } from "../reporters/sha256.js";
11
+ import { UsageError } from "../usage.js";
12
+ /**
13
+ * A document at the target root that is not one to read: its name resolves outside the
14
+ * target root or to anything but a regular file, or the read fails. Nothing is read from
15
+ * it, and the run ends with the failure code.
16
+ */
17
+ export class TargetDocumentError extends Error {
18
+ constructor(label, reason) {
19
+ super(`${label}: ${reason}`);
20
+ this.name = "TargetDocumentError";
21
+ }
22
+ }
23
+ /** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
24
+ export function refuseOverBound(label, text) {
25
+ if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
26
+ throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
27
+ }
28
+ }
29
+ /** What `read` returns, or its throw as the document's {@link TargetDocumentError}. */
30
+ function attempt(label, read) {
31
+ try {
32
+ return read();
33
+ }
34
+ catch (error) {
35
+ throw new TargetDocumentError(label, message(error));
36
+ }
37
+ }
38
+ /**
39
+ * The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
40
+ * there, each refusal naming it `label`. A document past the bound is read one byte past
41
+ * it and refused as {@link refuseOverBound} refuses it.
42
+ */
43
+ export function readTargetDocument(host, targetRoot, name, label = name) {
44
+ const path = joinPath(targetRoot, name);
45
+ const kind = attempt(label, () => host.kindOf(path));
46
+ if (kind === "absent") {
47
+ return undefined;
48
+ }
49
+ const real = attempt(label, () => host.realPath(path));
50
+ if (relativePath(attempt(label, () => host.realPath(targetRoot)), real) === undefined) {
51
+ throw new TargetDocumentError(label, `resolves to ${real}, outside the target root`);
52
+ }
53
+ if (kind !== "file") {
54
+ throw new TargetDocumentError(label, "is not a regular file");
55
+ }
56
+ const text = attempt(label, () => host.readFile(real, CONFIG_MAX_BYTES + 1));
57
+ refuseOverBound(label, text);
58
+ return text;
59
+ }
@@ -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
  }