@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
|
@@ -68,16 +68,40 @@ function dialsOver(input, found, suppressed, unmarked) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Every finding of one run swept under its suppressions' marks, decided in one pass:
|
|
72
|
-
* family's findings completed by one step, so every component number is minted once,
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
71
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: each
|
|
72
|
+
* family's findings completed by one step, so every component number is minted once, the
|
|
73
|
+
* finding each record claims withheld, the rest withheld by the dials, so a root one family
|
|
74
|
+
* reports withholds what another reports in its component, and every declared edge
|
|
75
|
+
* evaluated over what remains, so a pending finding is in its evaluation alone. What the
|
|
76
|
+
* minimum confidence withheld is what the same decision at the lowest minimum adds.
|
|
77
77
|
*/
|
|
78
78
|
export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
|
|
79
79
|
const found = completedOver(input);
|
|
80
80
|
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
81
|
+
const would = [...unmarked, ...found];
|
|
82
|
+
const decided = dialledOver(input, suppressed, found, unmarked);
|
|
83
|
+
const shown = input.config.analysis.minConfidence === "possible"
|
|
84
|
+
? decided
|
|
85
|
+
: dialledOver(atPossible(input), {
|
|
86
|
+
unmarked: atPossible(suppressed.unmarked),
|
|
87
|
+
records: suppressed.records,
|
|
88
|
+
}, found, unmarked);
|
|
89
|
+
return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
|
|
90
|
+
}
|
|
91
|
+
/** The run's input with the minimum confidence at its lowest, which withholds nothing. */
|
|
92
|
+
function atPossible(input) {
|
|
93
|
+
const { config } = input;
|
|
94
|
+
return {
|
|
95
|
+
...input,
|
|
96
|
+
config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The run's completed findings decided under its dials: the finding each record claims
|
|
101
|
+
* withheld, the rest withheld by the dials, and every declared edge evaluated over what
|
|
102
|
+
* remains.
|
|
103
|
+
*/
|
|
104
|
+
function dialledOver(input, suppressed, found, unmarked) {
|
|
81
105
|
const would = [...unmarked, ...found];
|
|
82
106
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
83
107
|
const unanswered = heldRefs(input);
|
|
@@ -87,12 +111,23 @@ export function decidedFindings(input, suppressed = { unmarked: input, records:
|
|
|
87
111
|
!markup.has(finding.symbol.ref) &&
|
|
88
112
|
!insideSkipped(input.swept.skipped, finding.position)));
|
|
89
113
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
90
|
-
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
114
|
+
return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
|
|
115
|
+
}
|
|
116
|
+
/** The findings the lowest minimum would report that the run does not, counted by confidence. */
|
|
117
|
+
function withheldOf(reported, shown) {
|
|
118
|
+
const kept = new Set(reported.map(findingKey));
|
|
119
|
+
const counts = { certain: 0, probable: 0, possible: 0 };
|
|
120
|
+
for (const finding of shown) {
|
|
121
|
+
if (!kept.has(findingKey(finding))) {
|
|
122
|
+
counts[finding.confidence] += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return counts;
|
|
126
|
+
}
|
|
127
|
+
/** A finding's identity across two decisions of one run: its position, code and subject. */
|
|
128
|
+
function findingKey(finding) {
|
|
129
|
+
const { path, line, column } = finding.position;
|
|
130
|
+
return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
|
|
96
131
|
}
|
|
97
132
|
/** Every finding the run reports, each family's in table order. */
|
|
98
133
|
export function findingsOf(input) {
|
|
@@ -54,15 +54,16 @@ function findingOf(code, symbol, message, implementations) {
|
|
|
54
54
|
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
55
55
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
56
56
|
* interface no reference names, unless it is a marker, a method whose every
|
|
57
|
-
* implementation has an empty body. A test file's declaration is neither kind's
|
|
58
|
-
*
|
|
57
|
+
* implementation has an empty body. A test file's declaration is neither kind's subject,
|
|
58
|
+
* because a production sweep counts none of the references it can have, save an interface
|
|
59
|
+
* the run that counts test references does not reach, which is `DS1201`'s.
|
|
59
60
|
*/
|
|
60
61
|
export const interfaces = (input) => {
|
|
61
62
|
const run = indexed(input);
|
|
62
63
|
const found = [];
|
|
63
64
|
for (const candidate of input.swept.sweep.candidates) {
|
|
64
65
|
const symbol = run.symbolOf(candidate.id);
|
|
65
|
-
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
66
|
+
if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
if (symbol.kind === "interface") {
|
|
@@ -14,6 +14,19 @@ function overlapClause(overlap) {
|
|
|
14
14
|
? "no other rule is known to report it"
|
|
15
15
|
: `also reported by ${spelled(overlap)}`;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether one parameter's name is reported: every unread one of a function whose
|
|
19
|
+
* signature is free, and of a function used as a value only one no read parameter follows,
|
|
20
|
+
* because a caller may pass such a function more arguments than it declares.
|
|
21
|
+
*/
|
|
22
|
+
function parameterReported(part, free, freeButValued) {
|
|
23
|
+
const place = part.parameter;
|
|
24
|
+
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
28
|
+
return valued && place?.beforeARead !== true;
|
|
29
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
19
32
|
* change; a result needs that, every call in the loaded program to discard it, and every
|
|
@@ -25,10 +38,10 @@ function reported(input, part, callersUnknown) {
|
|
|
25
38
|
if (part.used) {
|
|
26
39
|
return false;
|
|
27
40
|
}
|
|
28
|
-
const { free, discardedEverywhere } = input.intraFunction;
|
|
41
|
+
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
29
42
|
switch (part.code) {
|
|
30
43
|
case UNUSED_PARAMETER:
|
|
31
|
-
return
|
|
44
|
+
return parameterReported(part, free, freeButValued);
|
|
32
45
|
case UNUSED_RESULT:
|
|
33
46
|
return (free.has(part.declaration) &&
|
|
34
47
|
discardedEverywhere.has(part.declaration) &&
|
|
@@ -49,9 +49,14 @@ function codeOf(judged) {
|
|
|
49
49
|
if (candidate.testOfDeadCode) {
|
|
50
50
|
return "DS1005";
|
|
51
51
|
}
|
|
52
|
-
if (judged.test
|
|
52
|
+
if ((judged.test && !candidate.unreferencedTest) ||
|
|
53
|
+
judged.containerDead ||
|
|
54
|
+
NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
53
55
|
return undefined;
|
|
54
56
|
}
|
|
57
|
+
if (candidate.unreferencedTest) {
|
|
58
|
+
return unreferencedCode(symbol);
|
|
59
|
+
}
|
|
55
60
|
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
56
61
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
57
62
|
return undefined;
|
|
@@ -73,6 +78,13 @@ function codeOf(judged) {
|
|
|
73
78
|
}
|
|
74
79
|
return judged.unreachable ? undefined : "DS1001";
|
|
75
80
|
}
|
|
81
|
+
/** The code a declaration nothing references carries, by its shape and its visibility. */
|
|
82
|
+
function unreferencedCode(symbol) {
|
|
83
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
84
|
+
return "DS1003";
|
|
85
|
+
}
|
|
86
|
+
return symbol.exported ? "DS1001" : "DS1002";
|
|
87
|
+
}
|
|
76
88
|
/** The visibility a member's message names, where it narrows the member's reach. */
|
|
77
89
|
function visibilityWord(symbol) {
|
|
78
90
|
switch (symbol.visibility) {
|
package/dist/src/graph.js
CHANGED
|
@@ -32,11 +32,11 @@ const SWEPT = {
|
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
34
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
35
|
-
* tool reads
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
36
|
+
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
37
|
+
* names what enters the package, an alias global stands for its export, and an entry file,
|
|
38
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
39
|
+
* an unseen consumer, so it is live under reachability alone.
|
|
40
40
|
*/
|
|
41
41
|
const NAMES_A_CALLER = {
|
|
42
42
|
"entry-file": true,
|
|
@@ -52,10 +52,10 @@ const NAMES_A_CALLER = {
|
|
|
52
52
|
"configuration-file": true,
|
|
53
53
|
"configuration-string": true,
|
|
54
54
|
"test-runner": true,
|
|
55
|
-
"mutation-testing": true,
|
|
56
55
|
"browser-tests": true,
|
|
57
56
|
worker: true,
|
|
58
57
|
"html-entry": true,
|
|
58
|
+
"workflow-step": true,
|
|
59
59
|
};
|
|
60
60
|
/** Whether the sweep judges a declaration of one kind. */
|
|
61
61
|
export function swept(kind) {
|
|
@@ -54,7 +54,7 @@ function typesAt(project, nodes) {
|
|
|
54
54
|
return typeAt;
|
|
55
55
|
}
|
|
56
56
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
57
|
-
function emptyImplementation(project, member) {
|
|
57
|
+
export function emptyImplementation(project, member) {
|
|
58
58
|
const own = project.ownPaths();
|
|
59
59
|
let bodies = 0;
|
|
60
60
|
for (const handle of member.declarations) {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members an indexed-access type names. `T["a" | "b"]` reads the members `a` and `b`
|
|
3
|
+
* of `T` as a property access by either name would. Where the index is a type parameter
|
|
4
|
+
* a call fixes, the call names the members the literal type argument names, written or
|
|
5
|
+
* inferred, which is read off the parameter the type parameter annotates whole. The type
|
|
6
|
+
* query of a type-query alias is no reference.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrowFunction, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isIndexedAccessTypeNode, isLiteralTypeNode, isMethodDeclaration, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedTypeNode, isPropertyAccessExpression, isStringLiteral, isTypeReferenceNode, isUnionTypeNode, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { nodeKey } from "./inventory.js";
|
|
10
|
+
import { renderPosition } from "./position.js";
|
|
11
|
+
import { UNANSWERED } from "./query.js";
|
|
12
|
+
import { typeQueryAliases } from "./type-query-alias.js";
|
|
13
|
+
function isGeneric(node) {
|
|
14
|
+
return ((isFunctionDeclaration(node) ||
|
|
15
|
+
isMethodDeclaration(node) ||
|
|
16
|
+
isArrowFunction(node) ||
|
|
17
|
+
isFunctionExpression(node)) &&
|
|
18
|
+
node.typeParameters !== undefined);
|
|
19
|
+
}
|
|
20
|
+
function unwrappedType(node) {
|
|
21
|
+
return isParenthesizedTypeNode(node) ? unwrappedType(node.type) : node;
|
|
22
|
+
}
|
|
23
|
+
/** The names a literal index type spells, or none where the index is any other type. */
|
|
24
|
+
function literalNames(index) {
|
|
25
|
+
const node = unwrappedType(index);
|
|
26
|
+
if (isUnionTypeNode(node)) {
|
|
27
|
+
const names = node.types.map(literalNames);
|
|
28
|
+
return names.every((one) => one !== undefined) ? names.flat() : undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!isLiteralTypeNode(node)) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const literal = node.literal;
|
|
34
|
+
if (isStringLiteral(literal) || isNoSubstitutionTemplateLiteral(literal)) {
|
|
35
|
+
return [literal.text];
|
|
36
|
+
}
|
|
37
|
+
return isNumericLiteral(literal) ? [String(Number(literal.text))] : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The name a literal type carries, a union's each, or none where a constituent is no literal. */
|
|
40
|
+
function literalTypeNames(type, parts) {
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const part of parts(type)) {
|
|
43
|
+
if (part.isStringLiteralType()) {
|
|
44
|
+
names.push(part.value);
|
|
45
|
+
}
|
|
46
|
+
else if (part.isNumberLiteralType()) {
|
|
47
|
+
names.push(String(part.value));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The references the indexed-access types of one project's own files make to the members
|
|
57
|
+
* they name, each made by the declaration that holds the type or the call, or by the file.
|
|
58
|
+
*/
|
|
59
|
+
export function indexedAccessReferences(project, held, chains, root, testFiles) {
|
|
60
|
+
const queries = project.queries;
|
|
61
|
+
const accesses = [];
|
|
62
|
+
const generics = new Map();
|
|
63
|
+
const calls = [];
|
|
64
|
+
const names = new Set();
|
|
65
|
+
for (const file of project.ownSourceFiles()) {
|
|
66
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
67
|
+
if (fileId === undefined) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const aliasTypes = new Set(typeQueryAliases(file).map((alias) => alias.type));
|
|
71
|
+
const visit = (node, from, enclosing) => {
|
|
72
|
+
const own = held.declarations.get(nodeKey(file, node));
|
|
73
|
+
const holder = own ?? from;
|
|
74
|
+
const inside = isGeneric(node) ? [...enclosing, node] : enclosing;
|
|
75
|
+
if (isIndexedAccessTypeNode(node) && !aliasTypes.has(node)) {
|
|
76
|
+
const literal = literalNames(node.indexType);
|
|
77
|
+
if (literal !== undefined) {
|
|
78
|
+
accesses.push({ file, from: holder, at: node, object: node.objectType, names: literal });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
parameterIndex(node, inside, file, generics, names);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
85
|
+
const callee = node.expression;
|
|
86
|
+
const name = isIdentifier(callee)
|
|
87
|
+
? callee.text
|
|
88
|
+
: isPropertyAccessExpression(callee) && isIdentifier(callee.name)
|
|
89
|
+
? callee.name.text
|
|
90
|
+
: undefined;
|
|
91
|
+
if (name !== undefined) {
|
|
92
|
+
calls.push({ file, from: holder, call: node, name });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
node.forEachChild((child) => {
|
|
96
|
+
visit(child, holder, inside);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
file.forEachChild((child) => {
|
|
100
|
+
visit(child, fileId, []);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const { file, from, call, name } of calls) {
|
|
104
|
+
if (!names.has(name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const resolved = queries.resolvedSignature(call);
|
|
108
|
+
if (resolved === UNANSWERED || resolved?.declaration === undefined) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const declaration = project.declarationAt(resolved.declaration)?.node;
|
|
112
|
+
const byParameter = declaration === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: generics.get(nodeKey(declaration.getSourceFile(), declaration));
|
|
115
|
+
if (byParameter === undefined) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const { object, parameter } of byParameter) {
|
|
119
|
+
const fixed = queries.typeAtPosition(resolved, parameter);
|
|
120
|
+
const literal = fixed === UNANSWERED ? undefined : literalTypeNames(fixed, (type) => partsOf(type));
|
|
121
|
+
if (literal !== undefined) {
|
|
122
|
+
accesses.push({ file, from, at: call, object, names: literal });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function partsOf(type) {
|
|
127
|
+
const parts = queries.constituents(type);
|
|
128
|
+
return parts === UNANSWERED ? [type] : parts;
|
|
129
|
+
}
|
|
130
|
+
const objectNodes = accesses.flatMap((one) => ("kind" in one.object ? [one.object] : []));
|
|
131
|
+
const typeOf = new Map();
|
|
132
|
+
queries.typesAt(objectNodes).forEach((answer, index) => {
|
|
133
|
+
const node = objectNodes[index];
|
|
134
|
+
if (answer !== UNANSWERED && answer !== undefined && node !== undefined) {
|
|
135
|
+
typeOf.set(node, answer);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const found = [];
|
|
139
|
+
for (const access of accesses) {
|
|
140
|
+
const type = "kind" in access.object ? typeOf.get(access.object) : access.object;
|
|
141
|
+
if (type === undefined) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const position = renderPosition(access.file, root, access.at.getStart());
|
|
145
|
+
for (const part of partsOf(type)) {
|
|
146
|
+
const properties = queries.propertiesOf(part);
|
|
147
|
+
if (properties === UNANSWERED) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const property of properties) {
|
|
151
|
+
if (!access.names.includes(property.escapedName)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const to of chains.declarationsOf(property)) {
|
|
155
|
+
found.push({
|
|
156
|
+
from: access.from,
|
|
157
|
+
to,
|
|
158
|
+
position,
|
|
159
|
+
use: "read",
|
|
160
|
+
resolution: "batch",
|
|
161
|
+
test: testFiles.has(position.path),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Records an indexed-access type whose index names a type parameter of an enclosing
|
|
171
|
+
* function that a parameter of that function is annotated with whole, so a call's
|
|
172
|
+
* argument for that parameter fixes the index.
|
|
173
|
+
*/
|
|
174
|
+
function parameterIndex(node, enclosing, file, generics, names) {
|
|
175
|
+
const index = unwrappedType(node.indexType);
|
|
176
|
+
if (!isTypeReferenceNode(index) || !isIdentifier(index.typeName)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const spelled = index.typeName.text;
|
|
180
|
+
for (const generic of [...enclosing].reverse()) {
|
|
181
|
+
const declares = (generic.typeParameters ?? []).some((parameter) => isIdentifier(parameter.name) && parameter.name.text === spelled);
|
|
182
|
+
if (!declares) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const at = generic.parameters.findIndex((parameter) => {
|
|
186
|
+
const type = parameter.type === undefined ? undefined : unwrappedType(parameter.type);
|
|
187
|
+
return (type !== undefined &&
|
|
188
|
+
isTypeReferenceNode(type) &&
|
|
189
|
+
isIdentifier(type.typeName) &&
|
|
190
|
+
type.typeName.text === spelled);
|
|
191
|
+
});
|
|
192
|
+
if (at >= 0) {
|
|
193
|
+
const key = nodeKey(file, generic);
|
|
194
|
+
generics.set(key, [...(generics.get(key) ?? []), { object: node.objectType, parameter: at }]);
|
|
195
|
+
const name = calledName(generic);
|
|
196
|
+
if (name !== undefined) {
|
|
197
|
+
names.add(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** The name a call writes to call one generic function: its own, or its variable's. */
|
|
204
|
+
function calledName(generic) {
|
|
205
|
+
const own = generic.name;
|
|
206
|
+
if (own !== undefined && isIdentifier(own)) {
|
|
207
|
+
return own.text;
|
|
208
|
+
}
|
|
209
|
+
const parent = generic.parent;
|
|
210
|
+
return parent.kind === SyntaxKind.VariableDeclaration &&
|
|
211
|
+
parent.name !== undefined &&
|
|
212
|
+
isIdentifier(parent.name)
|
|
213
|
+
? parent.name.text
|
|
214
|
+
: undefined;
|
|
215
|
+
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The interface-satisfaction class: a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* declaration whose type is written, an assignment, an argument, a returned value, an
|
|
9
|
-
* array element, an object property, or the operand of `as` or `satisfies`, read
|
|
10
|
-
* through parentheses, conditionals and the operators whose result is an operand.
|
|
11
|
-
* `this` in an instance member is a value of its class. Types are asked in batches;
|
|
12
|
-
* the contextual lookup has none, so it is asked only of a value whose type is a class
|
|
13
|
-
* of the target, and each distinct pair, interface and class is asked once.
|
|
2
|
+
* The interface-satisfaction class: a member is held back when a value of its class
|
|
3
|
+
* reaches a position the checker types as an interface requiring that member, through an
|
|
4
|
+
* `implements` entry, a typed initializer, an assignment, an argument, a return, an array
|
|
5
|
+
* element, an object property or an `as` or `satisfies` operand. `this` is a value of its
|
|
6
|
+
* class, and a position typed by a type parameter reaches its constraint. The contextual
|
|
7
|
+
* lookup has no batch form, so it is asked only of a value typed as a target class.
|
|
14
8
|
*/
|
|
15
|
-
import { isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
16
10
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
17
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
|
+
import { emptyImplementation } from "./implementations.js";
|
|
18
13
|
import { nodeKey } from "./inventory.js";
|
|
19
14
|
import { renderPosition } from "./position.js";
|
|
20
15
|
import { must } from "./query.js";
|
|
@@ -153,6 +148,10 @@ export function interfaceSatisfaction(input) {
|
|
|
153
148
|
const interfaceNames = new Map();
|
|
154
149
|
/** The interface types one position's type is or holds, each with the interface's name. */
|
|
155
150
|
const interfacesOf = (type) => partsOf(type).flatMap((part) => {
|
|
151
|
+
if (part.isTypeParameter()) {
|
|
152
|
+
const constraint = must(queries.constraintOf(part));
|
|
153
|
+
return constraint === undefined ? [] : interfacesOf(constraint);
|
|
154
|
+
}
|
|
156
155
|
if (!interfaceNames.has(part.id)) {
|
|
157
156
|
const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
|
|
158
157
|
interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
|
|
@@ -162,6 +161,21 @@ export function interfaceSatisfaction(input) {
|
|
|
162
161
|
const name = interfaceNames.get(part.id);
|
|
163
162
|
return name === undefined ? [] : [{ type: part, name }];
|
|
164
163
|
});
|
|
164
|
+
/**
|
|
165
|
+
* The type an argument's parameter declares in the generic signature a call
|
|
166
|
+
* instantiates, which is a type parameter where the argument is a type argument's value:
|
|
167
|
+
* the contextual type of such an argument is the instantiation, not the parameter.
|
|
168
|
+
*/
|
|
169
|
+
const genericPosition = (value) => {
|
|
170
|
+
const call = value.parent;
|
|
171
|
+
if (!isCallExpression(call) && !isNewExpression(call)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const at = (call.arguments ?? []).findIndex((argument) => argument === value);
|
|
175
|
+
const resolved = at < 0 ? undefined : must(queries.resolvedSignature(call));
|
|
176
|
+
const generic = resolved === undefined ? undefined : must(queries.genericOf(resolved));
|
|
177
|
+
return generic === undefined ? undefined : must(queries.typeAtPosition(generic, at));
|
|
178
|
+
};
|
|
165
179
|
const pairs = new Map();
|
|
166
180
|
const reach = (owner, source, target, name, site) => {
|
|
167
181
|
const key = `${String(source.id)}:${String(target.id)}`;
|
|
@@ -197,12 +211,11 @@ export function interfaceSatisfaction(input) {
|
|
|
197
211
|
const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
|
|
198
212
|
carrying.forEach(({ flow, sources }, index) => {
|
|
199
213
|
const position = must(positions[index]);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
214
|
+
for (const type of [position, genericPosition(flow.value)]) {
|
|
215
|
+
for (const reached of type === undefined ? [] : interfacesOf(type)) {
|
|
216
|
+
for (const source of sources) {
|
|
217
|
+
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
});
|
|
@@ -218,7 +231,10 @@ export function interfaceSatisfaction(input) {
|
|
|
218
231
|
}
|
|
219
232
|
let names = required.get(pair.target.id);
|
|
220
233
|
if (names === undefined) {
|
|
221
|
-
names = new
|
|
234
|
+
names = new Map(must(queries.propertiesOf(pair.target)).map((property) => [
|
|
235
|
+
property.escapedName,
|
|
236
|
+
chains.declarationsOf(property),
|
|
237
|
+
]));
|
|
222
238
|
required.set(pair.target.id, names);
|
|
223
239
|
}
|
|
224
240
|
let answering = members.get(pair.owner.id);
|
|
@@ -226,18 +242,23 @@ export function interfaceSatisfaction(input) {
|
|
|
226
242
|
const instance = instanceOf.get(pair.owner.id) ?? pair.source;
|
|
227
243
|
answering = new Map(must(queries.propertiesOf(instance)).map((property) => [
|
|
228
244
|
property.escapedName,
|
|
229
|
-
|
|
245
|
+
{
|
|
246
|
+
ids: chains.declarationsOf(property),
|
|
247
|
+
empty: emptyImplementation(project, property),
|
|
248
|
+
},
|
|
230
249
|
]));
|
|
231
250
|
members.set(pair.owner.id, answering);
|
|
232
251
|
}
|
|
233
252
|
const detail = `satisfies ${pair.name}`;
|
|
234
|
-
for (const name of names) {
|
|
235
|
-
|
|
253
|
+
for (const [name, methods] of names) {
|
|
254
|
+
const member = answering.get(name);
|
|
255
|
+
for (const id of member?.ids ?? []) {
|
|
236
256
|
for (const site of pair.sites) {
|
|
237
257
|
found.push({
|
|
238
258
|
id,
|
|
239
259
|
detail,
|
|
240
260
|
site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
|
|
261
|
+
implementing: { methods, empty: member?.empty ?? false },
|
|
241
262
|
});
|
|
242
263
|
}
|
|
243
264
|
}
|
|
@@ -245,3 +266,44 @@ export function interfaceSatisfaction(input) {
|
|
|
245
266
|
}
|
|
246
267
|
return found;
|
|
247
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Each project's records with every record dropped whose member implements only interface
|
|
271
|
+
* methods nothing calls, unless every implementation of such a method is empty, and a
|
|
272
|
+
* reference from each such method to the member, which then falls with the method.
|
|
273
|
+
* `references` is every reference of the run, a consumer's included.
|
|
274
|
+
*/
|
|
275
|
+
export function settledSatisfaction(projects, references) {
|
|
276
|
+
const methods = new Set(projects.flatMap((one) => one.held.symbols.filter((symbol) => symbol.kind === "interface-method").map((one) => one.id)));
|
|
277
|
+
const called = new Set(references.map((reference) => reference.to));
|
|
278
|
+
const bodies = new Map();
|
|
279
|
+
for (const record of projects.flatMap((one) => one.evidence)) {
|
|
280
|
+
for (const method of record.implementing?.methods ?? []) {
|
|
281
|
+
bodies.set(method, (bodies.get(method) ?? true) && record.implementing?.empty === true);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const uncalled = (method) => methods.has(method) && !called.has(method) && bodies.get(method) !== true;
|
|
285
|
+
return projects.map(({ evidence }) => {
|
|
286
|
+
const kept = [];
|
|
287
|
+
const added = [];
|
|
288
|
+
for (const record of evidence) {
|
|
289
|
+
const implemented = record.implementing?.methods ?? [];
|
|
290
|
+
if (record.class !== "interface-satisfaction" ||
|
|
291
|
+
implemented.length === 0 ||
|
|
292
|
+
!implemented.every(uncalled)) {
|
|
293
|
+
kept.push(record);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
for (const method of implemented) {
|
|
297
|
+
added.push({
|
|
298
|
+
from: method,
|
|
299
|
+
to: record.id,
|
|
300
|
+
position: record.site,
|
|
301
|
+
use: "read",
|
|
302
|
+
resolution: "override",
|
|
303
|
+
test: false,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { evidence: kept, references: added };
|
|
308
|
+
});
|
|
309
|
+
}
|