@cplieger/deadset-ts 3.12.1 → 4.0.0
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/README.md +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/edges.js +4 -1
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/dist/src/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
|
-
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
2
|
import { DiscoveryError } from "./discover.js";
|
|
3
|
+
import { queriesOf, UnansweredLog, } from "./query.js";
|
|
4
4
|
/**
|
|
5
5
|
* Opens the compiler client the analysis reads through.
|
|
6
6
|
*
|
|
@@ -13,7 +13,9 @@ export function openEngine(options) {
|
|
|
13
13
|
let closed = false;
|
|
14
14
|
return {
|
|
15
15
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
16
|
-
|
|
16
|
+
createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
|
|
17
|
+
batch: (questions) => api.batch(...questions),
|
|
18
|
+
ask: (_accessor, _locations, question) => question(),
|
|
17
19
|
getTimingInfo: () => api.getTimingInfo(),
|
|
18
20
|
close: () => {
|
|
19
21
|
if (closed) {
|
|
@@ -24,46 +26,53 @@ export function openEngine(options) {
|
|
|
24
26
|
},
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
|
-
function viewOf(project) {
|
|
29
|
+
function viewOf(project, configFile, engine, log) {
|
|
30
|
+
const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
|
|
28
31
|
// The snapshot is never updated, so a project's own file set is the same answer
|
|
29
32
|
// every time it is asked for. It is read once because the question is not free:
|
|
30
33
|
// deciding it consults the metadata of every file the program holds, the compiler's
|
|
31
34
|
// own library files included, and a run asks for the set once per pass.
|
|
32
35
|
let ownFiles;
|
|
36
|
+
let ownKeys;
|
|
37
|
+
const ownSourceFiles = () => {
|
|
38
|
+
ownFiles ??= project.program
|
|
39
|
+
.getSourceFileNames()
|
|
40
|
+
// The metadata decides which files belong to the target, and it is read by
|
|
41
|
+
// name. Fetching each file first and filtering afterwards costs one round
|
|
42
|
+
// trip per library file the answer then discards, and a program holds far
|
|
43
|
+
// more library files than target files.
|
|
44
|
+
.filter((name) => {
|
|
45
|
+
const metadata = project.program.getSourceFileMetadata(name);
|
|
46
|
+
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
47
|
+
})
|
|
48
|
+
.map((name) => project.program.getSourceFile(name))
|
|
49
|
+
.filter((file) => file !== undefined);
|
|
50
|
+
return ownFiles;
|
|
51
|
+
};
|
|
33
52
|
return {
|
|
34
|
-
configFile
|
|
53
|
+
configFile,
|
|
35
54
|
program: project.program,
|
|
36
|
-
|
|
37
|
-
ownSourceFiles
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// name. Fetching each file first and filtering afterwards costs one round
|
|
42
|
-
// trip per library file the answer then discards, and a program holds far
|
|
43
|
-
// more library files than target files.
|
|
44
|
-
.filter((name) => {
|
|
45
|
-
const metadata = project.program.getSourceFileMetadata(name);
|
|
46
|
-
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
47
|
-
})
|
|
48
|
-
.map((name) => project.program.getSourceFile(name))
|
|
49
|
-
.filter((file) => file !== undefined);
|
|
50
|
-
return ownFiles;
|
|
55
|
+
queries,
|
|
56
|
+
ownSourceFiles,
|
|
57
|
+
ownPaths: () => {
|
|
58
|
+
ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
59
|
+
return ownKeys;
|
|
51
60
|
},
|
|
52
61
|
handle: (node) => ({ node }),
|
|
53
|
-
symbolsAt: (handles) =>
|
|
54
|
-
resolvedSymbolAt: ({ node }) =>
|
|
55
|
-
shorthandValueAt: ({ node }) =>
|
|
62
|
+
symbolsAt: (handles, cap) => queries.symbolsAt(handles.map(({ node }) => node), cap),
|
|
63
|
+
resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
|
|
64
|
+
shorthandValueAt: ({ node }) => queries.shorthandValue(node),
|
|
56
65
|
aliasStepOf: (symbol) => {
|
|
57
66
|
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
58
67
|
// fails an assertion and the session ends.
|
|
59
68
|
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
60
69
|
return undefined;
|
|
61
70
|
}
|
|
62
|
-
const held =
|
|
71
|
+
const held = queries.immediateAliased(symbol);
|
|
63
72
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
64
73
|
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
65
74
|
// nothing at all.
|
|
66
|
-
return held ===
|
|
75
|
+
return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
|
|
67
76
|
},
|
|
68
77
|
declarationAt: (handle) => {
|
|
69
78
|
const node = handle.resolve(project);
|
|
@@ -78,11 +87,17 @@ function viewOf(project) {
|
|
|
78
87
|
*/
|
|
79
88
|
export function narrowedTo(project, keep) {
|
|
80
89
|
let kept;
|
|
90
|
+
let keys;
|
|
91
|
+
const ownSourceFiles = () => {
|
|
92
|
+
kept ??= project.ownSourceFiles().filter(keep);
|
|
93
|
+
return kept;
|
|
94
|
+
};
|
|
81
95
|
return {
|
|
82
96
|
...project,
|
|
83
|
-
ownSourceFiles
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
ownSourceFiles,
|
|
98
|
+
ownPaths: () => {
|
|
99
|
+
keys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
100
|
+
return keys;
|
|
86
101
|
},
|
|
87
102
|
};
|
|
88
103
|
}
|
|
@@ -93,6 +108,14 @@ export function diagnosticsOf(project) {
|
|
|
93
108
|
syntactic: project.program.getSyntacticDiagnostics(),
|
|
94
109
|
semantic: project.program.getSemanticDiagnostics(),
|
|
95
110
|
configParsing: project.program.getConfigFileParsingDiagnostics(),
|
|
111
|
+
locate: (fileName, offset) => {
|
|
112
|
+
const file = project.program.getSourceFile(fileName);
|
|
113
|
+
if (file === undefined || offset < 0 || offset > file.text.length) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const { line, character } = file.getLineAndCharacterOfPosition(offset);
|
|
117
|
+
return { line: line + 1, column: character + 1 };
|
|
118
|
+
},
|
|
96
119
|
};
|
|
97
120
|
}
|
|
98
121
|
/**
|
|
@@ -103,24 +126,41 @@ export function diagnosticsOf(project) {
|
|
|
103
126
|
* with its own program and checker; it is never updated, because the analysis
|
|
104
127
|
* reads and the tree does not change under it. The order the snapshot lists its
|
|
105
128
|
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
106
|
-
* instead
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
129
|
+
* instead. A configuration the snapshot opened no project for ends the run under
|
|
130
|
+
* `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
|
|
131
|
+
* the last project has been visited, and the client is released after it, whether the
|
|
132
|
+
* visit completed or threw.
|
|
110
133
|
*/
|
|
111
|
-
export function runSession(engine, configFiles, visit) {
|
|
134
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse") {
|
|
112
135
|
try {
|
|
113
|
-
const snapshot = engine.
|
|
136
|
+
const snapshot = engine.createSnapshot(configFiles);
|
|
114
137
|
try {
|
|
115
|
-
const opened = new Map(
|
|
116
|
-
const
|
|
138
|
+
const opened = new Map();
|
|
139
|
+
for (const project of snapshot.getProjects()) {
|
|
140
|
+
if (project.configFileName !== undefined) {
|
|
141
|
+
opened.set(project.configFileName, project);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const log = new UnansweredLog();
|
|
145
|
+
const projects = [];
|
|
146
|
+
const missing = [];
|
|
147
|
+
for (const configFile of configFiles) {
|
|
117
148
|
const project = opened.get(configFile);
|
|
118
149
|
if (project === undefined) {
|
|
119
|
-
|
|
150
|
+
if (unopened === "refuse") {
|
|
151
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
152
|
+
}
|
|
153
|
+
missing.push(configFile);
|
|
154
|
+
continue;
|
|
120
155
|
}
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
156
|
+
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
projects,
|
|
160
|
+
unopened: missing,
|
|
161
|
+
timing: engine.getTimingInfo(),
|
|
162
|
+
unanswered: log.questions,
|
|
163
|
+
};
|
|
124
164
|
}
|
|
125
165
|
finally {
|
|
126
166
|
snapshot.dispose();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a declaration of the inventory declares, where it declares one: the node
|
|
3
|
+
* whose parameters, body and result are the declaration's own parts.
|
|
4
|
+
*/
|
|
5
|
+
import { isArrowFunction, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isMethodDeclaration, isPropertyDeclaration, isVariableDeclaration, } from "@typescript/native/unstable/ast";
|
|
6
|
+
/** Whether one node is a function value written in place: an arrow or a function expression. */
|
|
7
|
+
function isFunctionValue(node) {
|
|
8
|
+
return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The function one declaration declares: a function declaration and a class method with
|
|
12
|
+
* a body are their own; a variable and a class property whose initializer is an arrow or
|
|
13
|
+
* a function expression declare that function. Any other declaration declares none, an
|
|
14
|
+
* overload signature and an abstract or ambient method among them, because no body is
|
|
15
|
+
* there to read its parts.
|
|
16
|
+
*/
|
|
17
|
+
export function signatureOf(node) {
|
|
18
|
+
if (isFunctionDeclaration(node)) {
|
|
19
|
+
return node.body === undefined ? undefined : node;
|
|
20
|
+
}
|
|
21
|
+
if (isMethodDeclaration(node)) {
|
|
22
|
+
const owner = node.parent;
|
|
23
|
+
return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
24
|
+
? node
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
if ((isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
28
|
+
isFunctionValue(node.initializer)) {
|
|
29
|
+
return node.initializer;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* one record shape, so one decoder and one matcher serve both, and a row moves into the
|
|
5
5
|
* ignore file by gaining a maintainer's reason.
|
|
6
6
|
*/
|
|
7
|
+
import { jsonChunks } from "./json-chunks.js";
|
|
7
8
|
import { joinPath } from "./paths.js";
|
|
8
9
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
9
10
|
import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
|
|
@@ -105,6 +106,14 @@ function readRecord(value, site, byReference, shape) {
|
|
|
105
106
|
}
|
|
106
107
|
return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
|
|
107
108
|
}
|
|
109
|
+
function byReferenceOf(symbols) {
|
|
110
|
+
const byReference = new Map();
|
|
111
|
+
for (const symbol of symbols) {
|
|
112
|
+
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
113
|
+
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
114
|
+
}
|
|
115
|
+
return byReference;
|
|
116
|
+
}
|
|
108
117
|
/** Reads one document at the target root, binding each record against the run's declarations. */
|
|
109
118
|
function readDocument(host, targetRoot, symbols, shape) {
|
|
110
119
|
const file = joinPath(targetRoot, shape.file);
|
|
@@ -139,11 +148,7 @@ function readDocument(host, targetRoot, symbols, shape) {
|
|
|
139
148
|
if ((description !== undefined && typeof description !== "string") || !Array.isArray(held)) {
|
|
140
149
|
return malformed(shape.file);
|
|
141
150
|
}
|
|
142
|
-
const byReference =
|
|
143
|
-
for (const symbol of symbols) {
|
|
144
|
-
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
145
|
-
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
146
|
-
}
|
|
151
|
+
const byReference = byReferenceOf(symbols);
|
|
147
152
|
const records = [];
|
|
148
153
|
const refusals = [];
|
|
149
154
|
held.forEach((value, index) => {
|
|
@@ -172,9 +177,26 @@ export function readIgnoreFile(host, targetRoot, symbols) {
|
|
|
172
177
|
export function readBaseline(host, targetRoot, symbols) {
|
|
173
178
|
return readDocument(host, targetRoot, symbols, BASELINE_SHAPE);
|
|
174
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Rows a baseline write holds in memory, bound against the run's declarations as the same
|
|
182
|
+
* rows read back from the document would be, each carrying the reason given.
|
|
183
|
+
*/
|
|
184
|
+
export function bindRows(rows, reason, symbols) {
|
|
185
|
+
const byReference = byReferenceOf(symbols);
|
|
186
|
+
const site = documentSite(BASELINE_SHAPE);
|
|
187
|
+
const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
|
|
188
|
+
return {
|
|
189
|
+
records: read.flatMap((one) => one.records),
|
|
190
|
+
refusals: read.flatMap((one) => one.refusals),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
175
193
|
/** What a written document says about itself, so the same report writes the same bytes. */
|
|
176
194
|
const BASELINE_DESCRIPTION = "Recorded findings: a run fails only on a finding this document does not hold. " +
|
|
177
195
|
"Every reason is the analyzer that recorded the row, never an adjudication.";
|
|
196
|
+
/** The reason every row a baseline write records carries. */
|
|
197
|
+
export function rowReason(provenance) {
|
|
198
|
+
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
199
|
+
}
|
|
178
200
|
/**
|
|
179
201
|
* A baseline holding one row per finding, in the order given, which is the report's. Every
|
|
180
202
|
* row carries the provenance as its reason. A finding missing a value a row needs, and an
|
|
@@ -185,12 +207,12 @@ export function writeBaseline(findings, provenance) {
|
|
|
185
207
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
186
208
|
throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
|
|
187
209
|
}
|
|
188
|
-
const reason =
|
|
210
|
+
const reason = rowReason(provenance);
|
|
189
211
|
const baseline = findings.map((found) => {
|
|
190
212
|
if (found.code === "" || found.symbol === "" || found.path === "") {
|
|
191
213
|
throw new Error(`a baseline row would carry no reason: a finding names code ${JSON.stringify(found.code)}, symbol ${JSON.stringify(found.symbol)} and path ${JSON.stringify(found.path)}`);
|
|
192
214
|
}
|
|
193
215
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
194
216
|
});
|
|
195
|
-
return
|
|
217
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
196
218
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { SyntaxKind } from "@typescript/native/unstable/ast";
|
|
7
7
|
import { nodeKey } from "./inventory.js";
|
|
8
8
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
9
|
+
import { signatureOf } from "./signatures.js";
|
|
9
10
|
import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
|
|
10
11
|
/**
|
|
11
12
|
* The three expressions the grammar applies to the text of each line comment, in this
|
|
@@ -134,7 +135,8 @@ export function lineComments(file) {
|
|
|
134
135
|
/**
|
|
135
136
|
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
136
137
|
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
137
|
-
* keyword, and a variable written on a later line of the statement also begins there.
|
|
138
|
+
* keyword, and a variable written on a later line of the statement also begins there. A
|
|
139
|
+
* function also begins, for a directive, on the line of each of its parameters.
|
|
138
140
|
*/
|
|
139
141
|
function declarationLines(file, held, symbols) {
|
|
140
142
|
const below = new Map();
|
|
@@ -151,6 +153,11 @@ function declarationLines(file, held, symbols) {
|
|
|
151
153
|
const one = id === undefined ? undefined : symbols.get(id);
|
|
152
154
|
if (one !== undefined) {
|
|
153
155
|
add(node.pos, one);
|
|
156
|
+
// A parameter on a later line of a wrapped signature takes its directive on the
|
|
157
|
+
// line above it, inside the parameter list, and the directive binds the function.
|
|
158
|
+
for (const parameter of signatureOf(node)?.parameters ?? []) {
|
|
159
|
+
add(parameter.pos, one);
|
|
160
|
+
}
|
|
154
161
|
const list = node.parent;
|
|
155
162
|
if (node.kind === SyntaxKind.VariableDeclaration &&
|
|
156
163
|
list.kind === SyntaxKind.VariableDeclarationList &&
|
package/dist/src/suppress.js
CHANGED
|
@@ -79,6 +79,7 @@ const ROW_SUBJECTS = new Set([
|
|
|
79
79
|
"dependency",
|
|
80
80
|
"module-directive",
|
|
81
81
|
"root",
|
|
82
|
+
"configured-declaration",
|
|
82
83
|
"suppression",
|
|
83
84
|
"edge",
|
|
84
85
|
]);
|
|
@@ -86,6 +87,32 @@ const ROW_SUBJECTS = new Set([
|
|
|
86
87
|
export function isRowSubject(kind) {
|
|
87
88
|
return ROW_SUBJECTS.has(kind);
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
92
|
+
* has no reference of its own, so a finding about one names the declaration that holds
|
|
93
|
+
* it, and the part is decided inside that declaration whatever uses it.
|
|
94
|
+
*/
|
|
95
|
+
const PART_SUBJECTS = new Set([
|
|
96
|
+
"parameter",
|
|
97
|
+
"receiver",
|
|
98
|
+
"result",
|
|
99
|
+
"statement",
|
|
100
|
+
"case",
|
|
101
|
+
"store",
|
|
102
|
+
]);
|
|
103
|
+
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
104
|
+
export function isPartSubject(kind) {
|
|
105
|
+
return PART_SUBJECTS.has(kind);
|
|
106
|
+
}
|
|
107
|
+
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
108
|
+
const PART_KIND_PREFIX = "DS18";
|
|
109
|
+
/**
|
|
110
|
+
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
111
|
+
* the record says the part is wanted, not that the declaration holding it is.
|
|
112
|
+
*/
|
|
113
|
+
export function isPartKind(code) {
|
|
114
|
+
return code.startsWith(PART_KIND_PREFIX);
|
|
115
|
+
}
|
|
89
116
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
90
117
|
function claimKey(finding) {
|
|
91
118
|
return `${finding.code}\u0000${positionKey(finding.position)}`;
|
package/dist/src/sweep.js
CHANGED
|
@@ -16,7 +16,10 @@ import { namesACaller, OUTSIDE } from "./graph.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export function sweep(graph, input) {
|
|
18
18
|
const marked = flagged(graph, input.marked);
|
|
19
|
-
const exempt = flagged(graph,
|
|
19
|
+
const exempt = flagged(graph, [
|
|
20
|
+
...(input.exempt ?? []).map((record) => record.id),
|
|
21
|
+
...(input.unanswered ?? []),
|
|
22
|
+
]);
|
|
20
23
|
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
21
24
|
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
22
25
|
// names a caller, and a mark, are live under reference counting as well; a root that
|
package/dist/src/value-reach.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The classes of the target a value of one type carries,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
+
* reaches them: through union and intersection constituents, array and tuple elements,
|
|
5
|
+
* and the data members and index signatures of the target's classes and of the
|
|
6
|
+
* interfaces and object types the target writes. The walk stops at `any`, `unknown`, a
|
|
7
|
+
* type parameter, a function and an interface or class of another program, so an
|
|
8
|
+
* instance held only there is missed.
|
|
7
9
|
*/
|
|
8
10
|
import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
9
11
|
import { classNames, typesAt } from "./class-members.js";
|
|
12
|
+
import { must } from "./query.js";
|
|
10
13
|
/** The object types whose members the walk reads whatever declares them. */
|
|
11
14
|
const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
12
15
|
/** The symbols of an anonymous object type the target writes as a type. */
|
|
13
16
|
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
17
|
+
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
18
|
+
const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
|
|
14
19
|
/** The global array types, whose element type is what an array value carries. */
|
|
15
20
|
function arrayTargets(project) {
|
|
16
|
-
const
|
|
21
|
+
const queries = project.queries;
|
|
17
22
|
return new Set(["Array", "ReadonlyArray"].flatMap((name) => {
|
|
18
|
-
const symbol =
|
|
19
|
-
return symbol === undefined ? [] : [
|
|
23
|
+
const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
|
|
24
|
+
return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
|
|
20
25
|
}));
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
@@ -24,7 +29,7 @@ function arrayTargets(project) {
|
|
|
24
29
|
* project's inventory.
|
|
25
30
|
*/
|
|
26
31
|
export function valueReach(project, held) {
|
|
27
|
-
const
|
|
32
|
+
const queries = project.queries;
|
|
28
33
|
const names = [...classNames(project, held)];
|
|
29
34
|
const declared = typesAt(project, names.map(([, node]) => node));
|
|
30
35
|
const classOfTarget = new Map();
|
|
@@ -35,12 +40,10 @@ export function valueReach(project, held) {
|
|
|
35
40
|
}
|
|
36
41
|
});
|
|
37
42
|
const arrays = arrayTargets(project);
|
|
38
|
-
const own =
|
|
43
|
+
const own = project.ownPaths();
|
|
39
44
|
const children = new Map();
|
|
40
45
|
/** The generic target of a type reference, and the type itself otherwise. */
|
|
41
|
-
const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0
|
|
42
|
-
? type.getTarget()
|
|
43
|
-
: type;
|
|
46
|
+
const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
|
|
44
47
|
const classOf = (type) => {
|
|
45
48
|
if (!type.isObjectType()) {
|
|
46
49
|
return undefined;
|
|
@@ -52,7 +55,7 @@ export function valueReach(project, held) {
|
|
|
52
55
|
if ((type.objectFlags & WRITTEN) !== 0) {
|
|
53
56
|
return true;
|
|
54
57
|
}
|
|
55
|
-
const symbol =
|
|
58
|
+
const symbol = must(queries.symbolOfType(type));
|
|
56
59
|
if (symbol === undefined) {
|
|
57
60
|
return false;
|
|
58
61
|
}
|
|
@@ -64,15 +67,27 @@ export function valueReach(project, held) {
|
|
|
64
67
|
};
|
|
65
68
|
/** The types a value of one object type holds in its data members. */
|
|
66
69
|
const memberTypes = (type) => {
|
|
67
|
-
const properties =
|
|
68
|
-
|
|
69
|
-
.filter((property) => (property.flags & SymbolFlags.Property) !== 0);
|
|
70
|
-
const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
|
|
70
|
+
const properties = must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0);
|
|
71
|
+
const types = queries.typesOfSymbols(properties).map(must);
|
|
71
72
|
return [
|
|
72
73
|
...types.filter((held) => held !== undefined),
|
|
73
|
-
...
|
|
74
|
+
...must(queries.indexInfosOf(type)).map((info) => info.valueType),
|
|
74
75
|
];
|
|
75
76
|
};
|
|
77
|
+
/** Whether the walk reads one type's members because the target writes the type, not as a class. */
|
|
78
|
+
const writtenObject = (type) => {
|
|
79
|
+
if (!type.isObjectType() || classOf(type) !== undefined) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const target = targetOf(type);
|
|
83
|
+
const isList = type.isTypeReference() &&
|
|
84
|
+
(arrays.has(target.id) ||
|
|
85
|
+
(target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
|
|
86
|
+
return !isList && isWritten(type);
|
|
87
|
+
};
|
|
88
|
+
/** The data members and conversion methods of one written object type. */
|
|
89
|
+
const carried = (type) => must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
|
|
90
|
+
((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
|
|
76
91
|
/** The types a value of one type holds, read once per type. */
|
|
77
92
|
const childrenOf = (type) => {
|
|
78
93
|
const known = children.get(type.id);
|
|
@@ -81,7 +96,7 @@ export function valueReach(project, held) {
|
|
|
81
96
|
}
|
|
82
97
|
let found = [];
|
|
83
98
|
if (type.isUnionType() || type.isIntersectionType()) {
|
|
84
|
-
found =
|
|
99
|
+
found = must(queries.constituents(type));
|
|
85
100
|
}
|
|
86
101
|
else if (type.isObjectType()) {
|
|
87
102
|
const target = targetOf(type);
|
|
@@ -92,15 +107,16 @@ export function valueReach(project, held) {
|
|
|
92
107
|
found = memberTypes(type);
|
|
93
108
|
}
|
|
94
109
|
else if (isList && type.isTypeReference()) {
|
|
95
|
-
found =
|
|
110
|
+
found = must(queries.typeArguments(type));
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
children.set(type.id, found);
|
|
99
114
|
return found;
|
|
100
115
|
};
|
|
101
116
|
return {
|
|
102
|
-
|
|
103
|
-
const
|
|
117
|
+
reachedFrom: (type) => {
|
|
118
|
+
const classes = new Set();
|
|
119
|
+
const members = [];
|
|
104
120
|
const seen = new Set();
|
|
105
121
|
const pending = [type];
|
|
106
122
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
@@ -110,11 +126,14 @@ export function valueReach(project, held) {
|
|
|
110
126
|
seen.add(next.id);
|
|
111
127
|
const owner = classOf(next);
|
|
112
128
|
if (owner !== undefined) {
|
|
113
|
-
|
|
129
|
+
classes.add(owner);
|
|
130
|
+
}
|
|
131
|
+
else if (writtenObject(next)) {
|
|
132
|
+
members.push(...carried(next));
|
|
114
133
|
}
|
|
115
134
|
pending.push(...childrenOf(next));
|
|
116
135
|
}
|
|
117
|
-
return
|
|
136
|
+
return { classes, members };
|
|
118
137
|
},
|
|
119
138
|
};
|
|
120
139
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { runAnalysis } from "../analysis.js";
|
|
1
|
+
import { runAnalysis, runBaselineWrite } from "../analysis.js";
|
|
2
2
|
import { ConfigError } from "../config.js";
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
4
4
|
import { moduleIdentity } from "../consumers.js";
|
|
5
|
-
import { analyzerProvenance,
|
|
5
|
+
import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
|
+
import { partialNotes } from "../partial.js";
|
|
7
8
|
import { buildReport, capFindings, sortFindings, } from "../report.js";
|
|
8
9
|
import { json } from "../reporters/json.js";
|
|
9
10
|
import { RENDERINGS } from "../reporters/reporters.js";
|
|
10
11
|
import { parseTemplate, TemplateError } from "../reporters/template.js";
|
|
11
12
|
import { resolve } from "../resolve.js";
|
|
12
|
-
import { writeBaseline } from "../suppress-file.js";
|
|
13
|
+
import { rowReason, writeBaseline } from "../suppress-file.js";
|
|
13
14
|
import { CONTRACT_VERSION } from "../version.js";
|
|
14
15
|
import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
|
|
15
16
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
@@ -61,6 +62,16 @@ function runRelative(invoked, path) {
|
|
|
61
62
|
}
|
|
62
63
|
return below;
|
|
63
64
|
}
|
|
65
|
+
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
66
|
+
function notBuiltOf(dropped, targetRoot) {
|
|
67
|
+
return dropped.map((one) => {
|
|
68
|
+
const project = relativePath(targetRoot, one.configFile);
|
|
69
|
+
if (project === undefined) {
|
|
70
|
+
throw new ReportPathError(`the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`);
|
|
71
|
+
}
|
|
72
|
+
return { id: one.id, project, error: one.error };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
64
75
|
/** Each project of the run as a report names it, by its configuration file below the target. */
|
|
65
76
|
function configurationsOf(projects, targetRoot) {
|
|
66
77
|
return projects.map((one) => {
|
|
@@ -107,8 +118,9 @@ function templateOf(host, invoked, named, formats) {
|
|
|
107
118
|
/**
|
|
108
119
|
* Analyzes the scope under the configuration and writes the JSON report to the path
|
|
109
120
|
* `--report` names, each rendering `--format` names beside it at the report's path with the
|
|
110
|
-
* format's suffix appended, and
|
|
111
|
-
*
|
|
121
|
+
* format's suffix appended, and where `--baseline-write` names a path the baseline a fixpoint
|
|
122
|
+
* over the analysis records, the report then being its first round, which reads no
|
|
123
|
+
* baseline. The template format renders the template `--template` names. Nothing is written to
|
|
112
124
|
* the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
|
|
113
125
|
* under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
|
|
114
126
|
*/
|
|
@@ -147,11 +159,23 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
147
159
|
let report;
|
|
148
160
|
let recorded;
|
|
149
161
|
let verdict;
|
|
162
|
+
let partial;
|
|
150
163
|
try {
|
|
151
164
|
const root = runRelative(invoked, targetRoot);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
const mode = { production: true };
|
|
166
|
+
let analysis;
|
|
167
|
+
if (documents.baseline === undefined) {
|
|
168
|
+
analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
|
|
169
|
+
recorded = [];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const reason = rowReason(analyzerProvenance(version));
|
|
173
|
+
const write = runBaselineWrite(openClient(false), host, scoped, config, provenance, mode, reason);
|
|
174
|
+
analysis = write.first;
|
|
175
|
+
recorded = write.rows;
|
|
176
|
+
}
|
|
177
|
+
const { result, run } = analysis;
|
|
178
|
+
partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
|
|
155
179
|
const built = buildReport({
|
|
156
180
|
contractVersion: CONTRACT_VERSION,
|
|
157
181
|
version,
|
|
@@ -159,6 +183,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
159
183
|
declaredGaps: DECLARED_GAPS,
|
|
160
184
|
target: { kind, root, identity: moduleIdentity(host, scoped.target) },
|
|
161
185
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
186
|
+
notBuilt: notBuiltOf(run.notBuilt, targetRoot),
|
|
162
187
|
loaded: run.consumers.map((consumer) => ({
|
|
163
188
|
id: consumer.id,
|
|
164
189
|
path: runRelative(invoked, consumer.path),
|
|
@@ -167,9 +192,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
167
192
|
result,
|
|
168
193
|
testFileRules: run.testFileRules,
|
|
169
194
|
});
|
|
170
|
-
// The baseline records every finding, so its rows are taken before the cap bounds what
|
|
171
|
-
// a rendering prints: a baseline missing a reported finding fails the next run on it.
|
|
172
|
-
recorded = recordedFindings(result);
|
|
173
195
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
174
196
|
verdict = verdictOf(result, config);
|
|
175
197
|
}
|
|
@@ -181,7 +203,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
181
203
|
return EXIT_FAILURE;
|
|
182
204
|
}
|
|
183
205
|
try {
|
|
184
|
-
host.
|
|
206
|
+
host.writeDocument(documents.report, json(report));
|
|
185
207
|
const options = {
|
|
186
208
|
failOn: config.reporters.failOn,
|
|
187
209
|
readSource: (path) => host.readFile(joinPath(targetRoot, path)),
|
|
@@ -190,17 +212,20 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
190
212
|
for (const format of documents.formats) {
|
|
191
213
|
const rendering = RENDERINGS.get(format);
|
|
192
214
|
if (rendering !== undefined) {
|
|
193
|
-
host.
|
|
215
|
+
host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
|
|
194
216
|
}
|
|
195
217
|
}
|
|
196
218
|
if (documents.baseline !== undefined) {
|
|
197
|
-
host.
|
|
219
|
+
host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
|
|
198
220
|
}
|
|
199
221
|
}
|
|
200
222
|
catch (error) {
|
|
201
223
|
err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
202
224
|
return EXIT_FAILURE;
|
|
203
225
|
}
|
|
226
|
+
for (const note of partial) {
|
|
227
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
228
|
+
}
|
|
204
229
|
if (report.totals.pending > 0) {
|
|
205
230
|
err.write(`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`);
|
|
206
231
|
}
|