@cplieger/deadset-ts 6.0.0 → 6.0.1
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 +1 -1
- package/dist/src/findings/completion.js +9 -4
- package/dist/src/findings/emitters.js +17 -13
- package/dist/src/findings/reads-and-writes.js +3 -2
- package/dist/src/findings/visibility-narrowing.js +2 -1
- package/dist/src/ref.js +8 -0
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/suppress-file.js +3 -5
- package/dist/src/workspace-programs.js +6 -31
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/findings/completion.ts +11 -4
- package/src/findings/emitters.ts +18 -15
- package/src/findings/reads-and-writes.ts +7 -2
- package/src/findings/visibility-narrowing.ts +2 -1
- package/src/ref.ts +9 -0
- package/src/reporters/sarif.ts +5 -1
- package/src/suppress-file.ts +3 -6
- package/src/workspace-programs.ts +6 -35
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
|
|
6
6
|
|
|
7
|
-
It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.
|
|
7
|
+
It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261007.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
|
|
8
8
|
|
|
9
9
|
## Why use it
|
|
10
10
|
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { componentId } from "../components.js";
|
|
7
7
|
import { OUTSIDE } from "../graph.js";
|
|
8
8
|
import { KINDS } from "../kinds.js";
|
|
9
|
-
import { byPosition } from "../position.js";
|
|
9
|
+
import { byPosition, positionKey } from "../position.js";
|
|
10
|
+
import { declarationKey } from "../ref.js";
|
|
10
11
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
11
12
|
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
12
13
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
@@ -117,7 +118,7 @@ function findingComponents(swept, cascade) {
|
|
|
117
118
|
export function completed(input, findings, kinds = KINDS) {
|
|
118
119
|
const { swept } = input;
|
|
119
120
|
const union = swept.matrix.union;
|
|
120
|
-
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
121
|
+
const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
|
|
121
122
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
122
123
|
const classOf = reachabilityClasses(input);
|
|
123
124
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
@@ -135,10 +136,14 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
135
136
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
136
137
|
}
|
|
137
138
|
// A row's reference may spell a declaration's, as an ignore entry naming one does, and
|
|
138
|
-
// a row is still never the declaration.
|
|
139
|
+
// a row is still never the declaration. Two declarations of one file can share a
|
|
140
|
+
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
141
|
+
const declared = union.at(positionKey(finding.position));
|
|
139
142
|
const at = isRowSubject(finding.symbol.kind)
|
|
140
143
|
? OUTSIDE
|
|
141
|
-
:
|
|
144
|
+
: union.symbols[declared]?.ref === finding.symbol.ref
|
|
145
|
+
? declared
|
|
146
|
+
: (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
|
|
142
147
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
143
148
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
144
149
|
const part = isPartSubject(finding.symbol.kind);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateEdges } from "../edges.js";
|
|
2
2
|
import { liveFilesAt } from "../matrix.js";
|
|
3
|
+
import { declarationKey } from "../ref.js";
|
|
3
4
|
import { ledgerOf } from "../suppress.js";
|
|
4
5
|
import { insideSkipped } from "../type-errors.js";
|
|
5
6
|
import { completed, dialsOf, reportable } from "./completion.js";
|
|
@@ -23,24 +24,24 @@ export const EMITTERS = new Map([
|
|
|
23
24
|
["intra-function", intraFunction],
|
|
24
25
|
]);
|
|
25
26
|
/**
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
27
|
+
* The keys of every declaration an unanswered question could have kept live, which the run
|
|
28
|
+
* reports nothing about: a finding about one would be computed from the gap.
|
|
28
29
|
*/
|
|
29
|
-
function
|
|
30
|
+
function heldKeys(input) {
|
|
30
31
|
const held = new Set(input.swept.heldByUnanswered);
|
|
31
32
|
if (held.size === 0) {
|
|
32
33
|
return held;
|
|
33
34
|
}
|
|
34
35
|
return new Set(input.swept.matrix.union.symbols
|
|
35
36
|
.filter((symbol) => held.has(symbol.id))
|
|
36
|
-
.map((symbol) => symbol.ref));
|
|
37
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* The
|
|
40
|
+
* The keys of every top-level declaration of a live component file. The file's
|
|
40
41
|
* markup may use each of them, which no analysis of the markup can rule out, so the run
|
|
41
42
|
* reports nothing about one while the file is live.
|
|
42
43
|
*/
|
|
43
|
-
function
|
|
44
|
+
function markupKeys(input) {
|
|
44
45
|
const { swept } = input;
|
|
45
46
|
if (swept.componentFiles.length === 0) {
|
|
46
47
|
return new Set();
|
|
@@ -48,7 +49,7 @@ function markupRefs(input) {
|
|
|
48
49
|
const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
|
|
49
50
|
return new Set(swept.matrix.union.symbols
|
|
50
51
|
.filter((symbol) => live.has(symbol.parent))
|
|
51
|
-
.map((symbol) => symbol.ref));
|
|
52
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
52
53
|
}
|
|
53
54
|
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
54
55
|
function completedOver(input, ...more) {
|
|
@@ -104,12 +105,15 @@ function atPossible(input) {
|
|
|
104
105
|
function dialledOver(input, suppressed, found, unmarked) {
|
|
105
106
|
const would = [...unmarked, ...found];
|
|
106
107
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
107
|
-
const unanswered =
|
|
108
|
-
const markup =
|
|
109
|
-
const kept = reportable(input, found.filter((finding) =>
|
|
110
|
-
|
|
111
|
-
!
|
|
112
|
-
|
|
108
|
+
const unanswered = heldKeys(input);
|
|
109
|
+
const markup = markupKeys(input);
|
|
110
|
+
const kept = reportable(input, found.filter((finding) => {
|
|
111
|
+
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
112
|
+
return (!ledger.withheld(finding) &&
|
|
113
|
+
!unanswered.has(key) &&
|
|
114
|
+
!markup.has(key) &&
|
|
115
|
+
!insideSkipped(input.swept.skipped, finding.position));
|
|
116
|
+
}));
|
|
113
117
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
114
118
|
return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
|
|
115
119
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { positionKey } from "../position.js";
|
|
2
|
+
import { declarationKey } from "../ref.js";
|
|
2
3
|
/** The containers whose type parameters `DS1303` reports: a function and a class method. */
|
|
3
4
|
const SIGNATURES = new Set(["function", "method"]);
|
|
4
5
|
function indexed(input) {
|
|
@@ -175,12 +176,12 @@ function unusedTypeParameters(run) {
|
|
|
175
176
|
.map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
178
|
-
* The
|
|
179
|
+
* The keys of the declarations the write-only kind reports, whatever the
|
|
179
180
|
* severity: a narrowing kind yields each to it, since deleting the declaration
|
|
180
181
|
* supersedes narrowing it.
|
|
181
182
|
*/
|
|
182
183
|
export function writeOnlyDeclarations(input) {
|
|
183
|
-
return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
|
|
184
|
+
return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
|
|
184
185
|
}
|
|
185
186
|
/**
|
|
186
187
|
* The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OUTSIDE } from "../graph.js";
|
|
2
|
+
import { declarationKey } from "../ref.js";
|
|
2
3
|
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
3
4
|
const UNNECESSARY_EXPORT = "DS1101";
|
|
4
5
|
const UNREACHABLE_EXPORT = "DS1103";
|
|
@@ -119,7 +120,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
119
120
|
const { closed } = world;
|
|
120
121
|
const file = facts.fileOf[at] ?? OUTSIDE;
|
|
121
122
|
if (!topLevelExport(facts, at, symbol) ||
|
|
122
|
-
world.writeOnly.has(symbol.ref) ||
|
|
123
|
+
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
123
124
|
symbol.kind === "export-alias" ||
|
|
124
125
|
entersByName(facts.rootedBy[at]) ||
|
|
125
126
|
entersByName(facts.rootedBy[file]) ||
|
package/dist/src/ref.js
CHANGED
|
@@ -107,6 +107,14 @@ export function renderRef(module, fragment) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* One declaration's key among a run's: its reference and the file that declares it. Two
|
|
112
|
+
* packages of one name spell one reference for two declarations, so the reference alone
|
|
113
|
+
* keys neither.
|
|
114
|
+
*/
|
|
115
|
+
export function declarationKey(ref, path) {
|
|
116
|
+
return `${ref}\u0000${path}`;
|
|
117
|
+
}
|
|
110
118
|
/**
|
|
111
119
|
* Whether one string is a reference of this language's grammar, in its canonical
|
|
112
120
|
* spelling. A string that is not, whether misspelled or spelled in a form the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsonChunks } from "../json-chunks.js";
|
|
2
2
|
import { KINDS } from "../kinds.js";
|
|
3
|
+
import { declarationKey } from "../ref.js";
|
|
3
4
|
import { lineHashes, symbolFingerprint } from "./fingerprint.js";
|
|
4
5
|
import { RenderError } from "./reporter.js";
|
|
5
6
|
import { utf8Bytes } from "./sha256.js";
|
|
@@ -122,7 +123,8 @@ function relatedOf(found) {
|
|
|
122
123
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
123
124
|
}
|
|
124
125
|
for (const one of found.component.members ?? []) {
|
|
125
|
-
if (one.ref
|
|
126
|
+
if (declarationKey(one.ref, one.position.path) !==
|
|
127
|
+
declarationKey(found.symbol.ref, found.position.path)) {
|
|
126
128
|
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { jsonChunks } from "./json-chunks.js";
|
|
8
8
|
import { joinPath } from "./paths.js";
|
|
9
9
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
10
|
+
import { declarationKey } from "./ref.js";
|
|
10
11
|
import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
|
|
11
12
|
/** The name and the location the grammar fixes for the ignore file. */
|
|
12
13
|
export const IGNORE_FILE = "deadset-ignore.json";
|
|
@@ -38,9 +39,6 @@ function documentSite(shape) {
|
|
|
38
39
|
function isObject(value) {
|
|
39
40
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
41
|
}
|
|
41
|
-
function referenceKey(ref, path) {
|
|
42
|
-
return `${ref}\u0000${path}`;
|
|
43
|
-
}
|
|
44
42
|
/**
|
|
45
43
|
* Reads one entry or row: the records it binds, the refusals it carries, or the
|
|
46
44
|
* malformed value that ends the run.
|
|
@@ -100,7 +98,7 @@ function readRecord(value, site, byReference, shape) {
|
|
|
100
98
|
if (refusals.length > 0) {
|
|
101
99
|
return { records: [], refusals };
|
|
102
100
|
}
|
|
103
|
-
const named = byReference.get(
|
|
101
|
+
const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
|
|
104
102
|
if (named.length === 0) {
|
|
105
103
|
return { records: [{ ...written, bound: "" }], refusals: [] };
|
|
106
104
|
}
|
|
@@ -109,7 +107,7 @@ function readRecord(value, site, byReference, shape) {
|
|
|
109
107
|
function byReferenceOf(symbols) {
|
|
110
108
|
const byReference = new Map();
|
|
111
109
|
for (const symbol of symbols) {
|
|
112
|
-
const key =
|
|
110
|
+
const key = declarationKey(symbol.ref, symbol.position.path);
|
|
113
111
|
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
114
112
|
}
|
|
115
113
|
return byReference;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
|
|
9
9
|
import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
|
|
10
|
-
import { dirnamePath,
|
|
10
|
+
import { dirnamePath, resolvePath } from "./paths.js";
|
|
11
11
|
/** The most questions one round trip carries. */
|
|
12
12
|
const BATCH = 2048;
|
|
13
13
|
/**
|
|
@@ -20,31 +20,6 @@ function analysisOptions(parsed) {
|
|
|
20
20
|
const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
|
|
21
21
|
return composite === true ? { ...kept, declaration: true } : kept;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* The references a built program can carry: only where every referenced
|
|
25
|
-
* configuration reads and is composite with output, because the compiler reports a
|
|
26
|
-
* broken reference at syntax a built program does not have.
|
|
27
|
-
*/
|
|
28
|
-
function carriedReferences(engine, parsed) {
|
|
29
|
-
const references = parsed.projectReferences ?? [];
|
|
30
|
-
for (const reference of references) {
|
|
31
|
-
// A reference that names a directory names the `tsconfig.json` in it.
|
|
32
|
-
const configFile = reference.path.endsWith(".json")
|
|
33
|
-
? reference.path
|
|
34
|
-
: joinPath(reference.path, "tsconfig.json");
|
|
35
|
-
let options;
|
|
36
|
-
try {
|
|
37
|
-
options = engine.parseConfigFile(configFile).options;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
if (options.composite !== true || options.noEmit === true) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return references;
|
|
47
|
-
}
|
|
48
23
|
/** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
|
|
49
24
|
function conditionsOf(parsed, mode) {
|
|
50
25
|
const resolution = parsed.options.moduleResolution;
|
|
@@ -94,7 +69,7 @@ function configurationOf(engine, host, reading) {
|
|
|
94
69
|
if (reason !== undefined) {
|
|
95
70
|
return { kind: "unread", reason };
|
|
96
71
|
}
|
|
97
|
-
const { options } = reading.parsed;
|
|
72
|
+
const { options, projectReferences: references = [] } = reading.parsed;
|
|
98
73
|
const raw = options["pathsBasePath"];
|
|
99
74
|
const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
|
|
100
75
|
const paths = {};
|
|
@@ -117,8 +92,8 @@ function configurationOf(engine, host, reading) {
|
|
|
117
92
|
...(options.composite === true ? { composite: false, declaration: true } : {}),
|
|
118
93
|
paths,
|
|
119
94
|
},
|
|
120
|
-
...(
|
|
121
|
-
? { references:
|
|
95
|
+
...(references.length > 0
|
|
96
|
+
? { references: references.map((reference) => ({ path: reference.path })) }
|
|
122
97
|
: {}),
|
|
123
98
|
...(exclude === undefined ? {} : { exclude }),
|
|
124
99
|
});
|
|
@@ -150,6 +125,7 @@ function configurationOf(engine, host, reading) {
|
|
|
150
125
|
/** The program one configuration is built as, under the answers it holds so far. */
|
|
151
126
|
function programOf(engine, reading) {
|
|
152
127
|
const compilerOptions = analysisOptions(reading.parsed);
|
|
128
|
+
const references = reading.parsed.projectReferences ?? [];
|
|
153
129
|
const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
|
|
154
130
|
return {
|
|
155
131
|
resolver,
|
|
@@ -159,7 +135,7 @@ function programOf(engine, reading) {
|
|
|
159
135
|
options: {
|
|
160
136
|
moduleResolver: resolver,
|
|
161
137
|
configFileParsingDiagnostics: reading.parsed.errors,
|
|
162
|
-
...(
|
|
138
|
+
...(references.length > 0 ? { projectReferences: references } : {}),
|
|
163
139
|
},
|
|
164
140
|
},
|
|
165
141
|
};
|
|
@@ -292,7 +268,6 @@ export function workspacePrograms(engine, host, configFiles, resolver) {
|
|
|
292
268
|
{
|
|
293
269
|
configFile,
|
|
294
270
|
parsed,
|
|
295
|
-
references: carriedReferences(engine, parsed),
|
|
296
271
|
entries: [],
|
|
297
272
|
seen: new Set(),
|
|
298
273
|
bound: new Map(),
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@typescript/native": "npm:typescript@7.1.0-dev.
|
|
32
|
+
"@typescript/native": "npm:typescript@7.1.0-dev.20261007.1"
|
|
33
33
|
},
|
|
34
34
|
"overrides": {
|
|
35
35
|
"qs": "^6.15.2"
|
|
@@ -10,7 +10,8 @@ import type { Cascade, Confidence, Severity } from "../config.ts";
|
|
|
10
10
|
import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
12
12
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
|
-
import { byPosition } from "../position.ts";
|
|
13
|
+
import { byPosition, positionKey } from "../position.ts";
|
|
14
|
+
import { declarationKey } from "../ref.ts";
|
|
14
15
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
15
16
|
import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
|
|
16
17
|
import type { Boundary } from "./boundary.ts";
|
|
@@ -149,7 +150,9 @@ export function completed(
|
|
|
149
150
|
): readonly CompletedFinding[] {
|
|
150
151
|
const { swept } = input;
|
|
151
152
|
const union = swept.matrix.union;
|
|
152
|
-
const positions = new Map(
|
|
153
|
+
const positions = new Map(
|
|
154
|
+
union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]),
|
|
155
|
+
);
|
|
153
156
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
154
157
|
const classOf = reachabilityClasses(input);
|
|
155
158
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
@@ -168,10 +171,14 @@ export function completed(
|
|
|
168
171
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
169
172
|
}
|
|
170
173
|
// A row's reference may spell a declaration's, as an ignore entry naming one does, and
|
|
171
|
-
// a row is still never the declaration.
|
|
174
|
+
// a row is still never the declaration. Two declarations of one file can share a
|
|
175
|
+
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
176
|
+
const declared = union.at(positionKey(finding.position));
|
|
172
177
|
const at = isRowSubject(finding.symbol.kind)
|
|
173
178
|
? OUTSIDE
|
|
174
|
-
:
|
|
179
|
+
: union.symbols[declared]?.ref === finding.symbol.ref
|
|
180
|
+
? declared
|
|
181
|
+
: (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
|
|
175
182
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
176
183
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
177
184
|
const part = isPartSubject(finding.symbol.kind);
|
package/src/findings/emitters.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Confidence } from "../config.ts";
|
|
|
2
2
|
import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
|
|
3
3
|
import type { CompletedFinding } from "../finding.ts";
|
|
4
4
|
import { liveFilesAt } from "../matrix.ts";
|
|
5
|
+
import { declarationKey } from "../ref.ts";
|
|
5
6
|
import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
|
|
6
7
|
import { insideSkipped } from "../type-errors.ts";
|
|
7
8
|
import { completed, dialsOf, reportable } from "./completion.ts";
|
|
@@ -28,10 +29,10 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
32
|
+
* The keys of every declaration an unanswered question could have kept live, which the run
|
|
33
|
+
* reports nothing about: a finding about one would be computed from the gap.
|
|
33
34
|
*/
|
|
34
|
-
function
|
|
35
|
+
function heldKeys(input: EmitterInput): ReadonlySet<string> {
|
|
35
36
|
const held = new Set(input.swept.heldByUnanswered);
|
|
36
37
|
if (held.size === 0) {
|
|
37
38
|
return held;
|
|
@@ -39,16 +40,16 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
|
|
|
39
40
|
return new Set(
|
|
40
41
|
input.swept.matrix.union.symbols
|
|
41
42
|
.filter((symbol) => held.has(symbol.id))
|
|
42
|
-
.map((symbol) => symbol.ref),
|
|
43
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
* The
|
|
48
|
+
* The keys of every top-level declaration of a live component file. The file's
|
|
48
49
|
* markup may use each of them, which no analysis of the markup can rule out, so the run
|
|
49
50
|
* reports nothing about one while the file is live.
|
|
50
51
|
*/
|
|
51
|
-
function
|
|
52
|
+
function markupKeys(input: EmitterInput): ReadonlySet<string> {
|
|
52
53
|
const { swept } = input;
|
|
53
54
|
if (swept.componentFiles.length === 0) {
|
|
54
55
|
return new Set();
|
|
@@ -61,7 +62,7 @@ function markupRefs(input: EmitterInput): ReadonlySet<string> {
|
|
|
61
62
|
return new Set(
|
|
62
63
|
swept.matrix.union.symbols
|
|
63
64
|
.filter((symbol) => live.has(symbol.parent))
|
|
64
|
-
.map((symbol) => symbol.ref),
|
|
65
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -175,17 +176,19 @@ function dialledOver(
|
|
|
175
176
|
): Omit<DecidedFindings, "would" | "withheld"> {
|
|
176
177
|
const would = [...unmarked, ...found];
|
|
177
178
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
178
|
-
const unanswered =
|
|
179
|
-
const markup =
|
|
179
|
+
const unanswered = heldKeys(input);
|
|
180
|
+
const markup = markupKeys(input);
|
|
180
181
|
const kept = reportable(
|
|
181
182
|
input,
|
|
182
|
-
found.filter(
|
|
183
|
-
(finding)
|
|
183
|
+
found.filter((finding) => {
|
|
184
|
+
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
185
|
+
return (
|
|
184
186
|
!ledger.withheld(finding) &&
|
|
185
|
-
!unanswered.has(
|
|
186
|
-
!markup.has(
|
|
187
|
-
!insideSkipped(input.swept.skipped, finding.position)
|
|
188
|
-
|
|
187
|
+
!unanswered.has(key) &&
|
|
188
|
+
!markup.has(key) &&
|
|
189
|
+
!insideSkipped(input.swept.skipped, finding.position)
|
|
190
|
+
);
|
|
191
|
+
}),
|
|
189
192
|
);
|
|
190
193
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
191
194
|
return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
|
|
@@ -2,6 +2,7 @@ import type { Finding, FindingDetails, FindingPosition } from "../finding.ts";
|
|
|
2
2
|
import type { InventorySymbol } from "../inventory.ts";
|
|
3
3
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
4
4
|
import { positionKey } from "../position.ts";
|
|
5
|
+
import { declarationKey } from "../ref.ts";
|
|
5
6
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
6
7
|
|
|
7
8
|
/** The codes of this family. */
|
|
@@ -242,12 +243,16 @@ function unusedTypeParameters(run: Run): readonly Finding[] {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
/**
|
|
245
|
-
* The
|
|
246
|
+
* The keys of the declarations the write-only kind reports, whatever the
|
|
246
247
|
* severity: a narrowing kind yields each to it, since deleting the declaration
|
|
247
248
|
* supersedes narrowing it.
|
|
248
249
|
*/
|
|
249
250
|
export function writeOnlyDeclarations(input: EmitterInput): ReadonlySet<string> {
|
|
250
|
-
return new Set(
|
|
251
|
+
return new Set(
|
|
252
|
+
writeOnlySymbols(indexed(input)).map((finding) =>
|
|
253
|
+
declarationKey(finding.symbol.ref, finding.position.path),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
/**
|
|
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
|
|
|
2
2
|
import { OUTSIDE, type Graph } from "../graph.ts";
|
|
3
3
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
4
4
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
5
|
+
import { declarationKey } from "../ref.ts";
|
|
5
6
|
import type { RootKind } from "../roots.ts";
|
|
6
7
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
7
8
|
import { writeOnlyDeclarations } from "./reads-and-writes.ts";
|
|
@@ -159,7 +160,7 @@ function narrowing(
|
|
|
159
160
|
const file = facts.fileOf[at] ?? OUTSIDE;
|
|
160
161
|
if (
|
|
161
162
|
!topLevelExport(facts, at, symbol) ||
|
|
162
|
-
world.writeOnly.has(symbol.ref) ||
|
|
163
|
+
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
163
164
|
symbol.kind === "export-alias" ||
|
|
164
165
|
entersByName(facts.rootedBy[at]) ||
|
|
165
166
|
entersByName(facts.rootedBy[file]) ||
|
package/src/ref.ts
CHANGED
|
@@ -169,6 +169,15 @@ export function renderRef(module: Module, fragment: Fragment): string {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* One declaration's key among a run's: its reference and the file that declares it. Two
|
|
174
|
+
* packages of one name spell one reference for two declarations, so the reference alone
|
|
175
|
+
* keys neither.
|
|
176
|
+
*/
|
|
177
|
+
export function declarationKey(ref: string, path: string): string {
|
|
178
|
+
return `${ref}\u0000${path}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
172
181
|
/**
|
|
173
182
|
* Whether one string is a reference of this language's grammar, in its canonical
|
|
174
183
|
* spelling. A string that is not, whether misspelled or spelled in a form the
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "../config.ts";
|
|
2
2
|
import { jsonChunks } from "../json-chunks.ts";
|
|
3
3
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
4
|
+
import { declarationKey } from "../ref.ts";
|
|
4
5
|
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
5
6
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
6
7
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
@@ -160,7 +161,10 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
160
161
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
161
162
|
}
|
|
162
163
|
for (const one of found.component.members ?? []) {
|
|
163
|
-
if (
|
|
164
|
+
if (
|
|
165
|
+
declarationKey(one.ref, one.position.path) !==
|
|
166
|
+
declarationKey(found.symbol.ref, found.position.path)
|
|
167
|
+
) {
|
|
164
168
|
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
165
169
|
}
|
|
166
170
|
}
|
package/src/suppress-file.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { jsonChunks } from "./json-chunks.ts";
|
|
|
11
11
|
import { joinPath } from "./paths.ts";
|
|
12
12
|
import { positionAt, walkDocument } from "./json-document.ts";
|
|
13
13
|
import type { Position } from "./position.ts";
|
|
14
|
+
import { declarationKey } from "./ref.ts";
|
|
14
15
|
import {
|
|
15
16
|
CODE_FORM,
|
|
16
17
|
SUPPRESSION_WITHOUT_REASON,
|
|
@@ -84,10 +85,6 @@ interface Read {
|
|
|
84
85
|
/** The declarations of the run by reference and file, which is what an entry names. */
|
|
85
86
|
type ByReference = ReadonlyMap<string, readonly InventorySymbol[]>;
|
|
86
87
|
|
|
87
|
-
function referenceKey(ref: string, path: string): string {
|
|
88
|
-
return `${ref}\u0000${path}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
88
|
/**
|
|
92
89
|
* Reads one entry or row: the records it binds, the refusals it carries, or the
|
|
93
90
|
* malformed value that ends the run.
|
|
@@ -148,7 +145,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
148
145
|
if (refusals.length > 0) {
|
|
149
146
|
return { records: [], refusals };
|
|
150
147
|
}
|
|
151
|
-
const named = byReference.get(
|
|
148
|
+
const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
|
|
152
149
|
if (named.length === 0) {
|
|
153
150
|
return { records: [{ ...written, bound: "" }], refusals: [] };
|
|
154
151
|
}
|
|
@@ -158,7 +155,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
158
155
|
function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
|
|
159
156
|
const byReference = new Map<string, InventorySymbol[]>();
|
|
160
157
|
for (const symbol of symbols) {
|
|
161
|
-
const key =
|
|
158
|
+
const key = declarationKey(symbol.ref, symbol.position.path);
|
|
162
159
|
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
163
160
|
}
|
|
164
161
|
return byReference;
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
|
|
18
18
|
import type { StringLiteralLikeNode } from "@typescript/native/unstable/ast";
|
|
19
19
|
import type { Host } from "./host.ts";
|
|
20
|
-
import { dirnamePath,
|
|
20
|
+
import { dirnamePath, resolvePath } from "./paths.ts";
|
|
21
21
|
import type { Engine } from "./session.ts";
|
|
22
22
|
import type { WorkspaceResolver } from "./workspace-resolution.ts";
|
|
23
23
|
|
|
@@ -82,34 +82,6 @@ function analysisOptions(parsed: Parsed): CreateSnapshotProgramParams["compilerO
|
|
|
82
82
|
return composite === true ? { ...kept, declaration: true } : kept;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* The references a built program can carry: only where every referenced
|
|
87
|
-
* configuration reads and is composite with output, because the compiler reports a
|
|
88
|
-
* broken reference at syntax a built program does not have.
|
|
89
|
-
*/
|
|
90
|
-
function carriedReferences(
|
|
91
|
-
engine: Engine,
|
|
92
|
-
parsed: Parsed,
|
|
93
|
-
): NonNullable<Parsed["projectReferences"]> {
|
|
94
|
-
const references = parsed.projectReferences ?? [];
|
|
95
|
-
for (const reference of references) {
|
|
96
|
-
// A reference that names a directory names the `tsconfig.json` in it.
|
|
97
|
-
const configFile = reference.path.endsWith(".json")
|
|
98
|
-
? reference.path
|
|
99
|
-
: joinPath(reference.path, "tsconfig.json");
|
|
100
|
-
let options: Parsed["options"];
|
|
101
|
-
try {
|
|
102
|
-
options = engine.parseConfigFile(configFile).options;
|
|
103
|
-
} catch {
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
if (options.composite !== true || options.noEmit === true) {
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return references;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
85
|
/** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
|
|
114
86
|
function conditionsOf(parsed: Parsed, mode: ResolutionMode | undefined): ReadonlySet<string> {
|
|
115
87
|
const resolution = parsed.options.moduleResolution;
|
|
@@ -132,7 +104,6 @@ function resolutionMode(kind: ModuleKind): ResolutionMode | undefined {
|
|
|
132
104
|
interface Reading {
|
|
133
105
|
readonly configFile: string;
|
|
134
106
|
readonly parsed: Parsed;
|
|
135
|
-
readonly references: NonNullable<Parsed["projectReferences"]>;
|
|
136
107
|
readonly entries: ModuleResolutionEntry[];
|
|
137
108
|
readonly seen: Set<string>;
|
|
138
109
|
/** Per member specifier, every file an import of it is read as. */
|
|
@@ -181,7 +152,7 @@ function configurationOf(
|
|
|
181
152
|
if (reason !== undefined) {
|
|
182
153
|
return { kind: "unread", reason };
|
|
183
154
|
}
|
|
184
|
-
const { options } = reading.parsed;
|
|
155
|
+
const { options, projectReferences: references = [] } = reading.parsed;
|
|
185
156
|
const raw: unknown = (options as Record<string, unknown>)["pathsBasePath"];
|
|
186
157
|
const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
|
|
187
158
|
const paths: Record<string, string[]> = {};
|
|
@@ -204,8 +175,8 @@ function configurationOf(
|
|
|
204
175
|
...(options.composite === true ? { composite: false, declaration: true } : {}),
|
|
205
176
|
paths,
|
|
206
177
|
},
|
|
207
|
-
...(
|
|
208
|
-
? { references:
|
|
178
|
+
...(references.length > 0
|
|
179
|
+
? { references: references.map((reference) => ({ path: reference.path })) }
|
|
209
180
|
: {}),
|
|
210
181
|
...(exclude === undefined ? {} : { exclude }),
|
|
211
182
|
});
|
|
@@ -240,6 +211,7 @@ function programOf(
|
|
|
240
211
|
reading: Reading,
|
|
241
212
|
): { readonly params: CreateSnapshotProgramParams; readonly resolver: ModuleResolver } {
|
|
242
213
|
const compilerOptions = analysisOptions(reading.parsed);
|
|
214
|
+
const references = reading.parsed.projectReferences ?? [];
|
|
243
215
|
const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
|
|
244
216
|
return {
|
|
245
217
|
resolver,
|
|
@@ -249,7 +221,7 @@ function programOf(
|
|
|
249
221
|
options: {
|
|
250
222
|
moduleResolver: resolver,
|
|
251
223
|
configFileParsingDiagnostics: reading.parsed.errors,
|
|
252
|
-
...(
|
|
224
|
+
...(references.length > 0 ? { projectReferences: references } : {}),
|
|
253
225
|
},
|
|
254
226
|
},
|
|
255
227
|
};
|
|
@@ -436,7 +408,6 @@ export function workspacePrograms(
|
|
|
436
408
|
{
|
|
437
409
|
configFile,
|
|
438
410
|
parsed,
|
|
439
|
-
references: carriedReferences(engine, parsed),
|
|
440
411
|
entries: [],
|
|
441
412
|
seen: new Set<string>(),
|
|
442
413
|
bound: new Map<string, Set<string>>(),
|