@cplieger/deadset-ts 7.0.0-dev.1 → 7.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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/src/suppress.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* The suppression grammar's records, and the ledger that decides what each one did. A
|
|
3
3
|
* record is one code bound to one site with its reason, from an inline directive, an
|
|
4
4
|
* ignore entry or a baseline row. A bound record marks its declaration live before the
|
|
5
|
-
* sweep,
|
|
6
|
-
*
|
|
5
|
+
* sweep, unless its code reports a part or a dependency row, and is in effect when it
|
|
6
|
+
* withholds a finding its code would have produced there, dormant when the configuration
|
|
7
|
+
* withholds every such finding anyway, and stale otherwise.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { ConfigError } from "./config.ts";
|
|
@@ -65,9 +66,6 @@ export interface Suppressions {
|
|
|
65
66
|
readonly refusals: readonly Refusal[];
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/** A target that carries no suppression. */
|
|
69
|
-
export const NO_SUPPRESSIONS: Suppressions = { records: [], refusals: [] };
|
|
70
|
-
|
|
71
69
|
/**
|
|
72
70
|
* A directive, an entry or a document the grammar refuses before any finding exists: an
|
|
73
71
|
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
@@ -144,41 +142,69 @@ export type Verdict = "in-effect" | "dormant" | "stale";
|
|
|
144
142
|
* them withholds is dormant.
|
|
145
143
|
*/
|
|
146
144
|
export interface Dials<F extends Finding = Finding> {
|
|
147
|
-
/** Whether the configuration
|
|
148
|
-
readonly
|
|
145
|
+
/** Whether the configuration sets the severity of one code to `allow`. */
|
|
146
|
+
readonly allows: (code: string) => boolean;
|
|
149
147
|
/** Whether it withholds one finding: by its severity, its confidence, or its component's root. */
|
|
150
148
|
readonly withholds: (finding: F) => boolean;
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
/** What every record did, and the findings the records withhold. */
|
|
154
|
-
export interface Ledger
|
|
152
|
+
export interface Ledger {
|
|
155
153
|
/** One verdict per record, in reading order. */
|
|
156
154
|
readonly verdicts: readonly Verdict[];
|
|
157
|
-
/** Per record, the finding it claimed, where it claimed one. */
|
|
158
|
-
readonly claims: readonly (F | undefined)[];
|
|
159
155
|
/** Whether a record withholds one finding of the run. */
|
|
160
156
|
readonly withheld: (finding: Finding) => boolean;
|
|
161
157
|
}
|
|
162
158
|
|
|
163
159
|
/**
|
|
164
|
-
* The subject kinds that are a row
|
|
165
|
-
*
|
|
160
|
+
* The subject kinds that are a dependency row, each with the code that reports one. An
|
|
161
|
+
* ignore entry or a baseline row naming the row's reference and file withholds its finding
|
|
162
|
+
* and marks nothing, and an inline directive never binds one.
|
|
163
|
+
*/
|
|
164
|
+
const DEPENDENCY_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
165
|
+
["dependency", ["DS1601"]],
|
|
166
|
+
["module-directive", ["DS1605"]],
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The subject kinds that are a row of a document rather than a declaration of the program,
|
|
171
|
+
* each with the codes that report one. Apart from a dependency row, nothing withholds a
|
|
172
|
+
* finding about one.
|
|
166
173
|
*/
|
|
167
|
-
const ROW_SUBJECTS:
|
|
168
|
-
"file",
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"edge",
|
|
174
|
+
const ROW_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
175
|
+
["file", ["DS1501", "DS1502"]],
|
|
176
|
+
...DEPENDENCY_SUBJECTS,
|
|
177
|
+
["root", ["DS1704"]],
|
|
178
|
+
["configured-declaration", ["DS1706"]],
|
|
179
|
+
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
180
|
+
["edge", ["DS1705"]],
|
|
175
181
|
]);
|
|
176
182
|
|
|
177
|
-
/** Whether a finding's subject is a row of a document
|
|
183
|
+
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
178
184
|
export function isRowSubject(kind: string): boolean {
|
|
179
185
|
return ROW_SUBJECTS.has(kind);
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
/** Whether a code reports a row of a document. */
|
|
189
|
+
export function isRowCode(code: string): boolean {
|
|
190
|
+
return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Whether a finding's subject is a dependency row. */
|
|
194
|
+
export function isDependencySubject(kind: string): boolean {
|
|
195
|
+
return DEPENDENCY_SUBJECTS.has(kind);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
199
|
+
export function isDependencyCode(code: string): boolean {
|
|
200
|
+
return [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Whether a code reports a row of a document that no record of any mechanism can name. */
|
|
204
|
+
export function isUnsuppressibleCode(code: string): boolean {
|
|
205
|
+
return isRowCode(code) && !isDependencyCode(code);
|
|
206
|
+
}
|
|
207
|
+
|
|
182
208
|
/**
|
|
183
209
|
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
184
210
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
@@ -218,10 +244,21 @@ function claimKey(finding: Finding): string {
|
|
|
218
244
|
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
219
245
|
* the declaration at the finding's position, or it names the finding's reference and the
|
|
220
246
|
* file the finding is reported in, each exactly. The second is how an entry copied from a
|
|
221
|
-
* finding about a part of a declaration reaches the part
|
|
247
|
+
* finding about a part of a declaration reaches the part, and the only way any record
|
|
248
|
+
* reaches a dependency row.
|
|
222
249
|
*/
|
|
223
250
|
export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
224
|
-
if (record.code !== finding.code
|
|
251
|
+
if (record.code !== finding.code) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
|
|
255
|
+
return (
|
|
256
|
+
record.mechanism !== "inline" &&
|
|
257
|
+
record.symbol === finding.symbol.ref &&
|
|
258
|
+
record.path === finding.position.path
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (ROW_SUBJECTS.has(finding.symbol.kind)) {
|
|
225
262
|
return false;
|
|
226
263
|
}
|
|
227
264
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
@@ -232,46 +269,41 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
232
269
|
|
|
233
270
|
/**
|
|
234
271
|
* Decides every record against `would`, the findings the run would produce were no record
|
|
235
|
-
* there: swept with no mark
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
272
|
+
* there: swept with no mark for a liveness kind, and as swept otherwise. The first record
|
|
273
|
+
* in reading order claims a finding, so a second naming it is stale; a part-kind record
|
|
274
|
+
* claims every finding of its code about a part of its declaration. A record is dormant
|
|
275
|
+
* when the dials withhold every finding it claims, read from those findings alone, or when
|
|
276
|
+
* it claims none and its code's severity is `allow`.
|
|
240
277
|
*/
|
|
241
278
|
export function ledgerOf<F extends Finding>(
|
|
242
279
|
records: readonly SuppressionRecord[],
|
|
243
280
|
would: readonly F[],
|
|
244
281
|
dials: Dials<F>,
|
|
245
|
-
): Ledger
|
|
282
|
+
): Ledger {
|
|
246
283
|
const taken = new Set<string>();
|
|
247
284
|
const verdicts: Verdict[] = [];
|
|
248
|
-
const claims: (F | undefined)[] = [];
|
|
249
285
|
for (const record of records) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
286
|
+
const open = (finding: F): boolean => !taken.has(claimKey(finding)) && pairs(record, finding);
|
|
287
|
+
const first = would.find(open);
|
|
288
|
+
const claimed =
|
|
289
|
+
first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
|
|
290
|
+
if (claimed.length === 0) {
|
|
291
|
+
verdicts.push(record.bound !== "" && dials.allows(record.code) ? "dormant" : "stale");
|
|
253
292
|
continue;
|
|
254
293
|
}
|
|
255
|
-
const claimed
|
|
256
|
-
|
|
257
|
-
);
|
|
258
|
-
claims.push(claimed);
|
|
259
|
-
if (claimed === undefined) {
|
|
260
|
-
verdicts.push("stale");
|
|
261
|
-
continue;
|
|
294
|
+
for (const finding of claimed) {
|
|
295
|
+
taken.add(claimKey(finding));
|
|
262
296
|
}
|
|
263
|
-
|
|
264
|
-
verdicts.push(dials.withholds(claimed) ? "dormant" : "in-effect");
|
|
297
|
+
verdicts.push(claimed.every((finding) => dials.withholds(finding)) ? "dormant" : "in-effect");
|
|
265
298
|
}
|
|
266
299
|
return {
|
|
267
300
|
verdicts,
|
|
268
|
-
|
|
269
|
-
withheld: (finding) => taken.has(claimKey(finding)) && !ROW_SUBJECTS.has(finding.symbol.kind),
|
|
301
|
+
withheld: (finding) => taken.has(claimKey(finding)),
|
|
270
302
|
};
|
|
271
303
|
}
|
|
272
304
|
|
|
273
305
|
/** The two suppression counts a report's totals carry. */
|
|
274
|
-
|
|
306
|
+
interface SuppressionCounts {
|
|
275
307
|
/** The records in effect. */
|
|
276
308
|
readonly inEffect: number;
|
|
277
309
|
/** The directives, entries and rows that carry a reason, each once whatever its codes. */
|
|
@@ -279,9 +311,9 @@ export interface SuppressionCounts {
|
|
|
279
311
|
}
|
|
280
312
|
|
|
281
313
|
/** The suppression counts of one run's records and the ledger over them. */
|
|
282
|
-
export function suppressionCounts
|
|
314
|
+
export function suppressionCounts(
|
|
283
315
|
records: readonly SuppressionRecord[],
|
|
284
|
-
ledger: Ledger
|
|
316
|
+
ledger: Ledger,
|
|
285
317
|
): SuppressionCounts {
|
|
286
318
|
const sites = new Set(
|
|
287
319
|
records.map((record) => `${record.mechanism}\u0000${positionKey(record.site)}`),
|
package/src/template-field.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.t
|
|
|
22
22
|
import { isComponentFile, type Position } from "./position.ts";
|
|
23
23
|
|
|
24
24
|
/** One template, by its path below the target root. */
|
|
25
|
-
|
|
25
|
+
interface TemplateFile {
|
|
26
26
|
readonly path: string;
|
|
27
27
|
readonly text: string;
|
|
28
28
|
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members read where code the target cannot see receives a value of a target type. An
|
|
3
|
+
* object literal argument of a call or a `new` whose callee has no implementation in the
|
|
4
|
+
* target has every member of the parameter type it is checked against read, nested
|
|
5
|
+
* literals included. An `as` expression over an object no file of the project declares, and
|
|
6
|
+
* a `new` through an unimplemented construct signature, hand the target a value the
|
|
7
|
+
* outside code holds, so every member of the type it is given is read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
isArrowFunction,
|
|
12
|
+
isAsExpression,
|
|
13
|
+
isCallExpression,
|
|
14
|
+
isClassDeclaration,
|
|
15
|
+
isClassExpression,
|
|
16
|
+
isFunctionDeclaration,
|
|
17
|
+
isFunctionExpression,
|
|
18
|
+
isHeritageClause,
|
|
19
|
+
isIdentifier,
|
|
20
|
+
isMethodDeclaration,
|
|
21
|
+
isNewExpression,
|
|
22
|
+
isObjectLiteralExpression,
|
|
23
|
+
isParameterDeclaration,
|
|
24
|
+
isParenthesizedExpression,
|
|
25
|
+
isPropertyAccessExpression,
|
|
26
|
+
isPropertyAssignment,
|
|
27
|
+
isPropertyDeclaration,
|
|
28
|
+
isSatisfiesExpression,
|
|
29
|
+
isTypeReferenceNode,
|
|
30
|
+
isVariableDeclaration,
|
|
31
|
+
type CallExpression,
|
|
32
|
+
type Expression,
|
|
33
|
+
type NewExpression,
|
|
34
|
+
type Node,
|
|
35
|
+
type ObjectLiteralExpression,
|
|
36
|
+
type ParameterDeclaration,
|
|
37
|
+
type SourceFile,
|
|
38
|
+
} from "@typescript/native/unstable/ast";
|
|
39
|
+
import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
40
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
41
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
42
|
+
import { renderPosition } from "./position.ts";
|
|
43
|
+
import { UNANSWERED } from "./query.ts";
|
|
44
|
+
import type { Reference } from "./references.ts";
|
|
45
|
+
import type { ProjectView } from "./session.ts";
|
|
46
|
+
|
|
47
|
+
/** One call or construction passing an object literal, and the declaration that writes it. */
|
|
48
|
+
interface Passing {
|
|
49
|
+
readonly file: SourceFile;
|
|
50
|
+
readonly from: string;
|
|
51
|
+
readonly call: CallExpression | NewExpression;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A node where an outside value takes a type of the target, and the declaration that writes it. */
|
|
55
|
+
interface HandedOver {
|
|
56
|
+
readonly file: SourceFile;
|
|
57
|
+
readonly from: string;
|
|
58
|
+
readonly node: Expression;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The value an `as` expression asserts a type of, through every `as` and parenthesis around it. */
|
|
62
|
+
function castOperand(node: Expression): Expression {
|
|
63
|
+
let at = node;
|
|
64
|
+
while (isAsExpression(at) || isParenthesizedExpression(at)) {
|
|
65
|
+
at = at.expression;
|
|
66
|
+
}
|
|
67
|
+
return at;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whether a declaration node carries a body of statements or an expression. */
|
|
71
|
+
function hasBody(node: Node): boolean {
|
|
72
|
+
return (node as { readonly body?: Node }).body !== undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isFunctionLike(node: Node | undefined): boolean {
|
|
76
|
+
return (
|
|
77
|
+
node !== undefined &&
|
|
78
|
+
(isArrowFunction(node) ||
|
|
79
|
+
isFunctionExpression(node) ||
|
|
80
|
+
isFunctionDeclaration(node) ||
|
|
81
|
+
isClassExpression(node))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The name a declaration is declared under, where it has one an identifier spells. */
|
|
86
|
+
function nameOf(node: Node): string | undefined {
|
|
87
|
+
const name = (node as { readonly name?: Node }).name;
|
|
88
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether an overload signature has its implementation beside it: a declaration of the same
|
|
93
|
+
* name and kind, in the same statement list or class body, that carries a body.
|
|
94
|
+
*/
|
|
95
|
+
function overloadImplemented(node: Node): boolean {
|
|
96
|
+
if (!isFunctionDeclaration(node) && !isMethodDeclaration(node)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const name = nameOf(node);
|
|
100
|
+
let implemented = false;
|
|
101
|
+
node.parent.forEachChild((sibling) => {
|
|
102
|
+
implemented ||= sibling.kind === node.kind && nameOf(sibling) === name && hasBody(sibling);
|
|
103
|
+
});
|
|
104
|
+
return implemented;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every object literal one argument holds, the argument itself included, through nested property values. */
|
|
108
|
+
function literalsOf(argument: Expression): readonly ObjectLiteralExpression[] {
|
|
109
|
+
if (!isObjectLiteralExpression(argument)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
return [
|
|
113
|
+
argument,
|
|
114
|
+
...argument.properties.flatMap((property) =>
|
|
115
|
+
isPropertyAssignment(property) ? literalsOf(property.initializer) : [],
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The reads the object-literal arguments of one project's own files make of the members of
|
|
122
|
+
* the types they are checked against, where the callee has no implementation in the target.
|
|
123
|
+
*/
|
|
124
|
+
export function unimplementedCalleeReads<Brand>(
|
|
125
|
+
project: ProjectView<Brand>,
|
|
126
|
+
held: Inventory,
|
|
127
|
+
chains: AliasChains,
|
|
128
|
+
root: string,
|
|
129
|
+
testFiles: ReadonlySet<string>,
|
|
130
|
+
): readonly Reference[] {
|
|
131
|
+
const queries = project.queries;
|
|
132
|
+
const own = project.ownPaths();
|
|
133
|
+
const passing: Passing[] = [];
|
|
134
|
+
const implementing: Node[] = [];
|
|
135
|
+
const asserted: (HandedOver & { readonly operand: Expression })[] = [];
|
|
136
|
+
const constructed: Passing[] = [];
|
|
137
|
+
const handing: (CallExpression | NewExpression)[] = [];
|
|
138
|
+
for (const file of project.ownSourceFiles()) {
|
|
139
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
140
|
+
if (fileId === undefined) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const visit = (node: Node, from: string): void => {
|
|
144
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
145
|
+
if (
|
|
146
|
+
(isCallExpression(node) || isNewExpression(node)) &&
|
|
147
|
+
(node.arguments ?? []).some((argument) => isObjectLiteralExpression(argument))
|
|
148
|
+
) {
|
|
149
|
+
passing.push({ file, from: holder, call: node });
|
|
150
|
+
}
|
|
151
|
+
if (isNewExpression(node)) {
|
|
152
|
+
constructed.push({ file, from: holder, call: node });
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
(isCallExpression(node) || isNewExpression(node)) &&
|
|
156
|
+
(node.arguments ?? []).some(
|
|
157
|
+
(argument) => isFunctionLike(argument) || isIdentifier(argument),
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
handing.push(node);
|
|
161
|
+
}
|
|
162
|
+
if (isAsExpression(node) && !isAsExpression(node.parent)) {
|
|
163
|
+
asserted.push({ file, from: holder, node, operand: castOperand(node) });
|
|
164
|
+
}
|
|
165
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
166
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
167
|
+
if (isHeritageClause(clause)) {
|
|
168
|
+
implementing.push(
|
|
169
|
+
...clause.types.map((one) =>
|
|
170
|
+
isTypeReferenceNode(one) ? one.typeName : one.expression,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// An object literal typed as an interface implements that interface's methods.
|
|
177
|
+
if (
|
|
178
|
+
isVariableDeclaration(node) &&
|
|
179
|
+
node.type !== undefined &&
|
|
180
|
+
isTypeReferenceNode(node.type) &&
|
|
181
|
+
node.initializer !== undefined &&
|
|
182
|
+
isObjectLiteralExpression(node.initializer)
|
|
183
|
+
) {
|
|
184
|
+
implementing.push(node.type.typeName);
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
(isSatisfiesExpression(node) || isAsExpression(node)) &&
|
|
188
|
+
isObjectLiteralExpression(node.expression) &&
|
|
189
|
+
isTypeReferenceNode(node.type)
|
|
190
|
+
) {
|
|
191
|
+
implementing.push(node.type.typeName);
|
|
192
|
+
}
|
|
193
|
+
node.forEachChild((child) => {
|
|
194
|
+
visit(child, holder);
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
file.forEachChild((child) => {
|
|
198
|
+
visit(child, fileId);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (passing.length === 0 && asserted.length === 0 && constructed.length === 0) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const resolved = (symbol: TSSymbol | undefined | typeof UNANSWERED): TSSymbol | undefined => {
|
|
206
|
+
if (symbol === UNANSWERED || symbol === undefined) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
210
|
+
return symbol;
|
|
211
|
+
}
|
|
212
|
+
const target = queries.aliased(symbol);
|
|
213
|
+
return target === UNANSWERED ? undefined : target;
|
|
214
|
+
};
|
|
215
|
+
const declarationsOf = (symbol: TSSymbol | undefined): readonly Node[] =>
|
|
216
|
+
(symbol?.declarations ?? []).flatMap((handle) => {
|
|
217
|
+
const node = project.declarationAt(handle)?.node;
|
|
218
|
+
return node === undefined ? [] : [node];
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const implemented = new Set<Node>(
|
|
222
|
+
queries.symbolsAt(implementing).flatMap((symbol) => declarationsOf(resolved(symbol))),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
/** Whether a declaration is a function or a class of the target, or a variable or property it initializes. */
|
|
226
|
+
const ownFunction = (node: Node): boolean =>
|
|
227
|
+
own.has(node.getSourceFile().fileName) &&
|
|
228
|
+
(isFunctionLike(node) ||
|
|
229
|
+
((isVariableDeclaration(node) || isPropertyAssignment(node) || isPropertyDeclaration(node)) &&
|
|
230
|
+
isFunctionLike(node.initializer)));
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether a function or a class of the target reaches one parameter of a function of the
|
|
234
|
+
* target, passed for it by a call that names the function. A call through another name
|
|
235
|
+
* is not followed, so its callee keeps no implementation and its literal reads more.
|
|
236
|
+
*/
|
|
237
|
+
const reachedParameters = new Map<ParameterDeclaration, boolean>();
|
|
238
|
+
const parameterReached = (parameter: ParameterDeclaration): boolean => {
|
|
239
|
+
const known = reachedParameters.get(parameter);
|
|
240
|
+
if (known !== undefined) {
|
|
241
|
+
return known;
|
|
242
|
+
}
|
|
243
|
+
const fn = parameter.parent;
|
|
244
|
+
const fnName = nameOf(fn) ?? (isVariableDeclaration(fn.parent) ? nameOf(fn.parent) : undefined);
|
|
245
|
+
const at =
|
|
246
|
+
(fn as { readonly parameters?: readonly Node[] }).parameters?.indexOf(parameter) ?? -1;
|
|
247
|
+
const passed = handing.flatMap((call) => {
|
|
248
|
+
const callee = call.expression;
|
|
249
|
+
const name = isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
250
|
+
if (fnName === undefined || !isIdentifier(name) || name.text !== fnName) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const signature = queries.resolvedSignature(call);
|
|
254
|
+
const handle =
|
|
255
|
+
signature === UNANSWERED || signature === undefined ? undefined : signature.declaration;
|
|
256
|
+
const argument =
|
|
257
|
+
handle === undefined || project.declarationAt(handle)?.node !== fn
|
|
258
|
+
? undefined
|
|
259
|
+
: (call.arguments ?? [])[at];
|
|
260
|
+
return argument === undefined || at < 0 ? [] : [argument];
|
|
261
|
+
});
|
|
262
|
+
const named = passed.filter((argument) => isIdentifier(argument));
|
|
263
|
+
const reached =
|
|
264
|
+
passed.some((argument) => isFunctionLike(argument)) ||
|
|
265
|
+
queries
|
|
266
|
+
.symbolsAt(named)
|
|
267
|
+
.some((symbol) => declarationsOf(resolved(symbol)).some((node) => ownFunction(node)));
|
|
268
|
+
reachedParameters.set(parameter, reached);
|
|
269
|
+
return reached;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** Whether some function, class or object literal of the target stands for a signature. */
|
|
273
|
+
const standsFor = (passed: Passing): boolean => {
|
|
274
|
+
const signature = queries.resolvedSignature(passed.call);
|
|
275
|
+
const handle =
|
|
276
|
+
signature === UNANSWERED || signature === undefined ? undefined : signature.declaration;
|
|
277
|
+
const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
278
|
+
if (declaration === undefined) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (hasBody(declaration) || overloadImplemented(declaration)) {
|
|
282
|
+
return own.has(handle?.path ?? "");
|
|
283
|
+
}
|
|
284
|
+
const holder = declaration.parent as Node | undefined;
|
|
285
|
+
if (holder !== undefined && implemented.has(holder)) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
const callee = passed.call.expression;
|
|
289
|
+
const name = isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
290
|
+
const [named] = queries.symbolsAt([name]);
|
|
291
|
+
return declarationsOf(resolved(named)).some((node) =>
|
|
292
|
+
isParameterDeclaration(node)
|
|
293
|
+
? own.has(node.getSourceFile().fileName) && parameterReached(node)
|
|
294
|
+
: ownFunction(node) && !isFunctionLike(node),
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const literals: { readonly passed: Passing; readonly literal: ObjectLiteralExpression }[] = [];
|
|
299
|
+
for (const passed of passing) {
|
|
300
|
+
if (standsFor(passed)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
for (const argument of passed.call.arguments ?? []) {
|
|
304
|
+
literals.push(...literalsOf(argument).map((literal) => ({ passed, literal })));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const contexts = queries.contextualTypes(literals.map((one) => one.literal));
|
|
308
|
+
|
|
309
|
+
const found: Reference[] = [];
|
|
310
|
+
literals.forEach(({ passed, literal }, at) => {
|
|
311
|
+
const context = contexts[at];
|
|
312
|
+
if (context === UNANSWERED || context === undefined) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const parts = queries.constituents(context);
|
|
316
|
+
const position = renderPosition(passed.file, root, literal.getStart());
|
|
317
|
+
for (const part of parts === UNANSWERED ? [context] : parts) {
|
|
318
|
+
const properties = queries.propertiesOf(part);
|
|
319
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
320
|
+
for (const to of chains.declarationsOf(property)) {
|
|
321
|
+
found.push({
|
|
322
|
+
from: passed.from,
|
|
323
|
+
to,
|
|
324
|
+
position,
|
|
325
|
+
use: "read",
|
|
326
|
+
resolution: "contextual",
|
|
327
|
+
test: testFiles.has(position.path),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
found.push(...handedOver());
|
|
334
|
+
return found;
|
|
335
|
+
|
|
336
|
+
/** Whether a type is an object no file of the project declares, a platform object's among them. */
|
|
337
|
+
function outsideValue(type: Type): boolean {
|
|
338
|
+
if (!type.isObjectType()) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const symbol = queries.symbolOfType(type);
|
|
342
|
+
const declared = symbol === UNANSWERED ? undefined : symbol;
|
|
343
|
+
return declared?.declarations.every((handle) => !own.has(handle.path)) === true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Whether a type is an object type a file of the project declares. */
|
|
347
|
+
function ownObject(type: Type | undefined | typeof UNANSWERED): type is Type {
|
|
348
|
+
const known = type === UNANSWERED ? undefined : type;
|
|
349
|
+
if (!known?.isObjectType()) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const symbol = queries.symbolOfType(known);
|
|
353
|
+
const declared = symbol === UNANSWERED ? undefined : symbol;
|
|
354
|
+
return declared?.declarations.some((handle) => own.has(handle.path)) === true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The reads of every member of each target type an outside value is handed over as. */
|
|
358
|
+
function handedOver(): readonly Reference[] {
|
|
359
|
+
const operandTypes = queries.typesAt(asserted.map((one) => one.operand));
|
|
360
|
+
const fromCasts: HandedOver[] = asserted.filter((_one, at) => {
|
|
361
|
+
const type = operandTypes[at];
|
|
362
|
+
return type !== UNANSWERED && type !== undefined && outsideValue(type);
|
|
363
|
+
});
|
|
364
|
+
const newTypes = queries.typesAt(constructed.map((one) => one.call));
|
|
365
|
+
// A construct signature whose declared result is one of its type parameters names no
|
|
366
|
+
// type the target declares, so only a result the signature itself names is handed over.
|
|
367
|
+
const fromConstructors: HandedOver[] = constructed
|
|
368
|
+
.filter((one, at) => {
|
|
369
|
+
if (!ownObject(newTypes[at]) || standsFor(one)) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const signature = queries.resolvedSignature(one.call);
|
|
373
|
+
const result =
|
|
374
|
+
signature === UNANSWERED || signature === undefined
|
|
375
|
+
? undefined
|
|
376
|
+
: queries.returnTypeOf(signature);
|
|
377
|
+
return result !== UNANSWERED && result !== undefined && !result.isTypeParameter();
|
|
378
|
+
})
|
|
379
|
+
.map((one) => ({ file: one.file, from: one.from, node: one.call }));
|
|
380
|
+
const handed = [...fromCasts, ...fromConstructors];
|
|
381
|
+
const types = queries.typesAt(handed.map((one) => one.node));
|
|
382
|
+
const reads: Reference[] = [];
|
|
383
|
+
handed.forEach((one, at) => {
|
|
384
|
+
const type = types[at];
|
|
385
|
+
if (type === UNANSWERED || type === undefined) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const position = renderPosition(one.file, root, one.node.getStart());
|
|
389
|
+
const parts = queries.constituents(type);
|
|
390
|
+
for (const part of parts === UNANSWERED ? [type] : parts) {
|
|
391
|
+
if (!ownObject(part)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const properties = queries.propertiesOf(part);
|
|
395
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
396
|
+
for (const to of chains.declarationsOf(property)) {
|
|
397
|
+
reads.push({
|
|
398
|
+
from: one.from,
|
|
399
|
+
to,
|
|
400
|
+
position,
|
|
401
|
+
use: "read",
|
|
402
|
+
resolution: "contextual",
|
|
403
|
+
test: testFiles.has(position.path),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return reads;
|
|
410
|
+
}
|
|
411
|
+
}
|
package/src/value-reach.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The classes of the target a value of one type carries, and the members of the other
|
|
3
|
-
* object types the target writes that it carries, as a serializer walking the value
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* instance held only there is missed.
|
|
3
|
+
* object types the target writes that it carries, as a serializer walking the value reaches
|
|
4
|
+
* them: through constituents, array and tuple elements, the data members and index
|
|
5
|
+
* signatures of the types the target writes, and the values `stored` gives for a type where
|
|
6
|
+
* it erases them. The walk stops at `any`, `unknown`, a type parameter, a function and an
|
|
7
|
+
* interface or class of another program, so an instance held only there is missed.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import {
|
|
@@ -38,7 +37,7 @@ export interface Reached {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/** What each type's values carry, each type asked of the checker once. */
|
|
41
|
-
|
|
40
|
+
interface ValueReach {
|
|
42
41
|
reachedFrom(type: Type): Reached;
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -60,7 +59,11 @@ function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
|
|
|
60
59
|
* The walk over one project's types, against the classes of `held`, the same
|
|
61
60
|
* project's inventory.
|
|
62
61
|
*/
|
|
63
|
-
export function valueReach<Brand>(
|
|
62
|
+
export function valueReach<Brand>(
|
|
63
|
+
project: ProjectView<Brand>,
|
|
64
|
+
held: Inventory,
|
|
65
|
+
stored: (type: Type) => readonly Type[] = () => [],
|
|
66
|
+
): ValueReach {
|
|
64
67
|
const queries = project.queries;
|
|
65
68
|
const names = [...classNames(project, held)];
|
|
66
69
|
const declared = typesAt(
|
|
@@ -161,6 +164,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
161
164
|
found = must(queries.typeArguments(type));
|
|
162
165
|
}
|
|
163
166
|
}
|
|
167
|
+
found = [...found, ...stored(type)];
|
|
164
168
|
children.set(type.id, found);
|
|
165
169
|
return found;
|
|
166
170
|
};
|