@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3
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.
- package/dist/src/analysis.js +30 -6
- package/dist/src/augmentations.js +105 -12
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +62 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +122 -49
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +13 -5
- package/dist/src/findings/visibility-narrowing.js +8 -3
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +76 -4
- package/dist/src/references.js +34 -3
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/suppress.js +1 -0
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/test-support.js +8 -11
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +48 -8
- package/src/augmentations.ts +134 -16
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +72 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/finding.ts +5 -0
- package/src/findings/completion.ts +153 -56
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +17 -5
- package/src/findings/visibility-narrowing.ts +9 -3
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +122 -4
- package/src/references.ts +38 -2
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/suppress.ts +1 -0
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/test-support.ts +17 -13
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
package/src/references.ts
CHANGED
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
isIdentifier,
|
|
16
16
|
isImportDeclaration,
|
|
17
17
|
isImportEqualsDeclaration,
|
|
18
|
+
isInterfaceDeclaration,
|
|
18
19
|
isMetaProperty,
|
|
20
|
+
isModuleBlock,
|
|
21
|
+
isModuleDeclaration,
|
|
19
22
|
isNewExpression,
|
|
20
23
|
isNamespaceExport,
|
|
21
24
|
isNoSubstitutionTemplateLiteral,
|
|
@@ -49,7 +52,7 @@ import {
|
|
|
49
52
|
type Inventory,
|
|
50
53
|
type InventorySymbol,
|
|
51
54
|
} from "./inventory.ts";
|
|
52
|
-
import { augmentationReferences } from "./augmentations.ts";
|
|
55
|
+
import { ambientGlobalInterfaces, augmentationReferences } from "./augmentations.ts";
|
|
53
56
|
import { elementAccessReferences } from "./element-access.ts";
|
|
54
57
|
import { indexedAccessReferences } from "./indexed-access.ts";
|
|
55
58
|
import { keyofComparisonReferences } from "./keyof-comparisons.ts";
|
|
@@ -1610,6 +1613,39 @@ export function references<Brand>(
|
|
|
1610
1613
|
}
|
|
1611
1614
|
}
|
|
1612
1615
|
|
|
1616
|
+
/** The ambient global interfaces of the walked files that merge into a declaration outside the project's own files. */
|
|
1617
|
+
const libraryMerged = (scripts: ReadonlySet<string>): ReadonlySet<string> => {
|
|
1618
|
+
const names = new Map<string, Node>();
|
|
1619
|
+
const visit = (node: Node): void => {
|
|
1620
|
+
if (isInterfaceDeclaration(node)) {
|
|
1621
|
+
const id = declaredAt(node);
|
|
1622
|
+
if (id !== undefined) {
|
|
1623
|
+
names.set(id, node.name);
|
|
1624
|
+
}
|
|
1625
|
+
} else if (isModuleDeclaration(node) || isModuleBlock(node)) {
|
|
1626
|
+
node.forEachChild(visit);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
for (const { file } of walked) {
|
|
1630
|
+
file.forEachChild(visit);
|
|
1631
|
+
}
|
|
1632
|
+
const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
|
|
1633
|
+
const asked = [...names].filter(([id]) => global.has(id));
|
|
1634
|
+
if (asked.length === 0) {
|
|
1635
|
+
return new Set();
|
|
1636
|
+
}
|
|
1637
|
+
const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
|
|
1638
|
+
return new Set(
|
|
1639
|
+
asked.flatMap(([id], at) => {
|
|
1640
|
+
const symbol = answers[at];
|
|
1641
|
+
return typeof symbol === "object" &&
|
|
1642
|
+
symbol.declarations.some((handle) => !ownPaths.has(handle.path))
|
|
1643
|
+
? [id]
|
|
1644
|
+
: [];
|
|
1645
|
+
}),
|
|
1646
|
+
);
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1613
1649
|
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1614
1650
|
for (const [file, writes] of valued) {
|
|
1615
1651
|
const uses = usesOf.get(file) ?? [];
|
|
@@ -1637,7 +1673,7 @@ export function references<Brand>(
|
|
|
1637
1673
|
const scripts = new Set(
|
|
1638
1674
|
walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId),
|
|
1639
1675
|
);
|
|
1640
|
-
found.push(...augmentationReferences(held, found, scripts));
|
|
1676
|
+
found.push(...augmentationReferences(held, found, scripts, libraryMerged(scripts)));
|
|
1641
1677
|
found.sort(
|
|
1642
1678
|
(a, b) =>
|
|
1643
1679
|
byPosition(a.position, b.position) ||
|
package/src/report.ts
CHANGED
|
@@ -81,11 +81,6 @@ export interface WireFinding {
|
|
|
81
81
|
readonly root: boolean;
|
|
82
82
|
readonly symbol_count: number;
|
|
83
83
|
readonly deletable_lines: number;
|
|
84
|
-
readonly members?: readonly {
|
|
85
|
-
readonly ref: string;
|
|
86
|
-
readonly name: string;
|
|
87
|
-
readonly position: WirePosition;
|
|
88
|
-
}[];
|
|
89
84
|
};
|
|
90
85
|
readonly retained_by: readonly string[];
|
|
91
86
|
readonly configurations: readonly string[];
|
|
@@ -96,6 +91,16 @@ export interface WireFinding {
|
|
|
96
91
|
readonly details: WireDetails;
|
|
97
92
|
}
|
|
98
93
|
|
|
94
|
+
/** One dead component a finding names, with its members, as the report lists it once. */
|
|
95
|
+
export interface WireComponent {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
readonly members: readonly {
|
|
98
|
+
readonly ref: string;
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly position: WirePosition;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
interface WireEdgeEvaluation {
|
|
100
105
|
readonly edge: string;
|
|
101
106
|
readonly side: string;
|
|
@@ -218,6 +223,8 @@ export interface Report {
|
|
|
218
223
|
}[];
|
|
219
224
|
};
|
|
220
225
|
readonly findings: readonly WireFinding[];
|
|
226
|
+
/** Present where the run lists each component in full. */
|
|
227
|
+
readonly components?: readonly WireComponent[];
|
|
221
228
|
readonly edge_evaluations: readonly WireEdgeEvaluation[];
|
|
222
229
|
readonly stale_suppressions: readonly WireStaleSuppression[];
|
|
223
230
|
readonly declared_gaps: readonly WireDeclaredGap[];
|
|
@@ -231,13 +238,20 @@ export interface Report {
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
/** One hint about the run's setup, as the report schema's `notes` spells it. */
|
|
234
|
-
export
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
export type ReportNote =
|
|
242
|
+
| {
|
|
243
|
+
readonly kind: "unmapped-entry";
|
|
244
|
+
/** The directory of the manifest the note is about, below the target root. */
|
|
245
|
+
readonly path: string;
|
|
246
|
+
readonly key: "ts.entry_files";
|
|
247
|
+
readonly message: string;
|
|
248
|
+
}
|
|
249
|
+
| {
|
|
250
|
+
readonly kind: "computed-load";
|
|
251
|
+
/** The directory of the file that writes the load, below the target root. */
|
|
252
|
+
readonly path: string;
|
|
253
|
+
readonly message: string;
|
|
254
|
+
};
|
|
241
255
|
|
|
242
256
|
/** What one run answered, from which its report is built. */
|
|
243
257
|
export interface ReportInput {
|
|
@@ -258,6 +272,26 @@ export interface ReportInput {
|
|
|
258
272
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
259
273
|
/** The notes the run states, in any order. */
|
|
260
274
|
readonly notes?: readonly ReportNote[];
|
|
275
|
+
/** Whether the run lists each component in full, which `reporters.cascade: full` asks for. */
|
|
276
|
+
readonly listsComponents?: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Each component the findings name, once, with the members the first finding naming it
|
|
281
|
+
* carries, in the order the findings are: `findings` paired with their wire forms and
|
|
282
|
+
* sorted, then the edge evaluations' findings.
|
|
283
|
+
*/
|
|
284
|
+
function componentsOf(
|
|
285
|
+
findings: readonly (readonly [WireFinding, CompletedFinding])[],
|
|
286
|
+
): readonly WireComponent[] {
|
|
287
|
+
const listed = new Map<string, WireComponent>();
|
|
288
|
+
for (const [wire, finding] of findings) {
|
|
289
|
+
const { members } = finding.component;
|
|
290
|
+
if (members !== undefined && !listed.has(wire.component.id)) {
|
|
291
|
+
listed.set(wire.component.id, { id: wire.component.id, members: members.map(wireSymbol) });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return [...listed.values()];
|
|
261
295
|
}
|
|
262
296
|
|
|
263
297
|
/** Two strings ordered bytewise. */
|
|
@@ -342,9 +376,6 @@ export function wireFinding(finding: CompletedFinding): WireFinding {
|
|
|
342
376
|
root: finding.component.root,
|
|
343
377
|
symbol_count: finding.component.symbolCount,
|
|
344
378
|
deletable_lines: finding.component.deletableLines,
|
|
345
|
-
...(finding.component.members === undefined
|
|
346
|
-
? {}
|
|
347
|
-
: { members: finding.component.members.map(wireSymbol) }),
|
|
348
379
|
},
|
|
349
380
|
retained_by: finding.retainedBy,
|
|
350
381
|
configurations: finding.configurations,
|
|
@@ -470,7 +501,13 @@ function totalsOf(findings: readonly WireFinding[], result: PassResult): WireTot
|
|
|
470
501
|
|
|
471
502
|
/** Builds the report of one run, every array in the order the Contract fixes for it. */
|
|
472
503
|
export function buildReport(input: ReportInput): Report {
|
|
473
|
-
const
|
|
504
|
+
const paired = input.result.findings
|
|
505
|
+
.map((finding) => [wireFinding(finding), finding] as const)
|
|
506
|
+
.sort(([a], [b]) => canonical(a, b));
|
|
507
|
+
const findings = paired.map(([wire]) => wire);
|
|
508
|
+
const evaluated = input.result.edgeEvaluations.flatMap((one) =>
|
|
509
|
+
one.finding === undefined ? [] : [[wireFinding(one.finding), one.finding] as const],
|
|
510
|
+
);
|
|
474
511
|
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
475
512
|
.sort((a, b) => compare(a.id, b.id))
|
|
476
513
|
.map((one) => ({ id: one.id, role: "consumer" as const, reason: one.reason }));
|
|
@@ -500,6 +537,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
500
537
|
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
501
538
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
502
539
|
findings,
|
|
540
|
+
...(input.listsComponents === true
|
|
541
|
+
? { components: componentsOf([...paired, ...evaluated]) }
|
|
542
|
+
: {}),
|
|
503
543
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
504
544
|
stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
|
|
505
545
|
// Every gap shares the one component of the canonical key a gap supplies, the analyzer's
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Confidence, Severity } from "../config.ts";
|
|
|
2
2
|
import { jsonChunks } from "../json-chunks.ts";
|
|
3
3
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
4
4
|
import { declarationKey } from "../ref.ts";
|
|
5
|
-
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
5
|
+
import type { Report, WireComponent, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
6
6
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
7
7
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
8
8
|
import { utf8Bytes } from "./sha256.ts";
|
|
@@ -151,8 +151,11 @@ interface Related {
|
|
|
151
151
|
readonly endLine: number;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** The members the report lists for one component, none where it lists none. */
|
|
155
|
+
type MembersOf = (id: string) => WireComponent["members"];
|
|
156
|
+
|
|
154
157
|
/** Every position a finding names beyond its own, in the mapping's order, the first hundred. */
|
|
155
|
-
function relatedOf(found: WireFinding): readonly Related[] {
|
|
158
|
+
function relatedOf(found: WireFinding, members: MembersOf): readonly Related[] {
|
|
156
159
|
const related: Related[] = [];
|
|
157
160
|
for (const one of found.details.implementations ?? []) {
|
|
158
161
|
related.push({ label: "implementation", at: one.position, endLine: one.position.end_line });
|
|
@@ -164,7 +167,7 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
164
167
|
const at = found.details.name_literal;
|
|
165
168
|
related.push({ label: "name", at, endLine: at.end_line });
|
|
166
169
|
}
|
|
167
|
-
for (const one of found.component.
|
|
170
|
+
for (const one of members(found.component.id)) {
|
|
168
171
|
if (
|
|
169
172
|
declarationKey(one.ref, one.position.path) !==
|
|
170
173
|
declarationKey(found.symbol.ref, found.position.path)
|
|
@@ -188,7 +191,9 @@ function messageWithLinks(message: string, related: readonly Related[]): string
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/** Every member of the finding the mapping places nowhere else, under its schema name. */
|
|
191
|
-
|
|
194
|
+
/** A result's member bag, its component holding the members the report lists for it once. */
|
|
195
|
+
function propertiesOf(found: WireFinding, members: MembersOf): Record<string, unknown> {
|
|
196
|
+
const listed = members(found.component.id);
|
|
192
197
|
return {
|
|
193
198
|
language: found.language,
|
|
194
199
|
symbol: found.symbol,
|
|
@@ -199,7 +204,7 @@ function propertiesOf(found: WireFinding): Record<string, unknown> {
|
|
|
199
204
|
: { liveness_relation: found.liveness_relation }),
|
|
200
205
|
test_only: found.test_only,
|
|
201
206
|
generated: found.generated,
|
|
202
|
-
component: found.component,
|
|
207
|
+
component: listed.length === 0 ? found.component : { ...found.component, members: listed },
|
|
203
208
|
retained_by: found.retained_by,
|
|
204
209
|
configurations: found.configurations,
|
|
205
210
|
consumers_loaded: found.consumers_loaded,
|
|
@@ -220,8 +225,9 @@ function findingResult(
|
|
|
220
225
|
found: WireFinding,
|
|
221
226
|
index: ReadonlyMap<string, number>,
|
|
222
227
|
hashOf: (at: Located) => string,
|
|
228
|
+
members: MembersOf,
|
|
223
229
|
): Record<string, unknown> {
|
|
224
|
-
const related = relatedOf(found);
|
|
230
|
+
const related = relatedOf(found, members);
|
|
225
231
|
return {
|
|
226
232
|
ruleId: found.code,
|
|
227
233
|
ruleIndex: ruleIndex(index, found.code),
|
|
@@ -241,7 +247,7 @@ function findingResult(
|
|
|
241
247
|
primaryLocationLineHash: hashOf(found.position),
|
|
242
248
|
"deadsetSymbolRef/v1": symbolFingerprint(found.code, found.symbol.ref),
|
|
243
249
|
},
|
|
244
|
-
properties: propertiesOf(found),
|
|
250
|
+
properties: propertiesOf(found, members),
|
|
245
251
|
};
|
|
246
252
|
}
|
|
247
253
|
|
|
@@ -275,8 +281,10 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
|
|
|
275
281
|
const ruleList = rules(kinds);
|
|
276
282
|
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
277
283
|
const hashOf = lineHashCache(options.readSource);
|
|
284
|
+
const listed = new Map((report.components ?? []).map((one) => [one.id, one.members]));
|
|
285
|
+
const members: MembersOf = (id) => listed.get(id) ?? [];
|
|
278
286
|
const results = [
|
|
279
|
-
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
287
|
+
...report.findings.map((found) => findingResult(found, index, hashOf, members)),
|
|
280
288
|
...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
|
|
281
289
|
];
|
|
282
290
|
const languages = [...report.analyzer.languages].sort().join("+");
|
package/src/roots.ts
CHANGED
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
isArrowFunction,
|
|
13
|
+
isAssertionExpression,
|
|
12
14
|
isExpressionWithTypeArguments,
|
|
13
15
|
isFunctionLikeDeclaration,
|
|
16
|
+
isIdentifier,
|
|
17
|
+
isMethodDeclaration,
|
|
18
|
+
isObjectLiteralExpression,
|
|
19
|
+
isParenthesizedExpression,
|
|
20
|
+
isPropertyAssignment,
|
|
21
|
+
isReturnStatement,
|
|
22
|
+
isSatisfiesExpression,
|
|
14
23
|
isPropertyAccessExpression,
|
|
15
24
|
isQualifiedName,
|
|
16
25
|
isTypeQueryNode,
|
|
@@ -18,7 +27,10 @@ import {
|
|
|
18
27
|
isPropertyDeclaration,
|
|
19
28
|
isVariableDeclaration,
|
|
20
29
|
isVariableStatement,
|
|
30
|
+
SyntaxKind,
|
|
31
|
+
type Expression,
|
|
21
32
|
type Node,
|
|
33
|
+
type ObjectLiteralExpression,
|
|
22
34
|
type SourceFile,
|
|
23
35
|
} from "@typescript/native/ast";
|
|
24
36
|
import { aliasChains } from "./alias-chain.ts";
|
|
@@ -67,6 +79,8 @@ export type RootKind =
|
|
|
67
79
|
| "pattern"
|
|
68
80
|
/** A declaration of a function or statement a type error skipped, and what it may have selected. */
|
|
69
81
|
| "type-error"
|
|
82
|
+
/** An ambient global interface merging into one declared outside the target, and its holders. */
|
|
83
|
+
| "global-augmentation"
|
|
70
84
|
/** A file a tool or the platform enters by its own convention, and what it exports where the tool reads it. */
|
|
71
85
|
| EntryRule;
|
|
72
86
|
|
|
@@ -446,6 +460,12 @@ export function roots<Brand>(
|
|
|
446
460
|
published.push(...entryFiles(entry));
|
|
447
461
|
}
|
|
448
462
|
}
|
|
463
|
+
if (options.publishedAPI) {
|
|
464
|
+
// A library's bundle hands what its entry exports to every program that loads it.
|
|
465
|
+
published.push(
|
|
466
|
+
...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
449
469
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
450
470
|
const patterned: { file: SourceFile; pattern: string }[] = [];
|
|
451
471
|
for (const pattern of options.entryFiles) {
|
|
@@ -713,17 +733,26 @@ function publishedAPI<Brand>(
|
|
|
713
733
|
const symbol = byId.get(id);
|
|
714
734
|
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
715
735
|
});
|
|
716
|
-
|
|
736
|
+
const inferred = inferredTypeSymbols(
|
|
717
737
|
project,
|
|
718
738
|
round.flatMap((id) => {
|
|
719
739
|
const symbol = byId.get(id);
|
|
720
740
|
return symbol === undefined ? [] : nodes.nodesOf(symbol);
|
|
721
741
|
}),
|
|
722
|
-
)
|
|
742
|
+
);
|
|
743
|
+
for (const symbol of inferred.types) {
|
|
723
744
|
for (const target of chains.chainOf(symbol)) {
|
|
724
745
|
reach(target.id);
|
|
725
746
|
}
|
|
726
747
|
}
|
|
748
|
+
for (const symbol of inferred.members) {
|
|
749
|
+
for (const target of chains.chainOf(symbol)) {
|
|
750
|
+
const member = byId.get(target.id);
|
|
751
|
+
if (member?.visibility === "public" || member?.visibility === "protected") {
|
|
752
|
+
reach(target.id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
727
756
|
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
728
757
|
if (symbol === UNANSWERED) {
|
|
729
758
|
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
@@ -783,30 +812,139 @@ function declarationNodes(
|
|
|
783
812
|
const INFERRED_DEPTH = 4;
|
|
784
813
|
|
|
785
814
|
/**
|
|
786
|
-
* The
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
|
|
790
|
-
|
|
815
|
+
* The values one function body returns, each through the type assertions around it: the
|
|
816
|
+
* value a caller receives carries the operand's type whatever type the assertion or the
|
|
817
|
+
* signature names. A nested function's returns are its own.
|
|
818
|
+
*/
|
|
819
|
+
function returnedValues(body: Node): readonly Expression[] {
|
|
820
|
+
const found: Expression[] = [];
|
|
821
|
+
const add = (value: Expression): void => {
|
|
822
|
+
for (let at: Expression | undefined = value; at !== undefined;) {
|
|
823
|
+
found.push(at);
|
|
824
|
+
at =
|
|
825
|
+
isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
|
|
826
|
+
? at.expression
|
|
827
|
+
: undefined;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
|
|
831
|
+
return found;
|
|
832
|
+
}
|
|
833
|
+
if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
|
|
834
|
+
add(body as Expression);
|
|
835
|
+
return found;
|
|
836
|
+
}
|
|
837
|
+
const visit = (node: Node): void => {
|
|
838
|
+
if (isFunctionLikeDeclaration(node)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (isReturnStatement(node) && node.expression !== undefined) {
|
|
842
|
+
add(node.expression);
|
|
843
|
+
}
|
|
844
|
+
node.forEachChild(visit);
|
|
845
|
+
};
|
|
846
|
+
body.forEachChild(visit);
|
|
847
|
+
return found;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
|
|
851
|
+
function returnedLiteral(value: Expression, body: Node): ObjectLiteralExpression | undefined {
|
|
852
|
+
if (isObjectLiteralExpression(value)) {
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
if (!isIdentifier(value)) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
let found: ObjectLiteralExpression | undefined;
|
|
859
|
+
const visit = (node: Node): void => {
|
|
860
|
+
if (
|
|
861
|
+
isVariableDeclaration(node) &&
|
|
862
|
+
isIdentifier(node.name) &&
|
|
863
|
+
node.name.text === value.text &&
|
|
864
|
+
node.initializer !== undefined
|
|
865
|
+
) {
|
|
866
|
+
let at: Expression = node.initializer;
|
|
867
|
+
while (
|
|
868
|
+
isAssertionExpression(at) ||
|
|
869
|
+
isSatisfiesExpression(at) ||
|
|
870
|
+
isParenthesizedExpression(at)
|
|
871
|
+
) {
|
|
872
|
+
at = at.expression;
|
|
873
|
+
}
|
|
874
|
+
found = isObjectLiteralExpression(at) ? at : found;
|
|
875
|
+
}
|
|
876
|
+
if (!isFunctionLikeDeclaration(node)) {
|
|
877
|
+
node.forEachChild(visit);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
body.forEachChild(visit);
|
|
881
|
+
return found;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* The values one function body returns, and those each method of an object literal it
|
|
886
|
+
* returns returns, to a fixed depth: a caller holding the literal calls its methods, and
|
|
887
|
+
* receives what they return.
|
|
888
|
+
*/
|
|
889
|
+
function returnedThroughLiterals(body: Node, depth = 0): readonly Expression[] {
|
|
890
|
+
const values = returnedValues(body);
|
|
891
|
+
if (depth >= INFERRED_DEPTH) {
|
|
892
|
+
return values;
|
|
893
|
+
}
|
|
894
|
+
const found = [...values];
|
|
895
|
+
for (const value of values) {
|
|
896
|
+
for (const property of returnedLiteral(value, body)?.properties ?? []) {
|
|
897
|
+
const fn = isMethodDeclaration(property)
|
|
898
|
+
? property
|
|
899
|
+
: isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
|
|
900
|
+
? property.initializer
|
|
901
|
+
: undefined;
|
|
902
|
+
if (fn?.body !== undefined) {
|
|
903
|
+
found.push(...returnedThroughLiterals(fn.body, depth + 1));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return found;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The declarations of the types the compiler infers where a declaration writes none, and
|
|
912
|
+
* of the values a function returns: the type of a variable or a property with no
|
|
913
|
+
* annotation, the result of a function or a method with no result annotation, and the type
|
|
914
|
+
* of each value its body returns, each with the types its type arguments and its union or
|
|
915
|
+
* intersection constituents name, to a fixed depth; and as `members` the properties of each
|
|
916
|
+
* returned value's type. A question the checker leaves unanswered names nothing.
|
|
791
917
|
*/
|
|
792
918
|
function inferredTypeSymbols<Brand>(
|
|
793
919
|
project: ProjectView<Brand>,
|
|
794
920
|
declarations: readonly Node[],
|
|
795
|
-
): readonly TSSymbol[] {
|
|
921
|
+
): { readonly types: readonly TSSymbol[]; readonly members: readonly TSSymbol[] } {
|
|
796
922
|
const { queries } = project;
|
|
797
|
-
const valued = declarations.flatMap((node) =>
|
|
923
|
+
const valued: Node[] = declarations.flatMap((node) =>
|
|
798
924
|
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
799
925
|
node.type === undefined &&
|
|
800
926
|
!isFunctionLikeDeclaration(node.initializer ?? node)
|
|
801
927
|
? [node.name]
|
|
802
928
|
: [],
|
|
803
929
|
);
|
|
930
|
+
const returned = declarations.flatMap((node) => {
|
|
931
|
+
const signature = isFunctionLikeDeclaration(node)
|
|
932
|
+
? node
|
|
933
|
+
: isVariableDeclaration(node) &&
|
|
934
|
+
node.initializer !== undefined &&
|
|
935
|
+
isFunctionLikeDeclaration(node.initializer)
|
|
936
|
+
? node.initializer
|
|
937
|
+
: undefined;
|
|
938
|
+
return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
|
|
939
|
+
});
|
|
804
940
|
const types: Type[] = [];
|
|
805
|
-
|
|
941
|
+
const returnedTypes: Type[] = [];
|
|
942
|
+
queries.typesAt([...valued, ...returned]).forEach((type, at) => {
|
|
806
943
|
if (type !== UNANSWERED && type !== undefined) {
|
|
807
|
-
types.push(type);
|
|
944
|
+
(at < valued.length ? types : returnedTypes).push(type);
|
|
808
945
|
}
|
|
809
946
|
});
|
|
947
|
+
types.push(...returnedTypes);
|
|
810
948
|
for (const node of declarations) {
|
|
811
949
|
const signature = isFunctionLikeDeclaration(node)
|
|
812
950
|
? node
|
|
@@ -849,7 +987,19 @@ function inferredTypeSymbols<Brand>(
|
|
|
849
987
|
types.forEach((type) => {
|
|
850
988
|
visit(type, 0);
|
|
851
989
|
});
|
|
852
|
-
|
|
990
|
+
// A returned value carries each property of its type, one an anonymous type declares or
|
|
991
|
+
// an intersection merges included, which no type name reaches.
|
|
992
|
+
const members = new Map<number, TSSymbol>();
|
|
993
|
+
for (const type of returnedTypes) {
|
|
994
|
+
const parts = queries.constituents(type);
|
|
995
|
+
for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
|
|
996
|
+
const properties = queries.propertiesOf(one);
|
|
997
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
998
|
+
members.set(property.id, property);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return { types: [...found.values()], members: [...members.values()] };
|
|
853
1003
|
}
|
|
854
1004
|
|
|
855
1005
|
/**
|