@cplieger/deadset-ts 7.0.0-dev.2 → 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 +89 -44
- package/dist/src/augmentations.js +14 -12
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +5 -5
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +71 -14
- package/dist/src/convention-rows.js +6 -2
- package/dist/src/conventions.js +3 -1
- package/dist/src/dependencies.js +80 -26
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/findings/completion.js +46 -15
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/self-check.js +17 -4
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -125
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +25 -0
- 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 +12 -1
- package/dist/src/report.js +1 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -7
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +13 -4
- package/dist/src/suppress.js +54 -24
- 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/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +135 -52
- package/src/augmentations.ts +14 -11
- package/src/components.ts +103 -21
- package/src/configuration-files.ts +5 -4
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +81 -17
- package/src/convention-rows.ts +6 -2
- package/src/conventions.ts +10 -1
- package/src/dependencies.ts +107 -30
- package/src/element-access.ts +446 -0
- package/src/erased-stores.ts +413 -0
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +54 -20
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +1 -1
- package/src/findings/self-check.ts +18 -4
- package/src/findings-pass.ts +12 -5
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -145
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +32 -0
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +6 -21
- package/src/matrix.ts +34 -2
- package/src/query.ts +10 -0
- package/src/references.ts +23 -1
- package/src/report.ts +3 -1
- package/src/reporters/sarif.ts +4 -0
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -29
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +14 -4
- package/src/suppress.ts +63 -28
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +11 -7
- package/src/verbs/analyze.ts +15 -9
package/dist/src/session.js
CHANGED
|
@@ -227,12 +227,12 @@ function projectsOf(snapshot, built, written) {
|
|
|
227
227
|
/**
|
|
228
228
|
* Runs `visit` over every project the compiler configurations name, inside one never updated
|
|
229
229
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
230
|
-
* built as that program, or opened from the configuration it writes
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
230
|
+
* built as that program, or opened from the configuration it writes. One the snapshot holds
|
|
231
|
+
* nothing for ends the run under `"refuse"` and is named in the result under `"record"`.
|
|
232
|
+
* `first` runs over every opened project before the first visit and `last` after the last,
|
|
233
|
+
* both while the client answers; the snapshot and the client are then released.
|
|
234
234
|
*/
|
|
235
|
-
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
|
|
235
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined, first = () => undefined) {
|
|
236
236
|
try {
|
|
237
237
|
const built = [];
|
|
238
238
|
const params = [];
|
|
@@ -253,7 +253,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
253
253
|
try {
|
|
254
254
|
const opened = projectsOf(snapshot, built, written);
|
|
255
255
|
const log = new UnansweredLog();
|
|
256
|
-
const
|
|
256
|
+
const views = [];
|
|
257
257
|
const missing = [];
|
|
258
258
|
for (const configFile of configFiles) {
|
|
259
259
|
const project = opened.get(configFile);
|
|
@@ -264,8 +264,13 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
264
264
|
missing.push(configFile);
|
|
265
265
|
continue;
|
|
266
266
|
}
|
|
267
|
-
|
|
267
|
+
views.push(viewOf(project, configFile, engine, log));
|
|
268
268
|
}
|
|
269
|
+
first(views.map((view) => ({
|
|
270
|
+
configFile: view.configFile,
|
|
271
|
+
fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
|
|
272
|
+
})));
|
|
273
|
+
const projects = views.map((view) => visit(view));
|
|
269
274
|
last();
|
|
270
275
|
return {
|
|
271
276
|
projects,
|
|
@@ -27,6 +27,12 @@ const BASELINE_SHAPE = {
|
|
|
27
27
|
documentWant: "an object carrying the baseline array, and an optional description",
|
|
28
28
|
recordWant: "a row naming code, symbol, path and reason, each a string",
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* The reference scheme of this analyzer's language. A record of the other scheme is
|
|
32
|
+
* form-checked and then left to the analyzer of its language: it binds nothing here, counts
|
|
33
|
+
* in neither total and is reported under no code.
|
|
34
|
+
*/
|
|
35
|
+
const OWN_SCHEME = "ts://";
|
|
30
36
|
/** The closed key list of one entry or row. */
|
|
31
37
|
const RECORD_KEYS = new Set(["code", "symbol", "path", "reason"]);
|
|
32
38
|
const CODE_WANT = "DS followed by four digits";
|
|
@@ -80,6 +86,9 @@ function readRecord(value, site, byReference, shape) {
|
|
|
80
86
|
if (path !== undefined && path !== "" && !isEntryPath(path)) {
|
|
81
87
|
return refuse(JSON.stringify(path), PATH_WANT);
|
|
82
88
|
}
|
|
89
|
+
if (!symbol.startsWith(OWN_SCHEME)) {
|
|
90
|
+
return { records: [], refusals: [] };
|
|
91
|
+
}
|
|
83
92
|
const written = {
|
|
84
93
|
code,
|
|
85
94
|
symbol,
|
|
@@ -196,12 +205,55 @@ export function rowReason(provenance) {
|
|
|
196
205
|
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
197
206
|
}
|
|
198
207
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
|
|
208
|
+
* The rows of the other language's scheme that the baseline document `text` holds, in
|
|
209
|
+
* document order and as written, which a write keeps. A document that is not a baseline,
|
|
210
|
+
* or a row in a shape no record takes, is refused rather than dropped.
|
|
211
|
+
*/
|
|
212
|
+
export function otherSchemeRows(text) {
|
|
213
|
+
const refuse = (detail) => {
|
|
214
|
+
throw new Error(`${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`);
|
|
215
|
+
};
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(text);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
222
|
+
}
|
|
223
|
+
const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
|
|
224
|
+
if (!Array.isArray(held)) {
|
|
225
|
+
return refuse(BASELINE_SHAPE.documentWant);
|
|
226
|
+
}
|
|
227
|
+
return held.flatMap((value) => {
|
|
228
|
+
try {
|
|
229
|
+
readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
233
|
+
}
|
|
234
|
+
// readRecord has checked every member is a string and the code and symbol are present.
|
|
235
|
+
const { code, symbol, path, reason } = value;
|
|
236
|
+
if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
code,
|
|
242
|
+
symbol,
|
|
243
|
+
...(path === undefined ? {} : { path }),
|
|
244
|
+
...(reason === undefined ? {} : { reason }),
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* A baseline holding one row per finding, in the order given, which is the report's, after
|
|
251
|
+
* the other language's rows `kept`, so two analyzers writing one document in either order
|
|
252
|
+
* write the same bytes. Every recorded row carries the provenance as its reason. A finding
|
|
253
|
+
* missing a value a row needs, and an identity missing its name or version, are refused
|
|
254
|
+
* rather than written, because the grammar requires a reason on every row.
|
|
203
255
|
*/
|
|
204
|
-
export function writeBaseline(findings, provenance) {
|
|
256
|
+
export function writeBaseline(findings, provenance, kept = []) {
|
|
205
257
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
206
258
|
throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
|
|
207
259
|
}
|
|
@@ -212,5 +264,5 @@ export function writeBaseline(findings, provenance) {
|
|
|
212
264
|
}
|
|
213
265
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
214
266
|
});
|
|
215
|
-
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
267
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
|
|
216
268
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The inline directive: `deadset:ignore`, the first token of a `//` line comment, naming
|
|
3
3
|
* one or more codes and a reason, and binding to every declaration that begins on the line
|
|
4
|
-
* below it.
|
|
4
|
+
* below it, or on its own line where it trails a token there.
|
|
5
5
|
*/
|
|
6
6
|
import { SyntaxKind } from "@typescript/native/unstable/ast";
|
|
7
7
|
import { nodeKey } from "./inventory.js";
|
|
@@ -170,6 +170,14 @@ function declarationLines(file, held, symbols) {
|
|
|
170
170
|
file.forEachChild(visit);
|
|
171
171
|
return below;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the comment at `offset` follows a token on its own line, so it binds the
|
|
175
|
+
* declarations that begin on that line rather than on the next.
|
|
176
|
+
*/
|
|
177
|
+
function trails(file, offset) {
|
|
178
|
+
const { line } = file.getLineAndCharacterOfPosition(offset);
|
|
179
|
+
return tokenStart(file.text, file.getPositionOfLineAndCharacter(line, 0)) < offset;
|
|
180
|
+
}
|
|
173
181
|
/**
|
|
174
182
|
* Every comment in the namespace one project's own files hold, each with the declarations
|
|
175
183
|
* it binds, read while the project's view is open.
|
|
@@ -187,19 +195,20 @@ export function inlineDirectives(project, held, targetRoot) {
|
|
|
187
195
|
const below = declarationLines(file, held, symbols);
|
|
188
196
|
for (const comment of comments) {
|
|
189
197
|
const site = renderPosition(file, targetRoot, comment.offset);
|
|
198
|
+
const target = trails(file, comment.offset) ? site.line : site.line + 1;
|
|
190
199
|
found.push({
|
|
191
200
|
site,
|
|
192
201
|
text: comment.text,
|
|
193
202
|
directive: classifyComment(comment.text),
|
|
194
|
-
bound: [...(below.get(
|
|
203
|
+
bound: [...(below.get(target) ?? [])],
|
|
195
204
|
});
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
return found;
|
|
199
208
|
}
|
|
200
209
|
/**
|
|
201
|
-
* The inline directives of a run's projects, each bound to the declarations
|
|
202
|
-
*
|
|
210
|
+
* The inline directives of a run's projects, each bound to the declarations the placement
|
|
211
|
+
* rule gives it, one directive binding the union of what each project holding its file
|
|
203
212
|
* declares there. A directive that lacks only its reason is one {@link Refusal} per code;
|
|
204
213
|
* any other malformed comment in the namespace ends the read with a
|
|
205
214
|
* {@link SuppressionError} naming the first one written.
|
package/dist/src/suppress.js
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
|
import { ConfigError } from "./config.js";
|
|
9
10
|
import { positionKey } from "./position.js";
|
|
@@ -68,28 +69,48 @@ export function isSymbolRef(value) {
|
|
|
68
69
|
export function blankReason(reason) {
|
|
69
70
|
return reason === undefined || /^[ \t\r\n]*$/u.test(reason);
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The subject kinds that are a dependency row, each with the code that reports one. An
|
|
74
|
+
* ignore entry or a baseline row naming the row's reference and file withholds its finding
|
|
75
|
+
* and marks nothing, and an inline directive never binds one.
|
|
76
|
+
*/
|
|
77
|
+
const DEPENDENCY_SUBJECTS = new Map([
|
|
78
|
+
["dependency", ["DS1601"]],
|
|
79
|
+
["module-directive", ["DS1605"]],
|
|
80
|
+
]);
|
|
71
81
|
/**
|
|
72
82
|
* The subject kinds that are a row of a document rather than a declaration of the program,
|
|
73
|
-
* each with the codes that report one.
|
|
74
|
-
*
|
|
83
|
+
* each with the codes that report one. Apart from a dependency row, nothing withholds a
|
|
84
|
+
* finding about one.
|
|
75
85
|
*/
|
|
76
86
|
const ROW_SUBJECTS = new Map([
|
|
77
87
|
["file", ["DS1501", "DS1502"]],
|
|
78
|
-
|
|
79
|
-
["module-directive", ["DS1605"]],
|
|
88
|
+
...DEPENDENCY_SUBJECTS,
|
|
80
89
|
["root", ["DS1704"]],
|
|
81
90
|
["configured-declaration", ["DS1706"]],
|
|
82
91
|
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
83
92
|
["edge", ["DS1705"]],
|
|
84
93
|
]);
|
|
85
|
-
/** Whether a finding's subject is a row of a document
|
|
94
|
+
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
86
95
|
export function isRowSubject(kind) {
|
|
87
96
|
return ROW_SUBJECTS.has(kind);
|
|
88
97
|
}
|
|
89
|
-
/** Whether a code reports a row of a document
|
|
98
|
+
/** Whether a code reports a row of a document. */
|
|
90
99
|
export function isRowCode(code) {
|
|
91
100
|
return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
92
101
|
}
|
|
102
|
+
/** Whether a finding's subject is a dependency row. */
|
|
103
|
+
export function isDependencySubject(kind) {
|
|
104
|
+
return DEPENDENCY_SUBJECTS.has(kind);
|
|
105
|
+
}
|
|
106
|
+
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
107
|
+
export function isDependencyCode(code) {
|
|
108
|
+
return [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
109
|
+
}
|
|
110
|
+
/** Whether a code reports a row of a document that no record of any mechanism can name. */
|
|
111
|
+
export function isUnsuppressibleCode(code) {
|
|
112
|
+
return isRowCode(code) && !isDependencyCode(code);
|
|
113
|
+
}
|
|
93
114
|
/**
|
|
94
115
|
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
95
116
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
@@ -124,10 +145,19 @@ function claimKey(finding) {
|
|
|
124
145
|
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
125
146
|
* the declaration at the finding's position, or it names the finding's reference and the
|
|
126
147
|
* file the finding is reported in, each exactly. The second is how an entry copied from a
|
|
127
|
-
* finding about a part of a declaration reaches the part
|
|
148
|
+
* finding about a part of a declaration reaches the part, and the only way any record
|
|
149
|
+
* reaches a dependency row.
|
|
128
150
|
*/
|
|
129
151
|
export function pairs(record, finding) {
|
|
130
|
-
if (record.code !== finding.code
|
|
152
|
+
if (record.code !== finding.code) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
|
|
156
|
+
return (record.mechanism !== "inline" &&
|
|
157
|
+
record.symbol === finding.symbol.ref &&
|
|
158
|
+
record.path === finding.position.path);
|
|
159
|
+
}
|
|
160
|
+
if (ROW_SUBJECTS.has(finding.symbol.kind)) {
|
|
131
161
|
return false;
|
|
132
162
|
}
|
|
133
163
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
@@ -137,31 +167,31 @@ export function pairs(record, finding) {
|
|
|
137
167
|
}
|
|
138
168
|
/**
|
|
139
169
|
* Decides every record against `would`, the findings the run would produce were no record
|
|
140
|
-
* there: swept with no mark
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
170
|
+
* there: swept with no mark for a liveness kind, and as swept otherwise. The first record
|
|
171
|
+
* in reading order claims a finding, so a second naming it is stale; a part-kind record
|
|
172
|
+
* claims every finding of its code about a part of its declaration. A record is dormant
|
|
173
|
+
* when the dials withhold every finding it claims, read from those findings alone, or when
|
|
174
|
+
* it claims none and its code's severity is `allow`.
|
|
145
175
|
*/
|
|
146
176
|
export function ledgerOf(records, would, dials) {
|
|
147
177
|
const taken = new Set();
|
|
148
178
|
const verdicts = [];
|
|
149
179
|
for (const record of records) {
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
const open = (finding) => !taken.has(claimKey(finding)) && pairs(record, finding);
|
|
181
|
+
const first = would.find(open);
|
|
182
|
+
const claimed = first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
|
|
183
|
+
if (claimed.length === 0) {
|
|
184
|
+
verdicts.push(record.bound !== "" && dials.allows(record.code) ? "dormant" : "stale");
|
|
152
185
|
continue;
|
|
153
186
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
verdicts.push("stale");
|
|
157
|
-
continue;
|
|
187
|
+
for (const finding of claimed) {
|
|
188
|
+
taken.add(claimKey(finding));
|
|
158
189
|
}
|
|
159
|
-
|
|
160
|
-
verdicts.push(dials.withholds(claimed) ? "dormant" : "in-effect");
|
|
190
|
+
verdicts.push(claimed.every((finding) => dials.withholds(finding)) ? "dormant" : "in-effect");
|
|
161
191
|
}
|
|
162
192
|
return {
|
|
163
193
|
verdicts,
|
|
164
|
-
withheld: (finding) => taken.has(claimKey(finding))
|
|
194
|
+
withheld: (finding) => taken.has(claimKey(finding)),
|
|
165
195
|
};
|
|
166
196
|
}
|
|
167
197
|
/** The suppression counts of one run's records and the ledger over them. */
|
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
import { isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isHeritageClause, isIdentifier, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isPropertyDeclaration, isSatisfiesExpression, isTypeReferenceNode, isVariableDeclaration, } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
11
|
+
import { nodeKey } from "./inventory.js";
|
|
12
|
+
import { renderPosition } from "./position.js";
|
|
13
|
+
import { UNANSWERED } from "./query.js";
|
|
14
|
+
/** The value an `as` expression asserts a type of, through every `as` and parenthesis around it. */
|
|
15
|
+
function castOperand(node) {
|
|
16
|
+
let at = node;
|
|
17
|
+
while (isAsExpression(at) || isParenthesizedExpression(at)) {
|
|
18
|
+
at = at.expression;
|
|
19
|
+
}
|
|
20
|
+
return at;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a declaration node carries a body of statements or an expression. */
|
|
23
|
+
function hasBody(node) {
|
|
24
|
+
return node.body !== undefined;
|
|
25
|
+
}
|
|
26
|
+
function isFunctionLike(node) {
|
|
27
|
+
return (node !== undefined &&
|
|
28
|
+
(isArrowFunction(node) ||
|
|
29
|
+
isFunctionExpression(node) ||
|
|
30
|
+
isFunctionDeclaration(node) ||
|
|
31
|
+
isClassExpression(node)));
|
|
32
|
+
}
|
|
33
|
+
/** The name a declaration is declared under, where it has one an identifier spells. */
|
|
34
|
+
function nameOf(node) {
|
|
35
|
+
const name = node.name;
|
|
36
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether an overload signature has its implementation beside it: a declaration of the same
|
|
40
|
+
* name and kind, in the same statement list or class body, that carries a body.
|
|
41
|
+
*/
|
|
42
|
+
function overloadImplemented(node) {
|
|
43
|
+
if (!isFunctionDeclaration(node) && !isMethodDeclaration(node)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const name = nameOf(node);
|
|
47
|
+
let implemented = false;
|
|
48
|
+
node.parent.forEachChild((sibling) => {
|
|
49
|
+
implemented ||= sibling.kind === node.kind && nameOf(sibling) === name && hasBody(sibling);
|
|
50
|
+
});
|
|
51
|
+
return implemented;
|
|
52
|
+
}
|
|
53
|
+
/** Every object literal one argument holds, the argument itself included, through nested property values. */
|
|
54
|
+
function literalsOf(argument) {
|
|
55
|
+
if (!isObjectLiteralExpression(argument)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
argument,
|
|
60
|
+
...argument.properties.flatMap((property) => isPropertyAssignment(property) ? literalsOf(property.initializer) : []),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The reads the object-literal arguments of one project's own files make of the members of
|
|
65
|
+
* the types they are checked against, where the callee has no implementation in the target.
|
|
66
|
+
*/
|
|
67
|
+
export function unimplementedCalleeReads(project, held, chains, root, testFiles) {
|
|
68
|
+
const queries = project.queries;
|
|
69
|
+
const own = project.ownPaths();
|
|
70
|
+
const passing = [];
|
|
71
|
+
const implementing = [];
|
|
72
|
+
const asserted = [];
|
|
73
|
+
const constructed = [];
|
|
74
|
+
const handing = [];
|
|
75
|
+
for (const file of project.ownSourceFiles()) {
|
|
76
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
77
|
+
if (fileId === undefined) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const visit = (node, from) => {
|
|
81
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
82
|
+
if ((isCallExpression(node) || isNewExpression(node)) &&
|
|
83
|
+
(node.arguments ?? []).some((argument) => isObjectLiteralExpression(argument))) {
|
|
84
|
+
passing.push({ file, from: holder, call: node });
|
|
85
|
+
}
|
|
86
|
+
if (isNewExpression(node)) {
|
|
87
|
+
constructed.push({ file, from: holder, call: node });
|
|
88
|
+
}
|
|
89
|
+
if ((isCallExpression(node) || isNewExpression(node)) &&
|
|
90
|
+
(node.arguments ?? []).some((argument) => isFunctionLike(argument) || isIdentifier(argument))) {
|
|
91
|
+
handing.push(node);
|
|
92
|
+
}
|
|
93
|
+
if (isAsExpression(node) && !isAsExpression(node.parent)) {
|
|
94
|
+
asserted.push({ file, from: holder, node, operand: castOperand(node) });
|
|
95
|
+
}
|
|
96
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
97
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
98
|
+
if (isHeritageClause(clause)) {
|
|
99
|
+
implementing.push(...clause.types.map((one) => isTypeReferenceNode(one) ? one.typeName : one.expression));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// An object literal typed as an interface implements that interface's methods.
|
|
104
|
+
if (isVariableDeclaration(node) &&
|
|
105
|
+
node.type !== undefined &&
|
|
106
|
+
isTypeReferenceNode(node.type) &&
|
|
107
|
+
node.initializer !== undefined &&
|
|
108
|
+
isObjectLiteralExpression(node.initializer)) {
|
|
109
|
+
implementing.push(node.type.typeName);
|
|
110
|
+
}
|
|
111
|
+
if ((isSatisfiesExpression(node) || isAsExpression(node)) &&
|
|
112
|
+
isObjectLiteralExpression(node.expression) &&
|
|
113
|
+
isTypeReferenceNode(node.type)) {
|
|
114
|
+
implementing.push(node.type.typeName);
|
|
115
|
+
}
|
|
116
|
+
node.forEachChild((child) => {
|
|
117
|
+
visit(child, holder);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
file.forEachChild((child) => {
|
|
121
|
+
visit(child, fileId);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (passing.length === 0 && asserted.length === 0 && constructed.length === 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const resolved = (symbol) => {
|
|
128
|
+
if (symbol === UNANSWERED || symbol === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
132
|
+
return symbol;
|
|
133
|
+
}
|
|
134
|
+
const target = queries.aliased(symbol);
|
|
135
|
+
return target === UNANSWERED ? undefined : target;
|
|
136
|
+
};
|
|
137
|
+
const declarationsOf = (symbol) => (symbol?.declarations ?? []).flatMap((handle) => {
|
|
138
|
+
const node = project.declarationAt(handle)?.node;
|
|
139
|
+
return node === undefined ? [] : [node];
|
|
140
|
+
});
|
|
141
|
+
const implemented = new Set(queries.symbolsAt(implementing).flatMap((symbol) => declarationsOf(resolved(symbol))));
|
|
142
|
+
/** Whether a declaration is a function or a class of the target, or a variable or property it initializes. */
|
|
143
|
+
const ownFunction = (node) => own.has(node.getSourceFile().fileName) &&
|
|
144
|
+
(isFunctionLike(node) ||
|
|
145
|
+
((isVariableDeclaration(node) || isPropertyAssignment(node) || isPropertyDeclaration(node)) &&
|
|
146
|
+
isFunctionLike(node.initializer)));
|
|
147
|
+
/**
|
|
148
|
+
* Whether a function or a class of the target reaches one parameter of a function of the
|
|
149
|
+
* target, passed for it by a call that names the function. A call through another name
|
|
150
|
+
* is not followed, so its callee keeps no implementation and its literal reads more.
|
|
151
|
+
*/
|
|
152
|
+
const reachedParameters = new Map();
|
|
153
|
+
const parameterReached = (parameter) => {
|
|
154
|
+
const known = reachedParameters.get(parameter);
|
|
155
|
+
if (known !== undefined) {
|
|
156
|
+
return known;
|
|
157
|
+
}
|
|
158
|
+
const fn = parameter.parent;
|
|
159
|
+
const fnName = nameOf(fn) ?? (isVariableDeclaration(fn.parent) ? nameOf(fn.parent) : undefined);
|
|
160
|
+
const at = fn.parameters?.indexOf(parameter) ?? -1;
|
|
161
|
+
const passed = handing.flatMap((call) => {
|
|
162
|
+
const callee = call.expression;
|
|
163
|
+
const name = isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
164
|
+
if (fnName === undefined || !isIdentifier(name) || name.text !== fnName) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const signature = queries.resolvedSignature(call);
|
|
168
|
+
const handle = signature === UNANSWERED || signature === undefined ? undefined : signature.declaration;
|
|
169
|
+
const argument = handle === undefined || project.declarationAt(handle)?.node !== fn
|
|
170
|
+
? undefined
|
|
171
|
+
: (call.arguments ?? [])[at];
|
|
172
|
+
return argument === undefined || at < 0 ? [] : [argument];
|
|
173
|
+
});
|
|
174
|
+
const named = passed.filter((argument) => isIdentifier(argument));
|
|
175
|
+
const reached = passed.some((argument) => isFunctionLike(argument)) ||
|
|
176
|
+
queries
|
|
177
|
+
.symbolsAt(named)
|
|
178
|
+
.some((symbol) => declarationsOf(resolved(symbol)).some((node) => ownFunction(node)));
|
|
179
|
+
reachedParameters.set(parameter, reached);
|
|
180
|
+
return reached;
|
|
181
|
+
};
|
|
182
|
+
/** Whether some function, class or object literal of the target stands for a signature. */
|
|
183
|
+
const standsFor = (passed) => {
|
|
184
|
+
const signature = queries.resolvedSignature(passed.call);
|
|
185
|
+
const handle = signature === UNANSWERED || signature === undefined ? undefined : signature.declaration;
|
|
186
|
+
const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
187
|
+
if (declaration === undefined) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (hasBody(declaration) || overloadImplemented(declaration)) {
|
|
191
|
+
return own.has(handle?.path ?? "");
|
|
192
|
+
}
|
|
193
|
+
const holder = declaration.parent;
|
|
194
|
+
if (holder !== undefined && implemented.has(holder)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
const callee = passed.call.expression;
|
|
198
|
+
const name = isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
199
|
+
const [named] = queries.symbolsAt([name]);
|
|
200
|
+
return declarationsOf(resolved(named)).some((node) => isParameterDeclaration(node)
|
|
201
|
+
? own.has(node.getSourceFile().fileName) && parameterReached(node)
|
|
202
|
+
: ownFunction(node) && !isFunctionLike(node));
|
|
203
|
+
};
|
|
204
|
+
const literals = [];
|
|
205
|
+
for (const passed of passing) {
|
|
206
|
+
if (standsFor(passed)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
for (const argument of passed.call.arguments ?? []) {
|
|
210
|
+
literals.push(...literalsOf(argument).map((literal) => ({ passed, literal })));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const contexts = queries.contextualTypes(literals.map((one) => one.literal));
|
|
214
|
+
const found = [];
|
|
215
|
+
literals.forEach(({ passed, literal }, at) => {
|
|
216
|
+
const context = contexts[at];
|
|
217
|
+
if (context === UNANSWERED || context === undefined) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const parts = queries.constituents(context);
|
|
221
|
+
const position = renderPosition(passed.file, root, literal.getStart());
|
|
222
|
+
for (const part of parts === UNANSWERED ? [context] : parts) {
|
|
223
|
+
const properties = queries.propertiesOf(part);
|
|
224
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
225
|
+
for (const to of chains.declarationsOf(property)) {
|
|
226
|
+
found.push({
|
|
227
|
+
from: passed.from,
|
|
228
|
+
to,
|
|
229
|
+
position,
|
|
230
|
+
use: "read",
|
|
231
|
+
resolution: "contextual",
|
|
232
|
+
test: testFiles.has(position.path),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
found.push(...handedOver());
|
|
239
|
+
return found;
|
|
240
|
+
/** Whether a type is an object no file of the project declares, a platform object's among them. */
|
|
241
|
+
function outsideValue(type) {
|
|
242
|
+
if (!type.isObjectType()) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const symbol = queries.symbolOfType(type);
|
|
246
|
+
const declared = symbol === UNANSWERED ? undefined : symbol;
|
|
247
|
+
return declared?.declarations.every((handle) => !own.has(handle.path)) === true;
|
|
248
|
+
}
|
|
249
|
+
/** Whether a type is an object type a file of the project declares. */
|
|
250
|
+
function ownObject(type) {
|
|
251
|
+
const known = type === UNANSWERED ? undefined : type;
|
|
252
|
+
if (!known?.isObjectType()) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const symbol = queries.symbolOfType(known);
|
|
256
|
+
const declared = symbol === UNANSWERED ? undefined : symbol;
|
|
257
|
+
return declared?.declarations.some((handle) => own.has(handle.path)) === true;
|
|
258
|
+
}
|
|
259
|
+
/** The reads of every member of each target type an outside value is handed over as. */
|
|
260
|
+
function handedOver() {
|
|
261
|
+
const operandTypes = queries.typesAt(asserted.map((one) => one.operand));
|
|
262
|
+
const fromCasts = asserted.filter((_one, at) => {
|
|
263
|
+
const type = operandTypes[at];
|
|
264
|
+
return type !== UNANSWERED && type !== undefined && outsideValue(type);
|
|
265
|
+
});
|
|
266
|
+
const newTypes = queries.typesAt(constructed.map((one) => one.call));
|
|
267
|
+
// A construct signature whose declared result is one of its type parameters names no
|
|
268
|
+
// type the target declares, so only a result the signature itself names is handed over.
|
|
269
|
+
const fromConstructors = constructed
|
|
270
|
+
.filter((one, at) => {
|
|
271
|
+
if (!ownObject(newTypes[at]) || standsFor(one)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const signature = queries.resolvedSignature(one.call);
|
|
275
|
+
const result = signature === UNANSWERED || signature === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: queries.returnTypeOf(signature);
|
|
278
|
+
return result !== UNANSWERED && result !== undefined && !result.isTypeParameter();
|
|
279
|
+
})
|
|
280
|
+
.map((one) => ({ file: one.file, from: one.from, node: one.call }));
|
|
281
|
+
const handed = [...fromCasts, ...fromConstructors];
|
|
282
|
+
const types = queries.typesAt(handed.map((one) => one.node));
|
|
283
|
+
const reads = [];
|
|
284
|
+
handed.forEach((one, at) => {
|
|
285
|
+
const type = types[at];
|
|
286
|
+
if (type === UNANSWERED || type === undefined) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const position = renderPosition(one.file, root, one.node.getStart());
|
|
290
|
+
const parts = queries.constituents(type);
|
|
291
|
+
for (const part of parts === UNANSWERED ? [type] : parts) {
|
|
292
|
+
if (!ownObject(part)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const properties = queries.propertiesOf(part);
|
|
296
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
297
|
+
for (const to of chains.declarationsOf(property)) {
|
|
298
|
+
reads.push({
|
|
299
|
+
from: one.from,
|
|
300
|
+
to,
|
|
301
|
+
position,
|
|
302
|
+
use: "read",
|
|
303
|
+
resolution: "contextual",
|
|
304
|
+
test: testFiles.has(position.path),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return reads;
|
|
311
|
+
}
|
|
312
|
+
}
|
package/dist/src/value-reach.js
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
|
import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
11
10
|
import { classNames, typesAt } from "./class-members.js";
|
|
@@ -28,7 +27,7 @@ function arrayTargets(project) {
|
|
|
28
27
|
* The walk over one project's types, against the classes of `held`, the same
|
|
29
28
|
* project's inventory.
|
|
30
29
|
*/
|
|
31
|
-
export function valueReach(project, held) {
|
|
30
|
+
export function valueReach(project, held, stored = () => []) {
|
|
32
31
|
const queries = project.queries;
|
|
33
32
|
const names = [...classNames(project, held)];
|
|
34
33
|
const declared = typesAt(project, names.map(([, node]) => node));
|
|
@@ -110,6 +109,7 @@ export function valueReach(project, held) {
|
|
|
110
109
|
found = must(queries.typeArguments(type));
|
|
111
110
|
}
|
|
112
111
|
}
|
|
112
|
+
found = [...found, ...stored(type)];
|
|
113
113
|
children.set(type.id, found);
|
|
114
114
|
return found;
|
|
115
115
|
};
|