@cplieger/deadset-ts 5.2.2 → 6.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 +1 -1
- package/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +47 -12
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/jsr.json +1 -1
- package/package.json +8 -2
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +71 -12
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +4 -1
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
- package/src/workspace-run.ts +3 -4
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
isBinaryExpression,
|
|
13
13
|
isBlock,
|
|
14
14
|
isBreakStatement,
|
|
15
|
+
isBindingElement,
|
|
15
16
|
isCallExpression,
|
|
16
17
|
isCaseClause,
|
|
18
|
+
isCatchClause,
|
|
17
19
|
isClassDeclaration,
|
|
18
20
|
isClassExpression,
|
|
19
21
|
isContinueStatement,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
isExportSpecifier,
|
|
25
27
|
isExpressionStatement,
|
|
26
28
|
isFunctionDeclaration,
|
|
29
|
+
isFunctionExpression,
|
|
27
30
|
isHeritageClause,
|
|
28
31
|
isIdentifier,
|
|
29
32
|
isIfStatement,
|
|
@@ -35,6 +38,8 @@ import {
|
|
|
35
38
|
isNamespaceImport,
|
|
36
39
|
isNoSubstitutionTemplateLiteral,
|
|
37
40
|
isNumericLiteral,
|
|
41
|
+
isObjectBindingPattern,
|
|
42
|
+
isObjectLiteralExpression,
|
|
38
43
|
isParenthesizedExpression,
|
|
39
44
|
isPrefixUnaryExpression,
|
|
40
45
|
isPropertyAccessExpression,
|
|
@@ -50,8 +55,10 @@ import {
|
|
|
50
55
|
isVoidExpression,
|
|
51
56
|
NodeFlags,
|
|
52
57
|
SyntaxKind,
|
|
58
|
+
type BindingName,
|
|
53
59
|
type Block,
|
|
54
60
|
type CallExpression,
|
|
61
|
+
type CatchClause,
|
|
55
62
|
type Identifier,
|
|
56
63
|
type Node,
|
|
57
64
|
type SourceFile,
|
|
@@ -81,6 +88,16 @@ export interface PartFact {
|
|
|
81
88
|
readonly message: string;
|
|
82
89
|
/** Whether the project found the part in use, which takes it out of the answer. */
|
|
83
90
|
readonly used: boolean;
|
|
91
|
+
/** Where the part is a parameter's name, what a function used as a value decides it by. */
|
|
92
|
+
readonly parameter?: ParameterPlace;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What decides a parameter of a function used as a value: a caller may pass it more arguments. */
|
|
96
|
+
interface ParameterPlace {
|
|
97
|
+
/** Whether a parameter after this one, which is no binding-pattern name, is read. */
|
|
98
|
+
readonly beforeARead: boolean;
|
|
99
|
+
/** Whether no declaration of the inventory declares the function, which is then a value. */
|
|
100
|
+
readonly inValue: boolean;
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
/** What one declaration's signature answers to beside its body. */
|
|
@@ -119,9 +136,6 @@ const NO_VALUE: ReadonlySet<string> = new Set([
|
|
|
119
136
|
"Promise<never>",
|
|
120
137
|
]);
|
|
121
138
|
|
|
122
|
-
/** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
|
|
123
|
-
const UNUSED_PREFIX = "_";
|
|
124
|
-
|
|
125
139
|
/** The rendered position of one node, from its first token to its last line. */
|
|
126
140
|
function spanOf(file: SourceFile, root: string, node: Node): FindingPosition {
|
|
127
141
|
const start = renderPosition(file, root, node.getStart());
|
|
@@ -410,9 +424,31 @@ function readsBinding(
|
|
|
410
424
|
return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
|
|
411
425
|
}
|
|
412
426
|
|
|
427
|
+
/** A function written as a value that no inventory declaration declares, and what holds it. */
|
|
428
|
+
interface ValueFunction {
|
|
429
|
+
readonly holder: string;
|
|
430
|
+
readonly signature: SignatureNode;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** One name a parameter binds, whole or through a binding pattern. */
|
|
434
|
+
interface ParameterName {
|
|
435
|
+
readonly name: Identifier;
|
|
436
|
+
/** The parameter's index in its list. */
|
|
437
|
+
readonly at: number;
|
|
438
|
+
readonly pattern: boolean;
|
|
439
|
+
/** A name beside a rest element of its object pattern: removing it changes what the rest holds. */
|
|
440
|
+
readonly exempt: boolean;
|
|
441
|
+
}
|
|
442
|
+
|
|
413
443
|
/** What one file's walk collects before the batch that resolves its names. */
|
|
414
444
|
interface FileWalk {
|
|
415
445
|
readonly declared: Declared[];
|
|
446
|
+
readonly values: ValueFunction[];
|
|
447
|
+
readonly catches: {
|
|
448
|
+
readonly clause: CatchClause;
|
|
449
|
+
readonly name: Identifier;
|
|
450
|
+
readonly holder: string;
|
|
451
|
+
}[];
|
|
416
452
|
readonly blocks: Block[];
|
|
417
453
|
readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
|
|
418
454
|
readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
|
|
@@ -443,6 +479,8 @@ export function projectParts<Brand>(
|
|
|
443
479
|
const id = held.declarations.get(nodeKey(file, node));
|
|
444
480
|
return id === undefined || kinds.get(id) === "file" ? undefined : id;
|
|
445
481
|
};
|
|
482
|
+
// A part no declaration holds belongs to the file, which is judged while the file is live.
|
|
483
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
446
484
|
const holderOf = (node: Node): string | undefined => {
|
|
447
485
|
for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
448
486
|
const id = declarationAt(at);
|
|
@@ -450,9 +488,17 @@ export function projectParts<Brand>(
|
|
|
450
488
|
return id;
|
|
451
489
|
}
|
|
452
490
|
}
|
|
453
|
-
return
|
|
491
|
+
return fileId;
|
|
454
492
|
};
|
|
455
|
-
const walk: FileWalk = {
|
|
493
|
+
const walk: FileWalk = {
|
|
494
|
+
declared: [],
|
|
495
|
+
values: [],
|
|
496
|
+
catches: [],
|
|
497
|
+
blocks: [],
|
|
498
|
+
unreachable: [],
|
|
499
|
+
cases: [],
|
|
500
|
+
};
|
|
501
|
+
const covered = new Set<Node>();
|
|
456
502
|
const position = (node: Node): string =>
|
|
457
503
|
positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
458
504
|
|
|
@@ -483,6 +529,19 @@ export function projectParts<Brand>(
|
|
|
483
529
|
const signature = id === undefined ? undefined : signatureOf(node);
|
|
484
530
|
if (id !== undefined && signature !== undefined) {
|
|
485
531
|
walk.declared.push({ id, node, signature });
|
|
532
|
+
covered.add(signature);
|
|
533
|
+
}
|
|
534
|
+
const value = covered.has(node) ? undefined : valueSignature(node);
|
|
535
|
+
const valueHolder = id ?? holderOf(node);
|
|
536
|
+
if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
|
|
537
|
+
walk.values.push({ holder: valueHolder, signature: value });
|
|
538
|
+
}
|
|
539
|
+
if (isCatchClause(node) && node.variableDeclaration !== undefined) {
|
|
540
|
+
const name = node.variableDeclaration.name;
|
|
541
|
+
const holder = holderOf(node);
|
|
542
|
+
if (isIdentifier(name) && holder !== undefined && silenced === 0) {
|
|
543
|
+
walk.catches.push({ clause: node, name, holder });
|
|
544
|
+
}
|
|
486
545
|
}
|
|
487
546
|
if (isCallExpression(node)) {
|
|
488
547
|
const name = calleeName(node);
|
|
@@ -532,16 +591,24 @@ export function projectParts<Brand>(
|
|
|
532
591
|
|
|
533
592
|
// One batch per file resolves every name a parameter or a local is read through.
|
|
534
593
|
const asked: Identifier[] = [];
|
|
535
|
-
const readers = new Map<
|
|
536
|
-
for (const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
594
|
+
const readers = new Map<SignatureNode, { names: ParameterName[]; uses: Identifier[] }>();
|
|
595
|
+
for (const signature of [
|
|
596
|
+
...walk.declared.map((one) => one.signature),
|
|
597
|
+
...walk.values.map((one) => one.signature),
|
|
598
|
+
]) {
|
|
599
|
+
const names = parameterNames(signature);
|
|
600
|
+
const declaredNames = new Set<Node>(names.map((one) => one.name));
|
|
601
|
+
const spelled = new Set(names.map((one) => one.name.text));
|
|
602
|
+
const uses = identifiersNaming(signature, spelled).filter((use) => !declaredNames.has(use));
|
|
603
|
+
readers.set(signature, { names, uses });
|
|
604
|
+
asked.push(...names.map((one) => one.name), ...uses);
|
|
544
605
|
}
|
|
606
|
+
const caught = walk.catches.map(({ clause, name }) =>
|
|
607
|
+
identifiersNaming(clause.block, new Set([name.text])),
|
|
608
|
+
);
|
|
609
|
+
walk.catches.forEach(({ name }, at) => {
|
|
610
|
+
asked.push(name, ...(caught[at] ?? []));
|
|
611
|
+
});
|
|
545
612
|
const stores = walk.blocks.map((block) => localsOf(block));
|
|
546
613
|
for (const store of stores) {
|
|
547
614
|
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
@@ -555,9 +622,30 @@ export function projectParts<Brand>(
|
|
|
555
622
|
stub: isStub(one.signature),
|
|
556
623
|
fixed: hierarchyFixes(one.node, extended),
|
|
557
624
|
});
|
|
558
|
-
const
|
|
559
|
-
|
|
625
|
+
const reader = readers.get(one.signature);
|
|
626
|
+
if (reader !== undefined) {
|
|
627
|
+
parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
for (const one of walk.values) {
|
|
631
|
+
const reader = readers.get(one.signature);
|
|
632
|
+
if (reader !== undefined) {
|
|
633
|
+
parts.push(
|
|
634
|
+
...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
560
637
|
}
|
|
638
|
+
walk.catches.forEach(({ name, holder }, at) => {
|
|
639
|
+
parts.push({
|
|
640
|
+
code: DEAD_STORE,
|
|
641
|
+
declaration: holder,
|
|
642
|
+
kind: "store",
|
|
643
|
+
name: name.text,
|
|
644
|
+
position: spanOf(file, targetRoot, name),
|
|
645
|
+
message: `value written to ${name.text} is never read`,
|
|
646
|
+
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
647
|
+
});
|
|
648
|
+
});
|
|
561
649
|
walk.blocks.forEach((block, at) => {
|
|
562
650
|
const holder = holderOf(block);
|
|
563
651
|
if (holder !== undefined) {
|
|
@@ -592,41 +680,107 @@ export function projectParts<Brand>(
|
|
|
592
680
|
return { parts, signatures, calls, specifiers };
|
|
593
681
|
}
|
|
594
682
|
|
|
595
|
-
/**
|
|
683
|
+
/**
|
|
684
|
+
* A function written in place that no declaration declares: an arrow, a function
|
|
685
|
+
* expression, a nested function declaration, or a method of an object literal.
|
|
686
|
+
*/
|
|
687
|
+
function valueSignature(node: Node): SignatureNode | undefined {
|
|
688
|
+
if (isArrowFunction(node) || isFunctionExpression(node)) {
|
|
689
|
+
return node;
|
|
690
|
+
}
|
|
691
|
+
if (isFunctionDeclaration(node)) {
|
|
692
|
+
return node.body === undefined ? undefined : node;
|
|
693
|
+
}
|
|
694
|
+
return isMethodDeclaration(node) &&
|
|
695
|
+
node.body !== undefined &&
|
|
696
|
+
isObjectLiteralExpression(node.parent)
|
|
697
|
+
? node
|
|
698
|
+
: undefined;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Every name one function's parameters bind, a binding pattern's names one by one. A
|
|
703
|
+
* `this` parameter receives no argument and a decorated one is read by its decorator, so
|
|
704
|
+
* neither binds a subject.
|
|
705
|
+
*/
|
|
706
|
+
function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
707
|
+
const found: ParameterName[] = [];
|
|
708
|
+
const patternNames = (pattern: BindingName, at: number): void => {
|
|
709
|
+
if (isIdentifier(pattern)) {
|
|
710
|
+
found.push({ name: pattern, at, pattern: true, exempt: false });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const rest =
|
|
714
|
+
isObjectBindingPattern(pattern) &&
|
|
715
|
+
pattern.elements.some((element) => element.dotDotDotToken !== undefined);
|
|
716
|
+
for (const element of pattern.elements) {
|
|
717
|
+
if (!isBindingElement(element) || element.name === undefined) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (isIdentifier(element.name)) {
|
|
721
|
+
const exempt = rest && element.dotDotDotToken === undefined;
|
|
722
|
+
found.push({ name: element.name, at, pattern: true, exempt });
|
|
723
|
+
} else {
|
|
724
|
+
patternNames(element.name, at);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
signature.parameters.forEach((parameter, at) => {
|
|
729
|
+
const name = parameter.name;
|
|
730
|
+
if (
|
|
731
|
+
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
|
|
732
|
+
) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (isIdentifier(name)) {
|
|
736
|
+
if (name.text !== "this") {
|
|
737
|
+
found.push({ name, at, pattern: false, exempt: false });
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
patternNames(name, at);
|
|
742
|
+
});
|
|
743
|
+
return found;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Each name one function's parameters bind and whether the function reads it. A binding
|
|
748
|
+
* pattern counts as read where its function reads any name it binds.
|
|
749
|
+
*/
|
|
596
750
|
function parameterParts(
|
|
597
751
|
file: SourceFile,
|
|
598
752
|
root: string,
|
|
599
|
-
|
|
600
|
-
|
|
753
|
+
declaration: string,
|
|
754
|
+
signature: SignatureNode,
|
|
755
|
+
reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
|
|
601
756
|
ids: ReadonlyMap<Node, Answer<number>>,
|
|
757
|
+
inValue: boolean,
|
|
602
758
|
): PartFact[] {
|
|
603
|
-
const signature = one.signature;
|
|
604
759
|
// A function that reads `arguments` reads every parameter by position.
|
|
605
760
|
const readsArguments =
|
|
606
761
|
!isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
|
|
762
|
+
const read = reader.names.map(
|
|
763
|
+
({ name }) =>
|
|
764
|
+
readsArguments ||
|
|
765
|
+
reader.uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
766
|
+
);
|
|
767
|
+
const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
|
|
607
768
|
const found: PartFact[] = [];
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
!isIdentifier(name) ||
|
|
612
|
-
name.text === "this" ||
|
|
613
|
-
name.text.startsWith(UNUSED_PREFIX) ||
|
|
614
|
-
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
|
|
615
|
-
) {
|
|
616
|
-
continue;
|
|
769
|
+
reader.names.forEach(({ name, at, pattern, exempt }, index) => {
|
|
770
|
+
if (exempt) {
|
|
771
|
+
return;
|
|
617
772
|
}
|
|
618
773
|
found.push({
|
|
619
774
|
code: UNUSED_PARAMETER,
|
|
620
|
-
declaration
|
|
775
|
+
declaration,
|
|
621
776
|
kind: "parameter",
|
|
622
777
|
name: name.text,
|
|
623
778
|
position: spanOf(file, root, name),
|
|
624
779
|
message: `parameter ${name.text} is never read in the body`,
|
|
625
|
-
used:
|
|
626
|
-
|
|
627
|
-
uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
780
|
+
used: read[index] === true,
|
|
781
|
+
parameter: { beforeARead: !pattern && at < lastRead, inValue },
|
|
628
782
|
});
|
|
629
|
-
}
|
|
783
|
+
});
|
|
630
784
|
return found;
|
|
631
785
|
}
|
|
632
786
|
|
|
@@ -823,6 +977,8 @@ export interface IntraFunctionFacts {
|
|
|
823
977
|
readonly parts: readonly PartFact[];
|
|
824
978
|
/** The declarations whose signature is free to change. */
|
|
825
979
|
readonly free: ReadonlySet<string>;
|
|
980
|
+
/** The declarations whose signature is free but for a use as a value. */
|
|
981
|
+
readonly freeButValued: ReadonlySet<string>;
|
|
826
982
|
/**
|
|
827
983
|
* The declarations every reference of which is a call in the loaded program that
|
|
828
984
|
* discards the call's value, with at least one such call.
|
|
@@ -882,8 +1038,13 @@ export function intraFunctionFacts(
|
|
|
882
1038
|
.map((one) => one.declaration)
|
|
883
1039
|
.filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)),
|
|
884
1040
|
);
|
|
1041
|
+
const freeButValued = new Set(
|
|
1042
|
+
signatures
|
|
1043
|
+
.map((one) => one.declaration)
|
|
1044
|
+
.filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
|
|
1045
|
+
);
|
|
885
1046
|
const discardedEverywhere = new Set(
|
|
886
1047
|
[...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id),
|
|
887
1048
|
);
|
|
888
|
-
return { parts: [...parts.values()], free, discardedEverywhere };
|
|
1049
|
+
return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
|
|
889
1050
|
}
|
package/src/inventory.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isPropertyDeclaration,
|
|
21
21
|
isPropertySignatureDeclaration,
|
|
22
22
|
isSetAccessorDeclaration,
|
|
23
|
+
isStringLiteral,
|
|
23
24
|
isTypeAliasDeclaration,
|
|
24
25
|
isTypeLiteralNode,
|
|
25
26
|
isUnionTypeNode,
|
|
@@ -178,6 +179,8 @@ export interface Inventory {
|
|
|
178
179
|
* component, its declared name where that differs, and a type parameter's own name.
|
|
179
180
|
*/
|
|
180
181
|
readonly byName: ReadonlyMap<string, readonly string[]>;
|
|
182
|
+
/** Each `declare module` declaration whose name is a string literal, by id, to that name. */
|
|
183
|
+
readonly modules: ReadonlyMap<string, string>;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
/** A declaration under construction, before its reference is rendered. */
|
|
@@ -532,6 +535,7 @@ export function inventory<Brand>(
|
|
|
532
535
|
const exportScopes: ExportScope[] = [];
|
|
533
536
|
const containers: Container[] = [];
|
|
534
537
|
const starReExports: string[] = [];
|
|
538
|
+
const modules: (readonly [Building, string])[] = [];
|
|
535
539
|
let outsideOwnFiles = 0;
|
|
536
540
|
|
|
537
541
|
const keep = (record: Building): Building => {
|
|
@@ -737,6 +741,9 @@ export function inventory<Brand>(
|
|
|
737
741
|
localName: undefined,
|
|
738
742
|
});
|
|
739
743
|
exportScopes.push({ owner: record, nameNode: node.name });
|
|
744
|
+
if (isStringLiteral(node.name)) {
|
|
745
|
+
modules.push([record, node.name.text]);
|
|
746
|
+
}
|
|
740
747
|
const body = node.body;
|
|
741
748
|
if (body === undefined) {
|
|
742
749
|
return;
|
|
@@ -1022,6 +1029,7 @@ export function inventory<Brand>(
|
|
|
1022
1029
|
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
1023
1030
|
.map((symbol) => symbol.id),
|
|
1024
1031
|
byName: namesOf(building),
|
|
1032
|
+
modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
|
|
1025
1033
|
};
|
|
1026
1034
|
}
|
|
1027
1035
|
|
package/src/kinds.ts
CHANGED
|
@@ -101,9 +101,9 @@ const ROWS: readonly KindRow[] = [
|
|
|
101
101
|
code: "DS1101",
|
|
102
102
|
name: "unnecessary-export",
|
|
103
103
|
languages: ["go", "ts"],
|
|
104
|
-
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method
|
|
104
|
+
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
|
|
105
105
|
precondition:
|
|
106
|
-
"Closed world only.
|
|
106
|
+
"Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
|
|
107
107
|
defaultSeverity: "warn",
|
|
108
108
|
maxClass: "certain",
|
|
109
109
|
fixability: "narrowable",
|
|
@@ -113,7 +113,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
113
113
|
code: "DS1102",
|
|
114
114
|
name: "unnecessary-exposure",
|
|
115
115
|
languages: ["go"],
|
|
116
|
-
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method
|
|
116
|
+
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
|
|
117
117
|
precondition:
|
|
118
118
|
"Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
|
|
119
119
|
defaultSeverity: "warn",
|
|
@@ -137,7 +137,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
137
137
|
languages: ["ts"],
|
|
138
138
|
rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
|
|
139
139
|
precondition:
|
|
140
|
-
"Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file
|
|
140
|
+
"Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
|
|
141
141
|
defaultSeverity: "warn",
|
|
142
142
|
maxClass: "certain",
|
|
143
143
|
fixability: "narrowable",
|
|
@@ -157,9 +157,9 @@ const ROWS: readonly KindRow[] = [
|
|
|
157
157
|
code: "DS1203",
|
|
158
158
|
name: "uncalled-interface-method",
|
|
159
159
|
languages: ["go", "ts"],
|
|
160
|
-
rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions.",
|
|
160
|
+
rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions. Each method that implements it and that nothing else keeps is reported under the code its own references select. Such a method counts as referenced by the interface method alone, so it belongs to this finding's dead component, whose root member is the interface method.",
|
|
161
161
|
precondition:
|
|
162
|
-
"
|
|
162
|
+
"In Go, every method of an interface that declares an unexported method is exempt. That is the sum-type shape, whose method set exists to restrict the implementors, and a TypeScript interface cannot take it because it declares no member less visible than itself. In both languages, every marker method is exempt, an interface method whose every implementation carries an empty body.",
|
|
163
163
|
defaultSeverity: "warn",
|
|
164
164
|
maxClass: "certain",
|
|
165
165
|
fixability: "manual",
|
|
@@ -213,7 +213,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
213
213
|
languages: ["go", "ts"],
|
|
214
214
|
rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint is the ignore tag is never reported: the toolchain applies no build constraint to a file named on its own command line, so that tag is the toolchain's convention for a file built by hand. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
|
|
215
215
|
precondition:
|
|
216
|
-
'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled
|
|
216
|
+
'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
|
|
217
217
|
defaultSeverity: "deny",
|
|
218
218
|
maxClass: "certain",
|
|
219
219
|
fixability: "deletable",
|
|
@@ -235,7 +235,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
235
235
|
languages: ["go", "ts"],
|
|
236
236
|
rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
|
|
237
237
|
precondition:
|
|
238
|
-
"On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and
|
|
238
|
+
"On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
|
|
239
239
|
defaultSeverity: "deny",
|
|
240
240
|
maxClass: "certain",
|
|
241
241
|
fixability: "manual",
|
|
@@ -247,7 +247,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
247
247
|
languages: ["go"],
|
|
248
248
|
rule: "A replace directive whose target module is absent from the build list, the one case in which the directive is exactly a no-op.",
|
|
249
249
|
precondition:
|
|
250
|
-
"Only a replace whose target is absent from the build list. No other module-file directive is reported
|
|
250
|
+
"Only a replace whose target is absent from the build list. No other module-file directive is reported. An exclude directive is not, because not selected is a counterfactual about a resolution that did not happen. A workspace use entry and a tool directive are not, because each is an external entry point whose invocation lives outside anything the analysis reads.",
|
|
251
251
|
defaultSeverity: "warn",
|
|
252
252
|
maxClass: "certain",
|
|
253
253
|
fixability: "deletable",
|
|
@@ -267,7 +267,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
267
267
|
code: "DS1702",
|
|
268
268
|
name: "unscoped-ignore-entry",
|
|
269
269
|
languages: ["go", "ts"],
|
|
270
|
-
rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project
|
|
270
|
+
rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project. The inline directive is scoped by its position and cannot be unscoped.",
|
|
271
271
|
defaultSeverity: "deny",
|
|
272
272
|
maxClass: "certain",
|
|
273
273
|
fixability: "none",
|
|
@@ -319,7 +319,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
319
319
|
languages: ["go", "ts"],
|
|
320
320
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
321
321
|
precondition:
|
|
322
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
322
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported when its body reads no parameter after it, and an unread parameter before the last read one is not. A TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
323
323
|
defaultSeverity: "warn",
|
|
324
324
|
maxClass: "certain",
|
|
325
325
|
fixability: "manual",
|
|
@@ -367,11 +367,13 @@ const ROWS: readonly KindRow[] = [
|
|
|
367
367
|
name: "dead-store",
|
|
368
368
|
languages: ["go", "ts"],
|
|
369
369
|
rule: "A write to a local variable with no read before the next write to it or the end of its scope, computed on the same read-and-write classification the write-only-symbol kind uses. The finding names the write position.",
|
|
370
|
+
precondition:
|
|
371
|
+
"In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same.",
|
|
370
372
|
defaultSeverity: "deny",
|
|
371
373
|
maxClass: "certain",
|
|
372
374
|
fixability: "deletable",
|
|
373
375
|
fixed: false,
|
|
374
|
-
overlap: ["eslint no-useless-assignment"],
|
|
376
|
+
overlap: ["eslint no-useless-assignment", "@typescript-eslint/no-unused-vars"],
|
|
375
377
|
},
|
|
376
378
|
{
|
|
377
379
|
code: "DS1809",
|
package/src/matrix.ts
CHANGED
|
@@ -10,7 +10,14 @@ import type { InventorySymbol } from "./inventory.ts";
|
|
|
10
10
|
import { byPosition } from "./position.ts";
|
|
11
11
|
import type { Reference } from "./references.ts";
|
|
12
12
|
import type { Root } from "./roots.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
reachedWithTheTests,
|
|
15
|
+
sweep,
|
|
16
|
+
type Candidate,
|
|
17
|
+
type Liveness,
|
|
18
|
+
type Relation,
|
|
19
|
+
type SweepInput,
|
|
20
|
+
} from "./sweep.ts";
|
|
14
21
|
|
|
15
22
|
/** What the passes answered for one configuration of the run. */
|
|
16
23
|
export interface Configured {
|
|
@@ -62,6 +69,12 @@ export interface MatrixCandidate extends Candidate {
|
|
|
62
69
|
* declaration it contains is a member of a dead component.
|
|
63
70
|
*/
|
|
64
71
|
readonly heldByTests: string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Whether a test file declares it, no test of dead code, and the run that counts test
|
|
74
|
+
* references does not reach it: it is reported as a production declaration of its
|
|
75
|
+
* shape nothing references would be.
|
|
76
|
+
*/
|
|
77
|
+
readonly unreferencedTest: boolean;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/** One run's sweep. */
|
|
@@ -186,32 +199,61 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
186
199
|
const dead = union.symbols.map(() => false);
|
|
187
200
|
const testOfDeadCode = union.symbols.map(() => false);
|
|
188
201
|
const heldBy: (string | undefined)[] = union.symbols.map(() => undefined);
|
|
189
|
-
const
|
|
190
|
-
union
|
|
202
|
+
const unreferencedTest = union.symbols.map(() => false);
|
|
203
|
+
const testReached = reachedWithTheTests(union, [
|
|
204
|
+
...input.marked,
|
|
205
|
+
...(input.exempt ?? []).map((record) => record.id),
|
|
206
|
+
...(input.unanswered ?? []),
|
|
207
|
+
]);
|
|
208
|
+
const judged = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
|
|
191
209
|
const held = matrix.heldIn[at] ?? [];
|
|
192
210
|
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
193
|
-
|
|
211
|
+
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
212
|
+
? undefined
|
|
213
|
+
: found.filter((candidate): candidate is Candidate => candidate !== undefined);
|
|
214
|
+
});
|
|
215
|
+
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
216
|
+
// so a test is of dead code only where every production subject it names is dead in
|
|
217
|
+
// every configuration that holds it.
|
|
218
|
+
const subjectsDead = (at: number): boolean =>
|
|
219
|
+
(union.out[at] ?? []).every(
|
|
220
|
+
(edge) =>
|
|
221
|
+
union.test[edge.to] === true ||
|
|
222
|
+
union.support[edge.to] === true ||
|
|
223
|
+
union.subject[edge.to] !== true ||
|
|
224
|
+
judged[edge.to] !== undefined,
|
|
225
|
+
);
|
|
226
|
+
const candidates: MatrixCandidate[] = [];
|
|
227
|
+
union.symbols.forEach((symbol, at) => {
|
|
228
|
+
const all = judged[at];
|
|
229
|
+
if (all === undefined) {
|
|
194
230
|
return;
|
|
195
231
|
}
|
|
196
|
-
const
|
|
197
|
-
const admitted = all.every((candidate) => candidate.testOfDeadCode);
|
|
232
|
+
const held = matrix.heldIn[at] ?? [];
|
|
233
|
+
const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
|
|
198
234
|
const testRefs = union.made[at]?.test ?? 0;
|
|
199
235
|
const heldByTests =
|
|
200
236
|
heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
201
237
|
(union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
|
|
238
|
+
const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
|
|
202
239
|
dead[at] = true;
|
|
203
240
|
testOfDeadCode[at] = admitted;
|
|
204
241
|
heldBy[at] = heldByTests;
|
|
242
|
+
unreferencedTest[at] = unreferenced;
|
|
243
|
+
const productionRefs = union.made[at]?.production ?? 0;
|
|
244
|
+
const counted = all.every((candidate) => candidate.relation === "reference-counting");
|
|
205
245
|
candidates.push({
|
|
206
246
|
id: symbol.id,
|
|
207
|
-
productionRefs
|
|
247
|
+
productionRefs,
|
|
208
248
|
testRefs,
|
|
209
|
-
|
|
249
|
+
// The run that judges a test-file declaration counts its test references.
|
|
250
|
+
relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
|
|
210
251
|
? "reference-counting"
|
|
211
252
|
: "reachability",
|
|
212
253
|
testOfDeadCode: admitted,
|
|
213
254
|
configurations: held,
|
|
214
255
|
heldByTests,
|
|
256
|
+
unreferencedTest: unreferenced,
|
|
215
257
|
});
|
|
216
258
|
});
|
|
217
259
|
|
|
@@ -222,6 +264,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
222
264
|
union,
|
|
223
265
|
dead.map((one, at) => one && heldBy[at] === undefined),
|
|
224
266
|
testOfDeadCode,
|
|
267
|
+
unreferencedTest,
|
|
225
268
|
),
|
|
226
269
|
};
|
|
227
270
|
}
|