@cplieger/deadset-ts 8.0.0-dev.3 → 8.0.0-dev.4
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/analysis.js +34 -5
- package/dist/src/augmentations.js +1 -1
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/dependencies.js +22 -14
- package/dist/src/discover.js +14 -82
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +4 -2
- package/dist/src/kinds.js +1 -1
- package/dist/src/query.js +1 -0
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/roots.js +69 -41
- package/dist/src/serialization-contract.js +337 -9
- package/dist/src/verbs/analyze.js +0 -1
- package/dist/src/workflow-steps.js +32 -15
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +64 -5
- package/src/augmentations.ts +1 -1
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/dependencies.ts +27 -14
- package/src/discover.ts +14 -94
- package/src/exempt.ts +16 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +4 -2
- package/src/kinds.ts +1 -1
- package/src/query.ts +11 -0
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/roots.ts +73 -43
- package/src/serialization-contract.ts +455 -11
- package/src/sweep.ts +2 -0
- package/src/verbs/analyze.ts +0 -1
- package/src/workflow-steps.ts +35 -17
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.20261014.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
|
|
package/dist/src/analysis.js
CHANGED
|
@@ -20,7 +20,7 @@ import { enumGroup } from "./enum-group.js";
|
|
|
20
20
|
import { generatedFile, generatedFilesOf } from "./generated-file.js";
|
|
21
21
|
import { inOperandNames } from "./in-operands.js";
|
|
22
22
|
import { sourceFilesOf } from "./source-files.js";
|
|
23
|
-
import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
|
|
23
|
+
import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, writtenRecords, } from "./exempt.js";
|
|
24
24
|
import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
|
|
25
25
|
import { globExpression } from "./glob.js";
|
|
26
26
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
@@ -41,7 +41,7 @@ import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.
|
|
|
41
41
|
import { reflectiveLookup } from "./reflective-lookup.js";
|
|
42
42
|
import { references, testFileRulesOf } from "./references.js";
|
|
43
43
|
import { roots, unmatchedEverywhere, } from "./roots.js";
|
|
44
|
-
import { serializationContract } from "./serialization-contract.js";
|
|
44
|
+
import { decodedMembers, serializationContract } from "./serialization-contract.js";
|
|
45
45
|
import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
|
|
46
46
|
import { mergedGlobalInterfaces } from "./augmentations.js";
|
|
47
47
|
import { unseenLoadsOf } from "./unseen-loads.js";
|
|
@@ -534,6 +534,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
534
534
|
line: lineOf(file.originalText, warning.offset),
|
|
535
535
|
reason: warning.reason,
|
|
536
536
|
}))),
|
|
537
|
+
decoded: decodedMembers(project, projectRead.held, productionFiles(project, targetRoot, resolved.testFilePaths), consumers),
|
|
537
538
|
extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
|
|
538
539
|
};
|
|
539
540
|
});
|
|
@@ -549,11 +550,24 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
549
550
|
// Test-support code is code only test code imports in any configuration of the run: a
|
|
550
551
|
// configuration that holds no test file says nothing about what the tests import.
|
|
551
552
|
const { support, testUsed } = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
|
|
552
|
-
|
|
553
|
+
// A member a decode fills or a copy carries is written: a value decoded and sent again carries it on.
|
|
554
|
+
const written = new Set([
|
|
555
|
+
...countedWrites([...withOutside.flatMap((one) => one.references), ...read.consumers.references], support, mode),
|
|
556
|
+
...withOutside.flatMap((one) => [
|
|
557
|
+
...one.decoded.filled,
|
|
558
|
+
...one.decoded.parsed,
|
|
559
|
+
...one.decoded.copied,
|
|
560
|
+
]),
|
|
561
|
+
]);
|
|
562
|
+
const evidence = withOutside.map((one) => writtenRecords(one.evidence, written));
|
|
563
|
+
const settled = settledSatisfaction(withOutside.map((one, index) => ({
|
|
564
|
+
evidence: evidence[index] ?? [],
|
|
565
|
+
held: one.projectRead.held,
|
|
566
|
+
})), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
|
|
553
567
|
const answers = withOutside.map((one, index) => {
|
|
554
568
|
const { projectRead } = one;
|
|
555
569
|
const satisfied = settled[index];
|
|
556
|
-
const detected = heldRecords(satisfied?.evidence ??
|
|
570
|
+
const detected = heldRecords(satisfied?.evidence ?? evidence[index] ?? [], {
|
|
557
571
|
mode,
|
|
558
572
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
559
573
|
});
|
|
@@ -591,7 +605,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
591
605
|
.filter((reference) => reference.resolution === "by-name")
|
|
592
606
|
.map((reference) => reference.to),
|
|
593
607
|
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
608
|
+
...one.decoded.unanswered,
|
|
594
609
|
],
|
|
610
|
+
filledOutside: one.decoded.filled,
|
|
595
611
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
596
612
|
unmappedEntries: projectRead.rooted.unmappedEntries,
|
|
597
613
|
skipped: projectRead.skipped,
|
|
@@ -681,6 +697,13 @@ function evidenceReferences(symbols, inTestFiles) {
|
|
|
681
697
|
];
|
|
682
698
|
});
|
|
683
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* The declarations a write the mode counts stores into, a write a test-support file
|
|
702
|
+
* makes being a test's.
|
|
703
|
+
*/
|
|
704
|
+
function countedWrites(references, support, mode) {
|
|
705
|
+
return new Set(supportReferences(references, support).flatMap((reference) => reference.use === "write" && !(mode.production && reference.test) ? [reference.to] : []));
|
|
706
|
+
}
|
|
684
707
|
/**
|
|
685
708
|
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
686
709
|
* reference counting as a read: what the write-only kind would report but for a record.
|
|
@@ -765,6 +788,11 @@ function sweptOf(read, input) {
|
|
|
765
788
|
export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
|
|
766
789
|
return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
|
|
767
790
|
}
|
|
791
|
+
/** The project's own files below the target root that are no test file. */
|
|
792
|
+
function productionFiles(project, targetRoot, testFiles) {
|
|
793
|
+
const tests = new Set(testFiles);
|
|
794
|
+
return [...sourceFilesOf(project, targetRoot).byPath].flatMap(([path, file]) => tests.has(path) ? [] : [file]);
|
|
795
|
+
}
|
|
768
796
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
769
797
|
export function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
770
798
|
const targetRoot = scope.target.path;
|
|
@@ -782,7 +810,7 @@ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
782
810
|
parts: projectParts(project, read.held, references, targetRoot),
|
|
783
811
|
unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
|
|
784
812
|
}));
|
|
785
|
-
// Whether a declaration carries the marker decides its code, so one whose
|
|
813
|
+
// Whether a declaration carries the marker decides its code, so one whose answer went
|
|
786
814
|
// unread is reported by nothing.
|
|
787
815
|
const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
|
|
788
816
|
return unread.length === 0
|
|
@@ -865,6 +893,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
865
893
|
},
|
|
866
894
|
encapsulated: readManifest(host, targetRoot).declaresExports,
|
|
867
895
|
edges: readEdgeSides(host, targetRoot),
|
|
896
|
+
filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
|
|
868
897
|
},
|
|
869
898
|
selfCheck,
|
|
870
899
|
intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
|
|
@@ -10,7 +10,7 @@ import { isInterfaceDeclaration, isModuleBlock, isModuleDeclaration, } from "@ty
|
|
|
10
10
|
import { nodeKey } from "./inventory.js";
|
|
11
11
|
import { referenceKey } from "./matrix.js";
|
|
12
12
|
/** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
|
|
13
|
-
function isGlobalBlock(symbol, held, files) {
|
|
13
|
+
export function isGlobalBlock(symbol, held, files) {
|
|
14
14
|
return (symbol.kind === "namespace" &&
|
|
15
15
|
files.has(symbol.parent) &&
|
|
16
16
|
symbol.ref.endsWith("#global") &&
|
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "6.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:e2a3ba92a5a6fe81174439a944333daf8d17e52fdacb8599fc50e3adb1569773",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The uses contextual types make of the target's members, which no name node resolves to.
|
|
3
3
|
* An object-literal property writes the member of its name on each constituent of the
|
|
4
|
-
* literal's contextual type that declares one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
4
|
+
* literal's contextual type that declares one, and so does a property of a literal whose
|
|
5
|
+
* type is inferred, where a value of that type reaches such a position. A value reaching a
|
|
6
|
+
* position typed by another object type reads each of its members that type declares,
|
|
7
|
+
* through array elements and the parameters and results of functions and constructors
|
|
8
|
+
* too; a class instance there is the interface-satisfaction class's evidence instead.
|
|
8
9
|
*/
|
|
9
10
|
import { isClassDeclaration, isClassExpression, isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/ast";
|
|
10
11
|
import { SignatureKind } from "@typescript/native/sync";
|
|
@@ -164,6 +165,33 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
164
165
|
const members = [...membersOf(part).values()];
|
|
165
166
|
return members.length > 0 && members.every((member) => !member.own);
|
|
166
167
|
};
|
|
168
|
+
const literalTypes = new Map();
|
|
169
|
+
/**
|
|
170
|
+
* Whether an object type is an object literal's own, inferred where no contextual type
|
|
171
|
+
* names it: every member a property an object literal of the target writes.
|
|
172
|
+
*/
|
|
173
|
+
const literalTyped = (part) => {
|
|
174
|
+
let known = literalTypes.get(part.id);
|
|
175
|
+
if (known === undefined) {
|
|
176
|
+
const members = [...membersOf(part).values()];
|
|
177
|
+
known =
|
|
178
|
+
members.length > 0 &&
|
|
179
|
+
members.every((member) => member.ids.length === 0 &&
|
|
180
|
+
member.declarations.length > 0 &&
|
|
181
|
+
member.declarations.every((handle) => literalProperty(handle) !== undefined));
|
|
182
|
+
literalTypes.set(part.id, known);
|
|
183
|
+
}
|
|
184
|
+
return known;
|
|
185
|
+
};
|
|
186
|
+
/** The name node of the object literal property one declaration handle names, if it is one. */
|
|
187
|
+
const literalProperty = (handle) => {
|
|
188
|
+
const node = project.declarationAt(handle)?.node;
|
|
189
|
+
return node !== undefined &&
|
|
190
|
+
(isPropertyAssignment(node) || isShorthandPropertyAssignment(node)) &&
|
|
191
|
+
isObjectLiteralExpression(node.parent)
|
|
192
|
+
? node.name
|
|
193
|
+
: undefined;
|
|
194
|
+
};
|
|
167
195
|
const elements = new Map();
|
|
168
196
|
/** The element type of an array type, read once per type. */
|
|
169
197
|
const elementOf = (type) => {
|
|
@@ -246,10 +274,13 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
246
274
|
const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
|
|
247
275
|
for (const one of sources) {
|
|
248
276
|
if (carries(one)) {
|
|
249
|
-
into.push({ source: one, targets,
|
|
277
|
+
into.push({ source: one, targets, sourceKind: "members" });
|
|
250
278
|
}
|
|
251
279
|
else if (outside(one)) {
|
|
252
|
-
into.push({ source: one, targets,
|
|
280
|
+
into.push({ source: one, targets, sourceKind: "outside" });
|
|
281
|
+
}
|
|
282
|
+
else if (literalTyped(one)) {
|
|
283
|
+
into.push({ source: one, targets, sourceKind: "literal" });
|
|
253
284
|
}
|
|
254
285
|
const element = elementOf(one);
|
|
255
286
|
const called = element === undefined ? callsOf(one) : [];
|
|
@@ -290,7 +321,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
290
321
|
}
|
|
291
322
|
seen.add(type.id);
|
|
292
323
|
return partsOf(type).some((part) => {
|
|
293
|
-
if (carries(part) || outside(part)) {
|
|
324
|
+
if (carries(part) || outside(part) || literalTyped(part)) {
|
|
294
325
|
return true;
|
|
295
326
|
}
|
|
296
327
|
const element = elementOf(part);
|
|
@@ -307,6 +338,8 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
307
338
|
batches += batchesOf(sourced.length);
|
|
308
339
|
const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
|
|
309
340
|
const reads = [];
|
|
341
|
+
// A literal property writes a position's members once, however many values carry it there.
|
|
342
|
+
const carriedWrites = new Set();
|
|
310
343
|
sourced.forEach(({ flow, type }, index) => {
|
|
311
344
|
const position = positions[index];
|
|
312
345
|
if (position === undefined || !isAnswered(position)) {
|
|
@@ -315,10 +348,24 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
315
348
|
const checked = [];
|
|
316
349
|
compared(type, position, checked, new Set());
|
|
317
350
|
const ids = new Set();
|
|
318
|
-
for (const { source, targets,
|
|
351
|
+
for (const { source, targets, sourceKind } of checked) {
|
|
319
352
|
const tables = targets.map(membersOf);
|
|
320
353
|
const sourceMembers = membersOf(source);
|
|
321
|
-
if (
|
|
354
|
+
if (sourceKind === "literal") {
|
|
355
|
+
for (const [name, member] of sourceMembers) {
|
|
356
|
+
const written = [...new Set(tables.flatMap((table) => table.get(name)?.ids ?? []))];
|
|
357
|
+
for (const at of written.length === 0 ? [] : member.declarations) {
|
|
358
|
+
const node = literalProperty(at);
|
|
359
|
+
const key = `${at.path}#${String(at.index)}>${written.join(",")}`;
|
|
360
|
+
if (node !== undefined && !carriedWrites.has(key)) {
|
|
361
|
+
carriedWrites.add(key);
|
|
362
|
+
writes.push({ at: node, from: flow.from, ids: written });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (sourceKind === "outside") {
|
|
322
369
|
for (const table of tables) {
|
|
323
370
|
for (const [name, member] of table) {
|
|
324
371
|
if (sourceMembers.has(name)) {
|
package/dist/src/dependencies.js
CHANGED
|
@@ -14,7 +14,7 @@ import { scriptTokens } from "./manifest.js";
|
|
|
14
14
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
15
15
|
import { UNANSWERED } from "./query.js";
|
|
16
16
|
import { sourceFilesOf } from "./source-files.js";
|
|
17
|
-
import { workflowTokens } from "./workflow-steps.js";
|
|
17
|
+
import { workflowCommands, workflowTokens } from "./workflow-steps.js";
|
|
18
18
|
const MANIFEST = "package.json";
|
|
19
19
|
const MODULES_DIR = "node_modules";
|
|
20
20
|
const MODULES_SEGMENT = `/${MODULES_DIR}/`;
|
|
@@ -697,20 +697,27 @@ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
|
697
697
|
/** The commands one script string runs, split at the shell's list and pipeline operators. */
|
|
698
698
|
const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
|
|
699
699
|
/**
|
|
700
|
-
* The
|
|
701
|
-
*
|
|
702
|
-
*
|
|
700
|
+
* The command strings one manifest runs: each of its `scripts`, and the `exec` command of
|
|
701
|
+
* nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
|
|
702
|
+
* it, which nodemon runs the watched script with:
|
|
703
|
+
* https://github.com/remy/nodemon#config-files
|
|
703
704
|
*/
|
|
704
|
-
function
|
|
705
|
+
function manifestCommands(host, dir, manifest) {
|
|
705
706
|
const scripts = manifest["scripts"];
|
|
707
|
+
const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
|
|
708
|
+
return [
|
|
709
|
+
...(isRecord(scripts) ? Object.values(scripts) : []),
|
|
710
|
+
...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
|
|
711
|
+
].filter((command) => typeof command === "string");
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* The packages `scripts` preload by a runtime flag's bare module specifier, a flag
|
|
715
|
+
* counting only in a command whose program passes it to the runtime, and for Node only
|
|
716
|
+
* before the script it runs, whose own arguments come after it.
|
|
717
|
+
*/
|
|
718
|
+
function preloadsOf(scripts) {
|
|
706
719
|
const found = new Set();
|
|
707
|
-
|
|
708
|
-
return found;
|
|
709
|
-
}
|
|
710
|
-
for (const script of Object.values(scripts)) {
|
|
711
|
-
if (typeof script !== "string") {
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
720
|
+
for (const script of scripts) {
|
|
714
721
|
for (const command of script.split(COMMAND_SEPARATORS)) {
|
|
715
722
|
const tokens = scriptTokens(command);
|
|
716
723
|
let at = 0;
|
|
@@ -820,6 +827,7 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
820
827
|
}
|
|
821
828
|
}
|
|
822
829
|
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
830
|
+
const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
|
|
823
831
|
const declared = [];
|
|
824
832
|
const ran = new Set();
|
|
825
833
|
const peers = new Map();
|
|
@@ -830,14 +838,14 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
830
838
|
continue;
|
|
831
839
|
}
|
|
832
840
|
const tokens = scriptTokensOf(read.value);
|
|
833
|
-
const preloads =
|
|
841
|
+
const preloads = preloadsOf(manifestCommands(host, dir, read.value));
|
|
834
842
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
835
843
|
const found = installedOf(host, dir, dependency.name);
|
|
836
844
|
if (found === undefined && uninstalledHere.has(dir)) {
|
|
837
845
|
continue;
|
|
838
846
|
}
|
|
839
847
|
declared.push(dependency);
|
|
840
|
-
if (preloads.has(dependency.name)) {
|
|
848
|
+
if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
|
|
841
849
|
ran.add(declarationKey(dependency));
|
|
842
850
|
}
|
|
843
851
|
if (found === undefined) {
|
package/dist/src/discover.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DiagnosticCategory } from "@typescript/native/sync";
|
|
2
2
|
import { baseUrlPaths } from "./base-url.js";
|
|
3
|
-
import { dirnamePath,
|
|
3
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
4
4
|
import { SetupError } from "./setup-failure.js";
|
|
5
5
|
import { writtenPath } from "./workspace-programs.js";
|
|
6
6
|
/** Directory names discovery does not descend into. */
|
|
@@ -224,47 +224,6 @@ export function consumerProjects(engine, host, root) {
|
|
|
224
224
|
function derivedId(targetRoot, configFile) {
|
|
225
225
|
return relativePath(targetRoot, configFile) ?? configFile;
|
|
226
226
|
}
|
|
227
|
-
/**
|
|
228
|
-
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
229
|
-
* passed. The parse reports it and returns, and a snapshot opened from it never answers:
|
|
230
|
-
* https://github.com/microsoft/TypeScript/issues/64754
|
|
231
|
-
*/
|
|
232
|
-
const CIRCULAR_EXTENDS = 18000;
|
|
233
|
-
/** The configuration files one parsed configuration's own `extends` names by path. */
|
|
234
|
-
function extendedPaths(parsed, configFile) {
|
|
235
|
-
const { raw } = parsed;
|
|
236
|
-
const named = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
|
|
237
|
-
return (Array.isArray(named) ? named : [named])
|
|
238
|
-
.filter((one) => typeof one === "string")
|
|
239
|
-
.filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
|
|
240
|
-
.map((one) => resolvePath(dirnamePath(configFile), one));
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* The `extends` chain from `configFile` to the first configuration it reaches twice, each
|
|
244
|
-
* link by path, or undefined where no chain written by path returns.
|
|
245
|
-
*/
|
|
246
|
-
function extendsCycle(engine, configFile, parsed) {
|
|
247
|
-
const walk = (chain, at) => {
|
|
248
|
-
for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
|
|
249
|
-
if (chain.includes(next)) {
|
|
250
|
-
return [...chain, next];
|
|
251
|
-
}
|
|
252
|
-
let read;
|
|
253
|
-
try {
|
|
254
|
-
read = engine.parseConfigFile(next);
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const cycle = walk([...chain, next], read);
|
|
260
|
-
if (cycle !== undefined) {
|
|
261
|
-
return cycle;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return undefined;
|
|
265
|
-
};
|
|
266
|
-
return walk([configFile], parsed);
|
|
267
|
-
}
|
|
268
227
|
/** The configuration files one parsed configuration's `references` name. */
|
|
269
228
|
function referencesOf(parsed) {
|
|
270
229
|
// The parse answers a directory reference as the directory; the compiler reads a
|
|
@@ -272,53 +231,26 @@ function referencesOf(parsed) {
|
|
|
272
231
|
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
|
|
273
232
|
return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
|
|
274
233
|
}
|
|
275
|
-
/** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
|
|
276
|
-
function circularity(engine, configFile, parsed) {
|
|
277
|
-
const chain = extendsCycle(engine, configFile, parsed);
|
|
278
|
-
if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
|
|
282
|
-
}
|
|
283
234
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
235
|
+
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
236
|
+
* passed. A snapshot that opens two configurations whose `extends` chains reach one such
|
|
237
|
+
* cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
|
|
238
|
+
* so the analysis refuses one before any snapshot is asked to open it.
|
|
287
239
|
*/
|
|
288
|
-
|
|
289
|
-
for (const reference of references) {
|
|
290
|
-
if (visited.has(reference)) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
visited.add(reference);
|
|
294
|
-
let parsed;
|
|
295
|
-
try {
|
|
296
|
-
parsed = engine.parseConfigFile(reference);
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
const reason = circularity(engine, reference, parsed) ??
|
|
302
|
-
referencedCircularity(engine, referencesOf(parsed), visited);
|
|
303
|
-
if (reason !== undefined) {
|
|
304
|
-
return reason;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return undefined;
|
|
308
|
-
}
|
|
240
|
+
const CIRCULAR_EXTENDS = 18000;
|
|
309
241
|
/**
|
|
310
|
-
* The
|
|
311
|
-
*
|
|
312
|
-
* configuration it references is circular, which the analysis refuses before any
|
|
313
|
-
* snapshot is asked to open it.
|
|
242
|
+
* The configurations one configuration file's `references` name, or why it cannot be read:
|
|
243
|
+
* the compiler throws, or its parse reports the `extends` chain circular.
|
|
314
244
|
*/
|
|
315
245
|
function readConfiguration(engine, configFile) {
|
|
316
246
|
try {
|
|
317
247
|
const parsed = engine.parseConfigFile(configFile);
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
248
|
+
const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
|
|
249
|
+
// The compiler leaves the message's `{0}` unfilled:
|
|
250
|
+
// https://github.com/microsoft/TypeScript/pull/64755
|
|
251
|
+
return circular === undefined
|
|
252
|
+
? { references: referencesOf(parsed) }
|
|
253
|
+
: { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
|
|
322
254
|
}
|
|
323
255
|
catch (error) {
|
|
324
256
|
return { reason: error instanceof Error ? error.message : String(error) };
|
package/dist/src/exempt.js
CHANGED
|
@@ -106,11 +106,19 @@ export function evidenceRecords(input, detectors, disabled) {
|
|
|
106
106
|
site: evidence.site,
|
|
107
107
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
108
108
|
...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
|
|
109
|
+
...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
return found;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The records that hold given the declarations a write the mode counts stores into: one
|
|
117
|
+
* that holds only while written holds where its declaration is among them.
|
|
118
|
+
*/
|
|
119
|
+
export function writtenRecords(records, written) {
|
|
120
|
+
return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
|
|
121
|
+
}
|
|
114
122
|
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
115
123
|
export function heldRecords(records, holding) {
|
|
116
124
|
const found = [];
|
|
@@ -87,10 +87,12 @@ function testedByIn(input) {
|
|
|
87
87
|
* published API, a public member of a published type included, is `certain` where the
|
|
88
88
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
89
89
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
90
|
-
* consumer set. A member an `in` expression tests by name
|
|
90
|
+
* consumer set. A member an `in` expression tests by name, and one a function outside the
|
|
91
|
+
* analysis fills, is `possible`.
|
|
91
92
|
*/
|
|
92
93
|
function reachabilityClasses(input) {
|
|
93
94
|
const tested = testedByIn(input);
|
|
95
|
+
const filled = input.boundary.filledOutside ?? new Set();
|
|
94
96
|
const published = publishedIds(input.swept.matrix.union);
|
|
95
97
|
const { declared } = input.boundary.consumers;
|
|
96
98
|
const outside = declared.length === 0
|
|
@@ -99,7 +101,7 @@ function reachabilityClasses(input) {
|
|
|
99
101
|
? "certain"
|
|
100
102
|
: "probable";
|
|
101
103
|
return (id) => {
|
|
102
|
-
if (tested.has(id)) {
|
|
104
|
+
if (tested.has(id) || filled.has(id)) {
|
|
103
105
|
return "possible";
|
|
104
106
|
}
|
|
105
107
|
return published.has(id) ? outside : "certain";
|
package/dist/src/kinds.js
CHANGED
|
@@ -153,7 +153,7 @@ const ROWS = [
|
|
|
153
153
|
code: "DS1301",
|
|
154
154
|
name: "write-only-symbol",
|
|
155
155
|
languages: ["go", "ts"],
|
|
156
|
-
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
156
|
+
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A literal whose type the compiler infers, as one a callback returns to a generic call, writes in the same way the member of each property's name that each object type a value of the literal's type reaches declares, through array elements, type arguments and the parameters and results of functions, so `ids.map((id) => ({ id }))` returned as `Row[]` writes `Row.id`. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
157
157
|
defaultSeverity: "deny",
|
|
158
158
|
maxClass: "certain",
|
|
159
159
|
fixability: "deletable",
|
package/dist/src/query.js
CHANGED
|
@@ -220,6 +220,7 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
|
|
|
220
220
|
symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
|
|
221
221
|
signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
|
|
222
222
|
resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
|
|
223
|
+
resolvedSignatures: (nodes, cap = DEFAULT_BATCH_CAP) => each("getResolvedSignature", nodes, cap, nodeLocation, (node) => checker.getResolvedSignature.gen(node), (node) => checker.getResolvedSignature(node)),
|
|
223
224
|
parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
|
|
224
225
|
genericOf: (signature) => signature.target === undefined
|
|
225
226
|
? undefined
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { fails } from "../config.js";
|
|
2
1
|
import { coalesced } from "../json-chunks.js";
|
|
3
2
|
import { KINDS } from "../kinds.js";
|
|
4
3
|
/** The escapes a workflow command's message needs, so a line break does not end the command. */
|
|
@@ -19,6 +18,12 @@ function escapeProperty(value) {
|
|
|
19
18
|
function kindName(code) {
|
|
20
19
|
return KINDS.get(code)?.name ?? code;
|
|
21
20
|
}
|
|
21
|
+
/** The command each finding severity is annotated with, whatever the failing severity. */
|
|
22
|
+
const LEVELS = {
|
|
23
|
+
deny: "error",
|
|
24
|
+
warn: "warning",
|
|
25
|
+
allow: "notice",
|
|
26
|
+
};
|
|
22
27
|
function command(level, at, endLine, title, message) {
|
|
23
28
|
return `::${level} file=${escapeProperty(at.path)},line=${String(at.line)},col=${String(at.column)},endLine=${String(endLine)},title=${escapeProperty(title)}::${escapeData(message)}`;
|
|
24
29
|
}
|
|
@@ -26,16 +31,15 @@ function command(level, at, endLine, title, message) {
|
|
|
26
31
|
const SKIP_TITLE = "type error skipped";
|
|
27
32
|
const SKIP_MESSAGE = "the analysis did not evaluate the function or statement holding this type error: ";
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* is fixed on at the failing severity.
|
|
34
|
+
* The annotations `contract/grammar/annotations.md` states: one per finding, at the
|
|
35
|
+
* command its severity names, and one error per stale suppression, each naming its file,
|
|
36
|
+
* line, column and last line and titled by its code and kind; then one warning per
|
|
37
|
+
* type-error skip naming its file and line, and one per note naming its directory.
|
|
34
38
|
*/
|
|
35
|
-
export function annotations(report
|
|
39
|
+
export function annotations(report) {
|
|
36
40
|
return coalesced((function* lines() {
|
|
37
41
|
for (const found of report.findings) {
|
|
38
|
-
yield `${command(
|
|
42
|
+
yield `${command(LEVELS[found.severity], found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
|
|
39
43
|
}
|
|
40
44
|
for (const stale of report.stale_suppressions) {
|
|
41
45
|
yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
|
|
@@ -43,5 +47,8 @@ export function annotations(report, options) {
|
|
|
43
47
|
for (const skip of report.type_error_skips) {
|
|
44
48
|
yield `::warning file=${escapeProperty(skip.path)},line=${String(skip.line)},title=${escapeProperty(SKIP_TITLE)}::${escapeData(SKIP_MESSAGE + skip.message)}\n`;
|
|
45
49
|
}
|
|
50
|
+
for (const note of report.notes) {
|
|
51
|
+
yield `::warning file=${escapeProperty(note.path)},title=${escapeProperty(note.kind)}::${escapeData(note.message)}\n`;
|
|
52
|
+
}
|
|
46
53
|
})());
|
|
47
54
|
}
|