@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3
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/dist/src/analysis.js +30 -6
- package/dist/src/augmentations.js +105 -12
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +62 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +122 -49
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +13 -5
- package/dist/src/findings/visibility-narrowing.js +8 -3
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +76 -4
- package/dist/src/references.js +34 -3
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/suppress.js +1 -0
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/test-support.js +8 -11
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +48 -8
- package/src/augmentations.ts +134 -16
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +72 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/finding.ts +5 -0
- package/src/findings/completion.ts +153 -56
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +17 -5
- package/src/findings/visibility-narrowing.ts +9 -3
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +122 -4
- package/src/references.ts +38 -2
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/suppress.ts +1 -0
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/test-support.ts +17 -13
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
package/dist/src/kinds.js
CHANGED
|
@@ -43,7 +43,7 @@ const ROWS = [
|
|
|
43
43
|
code: "DS1004",
|
|
44
44
|
name: "test-only-use",
|
|
45
45
|
languages: ["go", "ts"],
|
|
46
|
-
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it.",
|
|
46
|
+
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it. A candidate found dead under reachability, every production reference to which is made by a declaration itself dead, is reported under this code as well where test code that is no test of dead code, and that the run counting test references reaches, references it, because that test keeps it, and the finding's message names both kinds of reference.",
|
|
47
47
|
defaultSeverity: "deny",
|
|
48
48
|
maxClass: "certain",
|
|
49
49
|
fixability: "deletable",
|
|
@@ -54,7 +54,7 @@ const ROWS = [
|
|
|
54
54
|
code: "DS1005",
|
|
55
55
|
name: "test-of-dead-code",
|
|
56
56
|
languages: ["go", "ts"],
|
|
57
|
-
rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
|
|
57
|
+
rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. A production declaration whose reachability class is `possible` because programs outside the analysis may use it, being published API of a library whose run declares no consumer, and in Go a declaration of a package no build links or an export of a unit a dropped module requires, counts as a live target symbol of every test that references it, so such a test is no test of dead code. The declaration is still reported at `possible`. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
|
|
58
58
|
defaultSeverity: "deny",
|
|
59
59
|
maxClass: "certain",
|
|
60
60
|
fixability: "deletable",
|
|
@@ -110,7 +110,7 @@ const ROWS = [
|
|
|
110
110
|
name: "redundant-export-keyword",
|
|
111
111
|
languages: ["ts"],
|
|
112
112
|
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.",
|
|
113
|
-
precondition: 'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, 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.',
|
|
113
|
+
precondition: 'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, while a file a build script\'s bundler call names is one, because the bundle exposes what it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and a file a Markdown or MDX document imports one only for the exports the import reads, as grammar/analysis.md "Markdown and MDX documents" states, 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.',
|
|
114
114
|
defaultSeverity: "warn",
|
|
115
115
|
maxClass: "certain",
|
|
116
116
|
fixability: "narrowable",
|
|
@@ -174,8 +174,8 @@ const ROWS = [
|
|
|
174
174
|
code: "DS1303",
|
|
175
175
|
name: "unused-type-parameter",
|
|
176
176
|
languages: ["go", "ts"],
|
|
177
|
-
rule: "A type parameter of a function
|
|
178
|
-
precondition: "Function and method type parameters
|
|
177
|
+
rule: "A type parameter of a function, a method or a TypeScript type alias that no part of the declaration's signature, body or aliased type names, at the certain class. The deletion is local to the declaration and the explicit instantiations the reference set already lists. A type parameter of a declaration that is itself dead falls with it and is reported under no code of its own.",
|
|
178
|
+
precondition: "Function and method type parameters, and in TypeScript those of a type alias. A TypeScript alias names no type of its own, so two instantiations that differ only in a parameter its type never names are one type. A Go type declaration's type parameter is never reported, because a phantom type parameter such as `type ID[T any] int` makes two instantiations distinct types while naming the parameter nowhere, so deleting it changes the program. Nor is one of a TypeScript class or interface, which merges only with declarations of the same type parameters. A method signature an interface or an object type declares belongs to that type declaration, so its type parameters are never reported either.",
|
|
179
179
|
defaultSeverity: "deny",
|
|
180
180
|
maxClass: "certain",
|
|
181
181
|
fixability: "deletable",
|
|
@@ -289,7 +289,7 @@ const ROWS = [
|
|
|
289
289
|
name: "unused-parameter",
|
|
290
290
|
languages: ["go", "ts"],
|
|
291
291
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
292
|
-
precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. 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 for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it.
|
|
292
|
+
precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. 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 for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. A name an object binding pattern of a parameter of such a function binds, annotated or not, that the body never reads is reported for deletion at `probable`, because the pattern reads the property it names from the argument and the code that hands the argument may act on that read, which nothing in the program shows. Any other 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. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
293
293
|
defaultSeverity: "warn",
|
|
294
294
|
maxClass: "certain",
|
|
295
295
|
fixability: "manual",
|
|
@@ -313,7 +313,7 @@ const ROWS = [
|
|
|
313
313
|
name: "unused-result",
|
|
314
314
|
languages: ["go", "ts"],
|
|
315
315
|
rule: "A result value that no call site of the function uses, on a function whose signature is free to change.",
|
|
316
|
-
precondition: "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
|
|
316
|
+
precondition: "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A function a program run by naming the file names, a file under the `ignore` tag or a script of a dropped module, has call sites outside the loaded graph. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
|
|
317
317
|
defaultSeverity: "warn",
|
|
318
318
|
maxClass: "certain",
|
|
319
319
|
fixability: "manual",
|
|
@@ -336,7 +336,7 @@ const ROWS = [
|
|
|
336
336
|
name: "dead-store",
|
|
337
337
|
languages: ["go", "ts"],
|
|
338
338
|
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.",
|
|
339
|
-
precondition: "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. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
|
|
339
|
+
precondition: "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. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A function declaration of a block of a function is a TypeScript local too, written with its function when the block starts, wherever it stands in the block, so one that nothing outside its own body reads is a dead store positioned at its name, and a read anywhere else in the block reads it. A `let` or `var` declaration with no initializer writes `undefined`, so one that nothing reads and nothing writes is a dead store positioned at its name. A local whose declaration carries the type annotation `never` is no subject: a write to it is the compiler's exhaustiveness check, which fails to compile once a case goes unhandled, so deleting it changes what the program checks. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
|
|
340
340
|
defaultSeverity: "deny",
|
|
341
341
|
maxClass: "certain",
|
|
342
342
|
fixability: "deletable",
|
package/dist/src/manifest.js
CHANGED
|
@@ -110,6 +110,24 @@ export function readManifest(host, targetRoot) {
|
|
|
110
110
|
found.push({ member, path: value, role: "import", published: true });
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
// A `browser` string replaces `main` in a browser bundle, and an object replaces each
|
|
114
|
+
// module it maps with the file it names: https://github.com/defunctzombie/package-browser-field-spec
|
|
115
|
+
const browser = manifest["browser"];
|
|
116
|
+
if (typeof browser === "string" && isTarget(browser, false)) {
|
|
117
|
+
found.push({ member: "browser", path: browser, role: "import", published: true });
|
|
118
|
+
}
|
|
119
|
+
else if (isRecord(browser)) {
|
|
120
|
+
for (const [replaced, value] of Object.entries(browser)) {
|
|
121
|
+
if (typeof value === "string" && isTarget(value, true)) {
|
|
122
|
+
found.push({
|
|
123
|
+
member: below("browser", replaced),
|
|
124
|
+
path: value,
|
|
125
|
+
role: "import",
|
|
126
|
+
published: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
113
131
|
const bin = manifest["bin"];
|
|
114
132
|
if (typeof bin === "string" && isTarget(bin, false)) {
|
|
115
133
|
found.push({ member: "bin", path: bin, role: "run", published: false });
|
package/dist/src/matrix.js
CHANGED
|
@@ -70,11 +70,13 @@ export function matrixOf(per) {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
symbols.sort((a, b) => byPosition(a.position, b.position));
|
|
73
|
+
const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
|
|
73
74
|
return {
|
|
74
75
|
configurations: per.map((one) => one.configuration),
|
|
75
76
|
graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles, one.ambient)),
|
|
76
77
|
union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])]))),
|
|
77
78
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
79
|
+
testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
|
|
78
80
|
dependents,
|
|
79
81
|
};
|
|
80
82
|
}
|
|
@@ -101,18 +103,24 @@ export function sweepMatrix(matrix, input) {
|
|
|
101
103
|
]));
|
|
102
104
|
const union = matrix.union;
|
|
103
105
|
const heldBy = union.symbols.map(() => undefined);
|
|
104
|
-
const
|
|
106
|
+
const held = [
|
|
105
107
|
...input.marked,
|
|
106
108
|
...(input.exempt ?? []).map((record) => record.id),
|
|
107
109
|
...(input.unanswered ?? []),
|
|
108
|
-
]
|
|
109
|
-
const
|
|
110
|
+
];
|
|
111
|
+
const testReached = reachedWithTheTests(union, held);
|
|
112
|
+
// A test uses what it names: a file a test reads through a star re-export or a
|
|
113
|
+
// namespace keeps the exports nothing names at their own class.
|
|
114
|
+
const testNamed = reachedWithTheTests(union, held, false);
|
|
115
|
+
const everywhere = union.symbols.map((symbol, at) => {
|
|
110
116
|
const held = matrix.heldIn[at] ?? [];
|
|
111
117
|
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
112
118
|
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
113
119
|
? undefined
|
|
114
120
|
: found.filter((candidate) => candidate !== undefined);
|
|
115
121
|
});
|
|
122
|
+
const compiled = compiledReferences(matrix, new Set(union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id]))), input.mode);
|
|
123
|
+
const judged = everywhere.map((found, at) => compiled.has(union.symbols[at]?.id ?? "") ? undefined : found);
|
|
116
124
|
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
117
125
|
// so a test is of dead code only where every production subject it names is dead in
|
|
118
126
|
// every configuration that holds it.
|
|
@@ -121,6 +129,9 @@ export function sweepMatrix(matrix, input) {
|
|
|
121
129
|
union.ambient[edge.to] === true ||
|
|
122
130
|
union.subject[edge.to] !== true ||
|
|
123
131
|
judged[edge.to] !== undefined);
|
|
132
|
+
const admittedAt = (at) => judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
|
|
133
|
+
const kept = keptByTests(union, testReached, admittedAt);
|
|
134
|
+
const testUsed = testUsedReach(union, union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true), judged.map((one) => one !== undefined));
|
|
124
135
|
const candidates = [];
|
|
125
136
|
union.symbols.forEach((symbol, at) => {
|
|
126
137
|
const all = judged[at];
|
|
@@ -128,7 +139,7 @@ export function sweepMatrix(matrix, input) {
|
|
|
128
139
|
return;
|
|
129
140
|
}
|
|
130
141
|
const held = matrix.heldIn[at] ?? [];
|
|
131
|
-
const admitted =
|
|
142
|
+
const admitted = admittedAt(at);
|
|
132
143
|
const testRefs = union.made[at]?.test ?? 0;
|
|
133
144
|
const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
134
145
|
(union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
|
|
@@ -148,6 +159,8 @@ export function sweepMatrix(matrix, input) {
|
|
|
148
159
|
configurations: held,
|
|
149
160
|
heldByTests,
|
|
150
161
|
unreferencedTest: unreferenced,
|
|
162
|
+
testUsed: testUsed[at] === true,
|
|
163
|
+
keptByTests: kept[at] === true,
|
|
151
164
|
});
|
|
152
165
|
});
|
|
153
166
|
const liveUnder = liveAnywhere(union.symbols, per);
|
|
@@ -157,6 +170,65 @@ export function sweepMatrix(matrix, input) {
|
|
|
157
170
|
components: deadComponents(matrix, { liveUnder, candidates }, new Set()),
|
|
158
171
|
};
|
|
159
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Every declaration of `dead`, those dead in every configuration that holds them, that a
|
|
175
|
+
* configuration's graph references from a judged declaration outside `dead` or already
|
|
176
|
+
* kept, until none joins: deleting one would break that configuration's compile. A
|
|
177
|
+
* production run counts no reference a test file made.
|
|
178
|
+
*/
|
|
179
|
+
function compiledReferences(matrix, dead, mode) {
|
|
180
|
+
const kept = new Set();
|
|
181
|
+
for (let changed = true; changed;) {
|
|
182
|
+
changed = false;
|
|
183
|
+
for (const graph of matrix.graphs) {
|
|
184
|
+
graph.out.forEach((edges, from) => {
|
|
185
|
+
const id = graph.symbols[from]?.id ?? "";
|
|
186
|
+
if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const edge of edges) {
|
|
190
|
+
const to = graph.symbols[edge.to]?.id ?? "";
|
|
191
|
+
if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
|
|
192
|
+
kept.add(to);
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return kept;
|
|
200
|
+
}
|
|
201
|
+
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
202
|
+
function testUsedReach(union, seeds, dead) {
|
|
203
|
+
// A file is no candidate, and its top level is what reaches what it alone calls.
|
|
204
|
+
const reached = seeds.map((seed, at) => seed && dead[at] === true);
|
|
205
|
+
const queue = seeds.flatMap((one, at) => (one ? [at] : []));
|
|
206
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
207
|
+
for (const edge of union.out[at] ?? []) {
|
|
208
|
+
if (dead[edge.to] === true && reached[edge.to] !== true) {
|
|
209
|
+
reached[edge.to] = true;
|
|
210
|
+
queue.push(edge.to);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return reached;
|
|
215
|
+
}
|
|
216
|
+
/** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
|
|
217
|
+
function keptByTests(union, reached, admitted) {
|
|
218
|
+
const kept = union.symbols.map(() => false);
|
|
219
|
+
union.out.forEach((edges, from) => {
|
|
220
|
+
const testCode = union.test[from] === true || union.support[from] === true;
|
|
221
|
+
if (!testCode || reached[from] !== true || admitted(from)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
for (const edge of edges) {
|
|
225
|
+
if (edge.test) {
|
|
226
|
+
kept[edge.to] = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return kept;
|
|
231
|
+
}
|
|
160
232
|
/**
|
|
161
233
|
* The dead components of one sweep over the union, with the file declarations at `files`
|
|
162
234
|
* dead as well: a file that is deleted whole takes the declarations it holds and every
|
package/dist/src/references.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isInterfaceDeclaration, isMetaProperty, isModuleBlock, isModuleDeclaration, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { contextualUses } from "./contextual-uses.js";
|
|
4
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
@@ -6,7 +6,7 @@ import { specifierCandidates } from "./configuration-files.js";
|
|
|
6
6
|
import { namespaceUses } from "./namespace-objects.js";
|
|
7
7
|
import { globExpression } from "./glob.js";
|
|
8
8
|
import { declarationsByName, isParameterProperty, nodeKey, } from "./inventory.js";
|
|
9
|
-
import { augmentationReferences } from "./augmentations.js";
|
|
9
|
+
import { ambientGlobalInterfaces, augmentationReferences } from "./augmentations.js";
|
|
10
10
|
import { elementAccessReferences } from "./element-access.js";
|
|
11
11
|
import { indexedAccessReferences } from "./indexed-access.js";
|
|
12
12
|
import { keyofComparisonReferences } from "./keyof-comparisons.js";
|
|
@@ -1135,6 +1135,37 @@ export function references(project, held, targetRoot, options) {
|
|
|
1135
1135
|
}
|
|
1136
1136
|
}
|
|
1137
1137
|
}
|
|
1138
|
+
/** The ambient global interfaces of the walked files that merge into a declaration outside the project's own files. */
|
|
1139
|
+
const libraryMerged = (scripts) => {
|
|
1140
|
+
const names = new Map();
|
|
1141
|
+
const visit = (node) => {
|
|
1142
|
+
if (isInterfaceDeclaration(node)) {
|
|
1143
|
+
const id = declaredAt(node);
|
|
1144
|
+
if (id !== undefined) {
|
|
1145
|
+
names.set(id, node.name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
else if (isModuleDeclaration(node) || isModuleBlock(node)) {
|
|
1149
|
+
node.forEachChild(visit);
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
for (const { file } of walked) {
|
|
1153
|
+
file.forEachChild(visit);
|
|
1154
|
+
}
|
|
1155
|
+
const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
|
|
1156
|
+
const asked = [...names].filter(([id]) => global.has(id));
|
|
1157
|
+
if (asked.length === 0) {
|
|
1158
|
+
return new Set();
|
|
1159
|
+
}
|
|
1160
|
+
const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
|
|
1161
|
+
return new Set(asked.flatMap(([id], at) => {
|
|
1162
|
+
const symbol = answers[at];
|
|
1163
|
+
return typeof symbol === "object" &&
|
|
1164
|
+
symbol.declarations.some((handle) => !ownPaths.has(handle.path))
|
|
1165
|
+
? [id]
|
|
1166
|
+
: [];
|
|
1167
|
+
}));
|
|
1168
|
+
};
|
|
1138
1169
|
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1139
1170
|
for (const [file, writes] of valued) {
|
|
1140
1171
|
const uses = usesOf.get(file) ?? [];
|
|
@@ -1159,7 +1190,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
1159
1190
|
typedLookups = (options.requestCount?.() ?? 0) - before;
|
|
1160
1191
|
}
|
|
1161
1192
|
const scripts = new Set(walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId));
|
|
1162
|
-
found.push(...augmentationReferences(held, found, scripts));
|
|
1193
|
+
found.push(...augmentationReferences(held, found, scripts, libraryMerged(scripts)));
|
|
1163
1194
|
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
1164
1195
|
compare(a.to, b.to) ||
|
|
1165
1196
|
compare(a.from, b.from) ||
|
package/dist/src/report.js
CHANGED
|
@@ -11,6 +11,21 @@ export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
|
|
|
11
11
|
export const ANALYZER_NAME = "deadset-ts";
|
|
12
12
|
/** The language this analyzer claims. */
|
|
13
13
|
export const LANGUAGE = "ts";
|
|
14
|
+
/**
|
|
15
|
+
* Each component the findings name, once, with the members the first finding naming it
|
|
16
|
+
* carries, in the order the findings are: `findings` paired with their wire forms and
|
|
17
|
+
* sorted, then the edge evaluations' findings.
|
|
18
|
+
*/
|
|
19
|
+
function componentsOf(findings) {
|
|
20
|
+
const listed = new Map();
|
|
21
|
+
for (const [wire, finding] of findings) {
|
|
22
|
+
const { members } = finding.component;
|
|
23
|
+
if (members !== undefined && !listed.has(wire.component.id)) {
|
|
24
|
+
listed.set(wire.component.id, { id: wire.component.id, members: members.map(wireSymbol) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...listed.values()];
|
|
28
|
+
}
|
|
14
29
|
/** Two strings ordered bytewise. */
|
|
15
30
|
function compare(a, b) {
|
|
16
31
|
if (a === b) {
|
|
@@ -85,9 +100,6 @@ export function wireFinding(finding) {
|
|
|
85
100
|
root: finding.component.root,
|
|
86
101
|
symbol_count: finding.component.symbolCount,
|
|
87
102
|
deletable_lines: finding.component.deletableLines,
|
|
88
|
-
...(finding.component.members === undefined
|
|
89
|
-
? {}
|
|
90
|
-
: { members: finding.component.members.map(wireSymbol) }),
|
|
91
103
|
},
|
|
92
104
|
retained_by: finding.retainedBy,
|
|
93
105
|
configurations: finding.configurations,
|
|
@@ -198,7 +210,11 @@ function totalsOf(findings, result) {
|
|
|
198
210
|
}
|
|
199
211
|
/** Builds the report of one run, every array in the order the Contract fixes for it. */
|
|
200
212
|
export function buildReport(input) {
|
|
201
|
-
const
|
|
213
|
+
const paired = input.result.findings
|
|
214
|
+
.map((finding) => [wireFinding(finding), finding])
|
|
215
|
+
.sort(([a], [b]) => canonical(a, b));
|
|
216
|
+
const findings = paired.map(([wire]) => wire);
|
|
217
|
+
const evaluated = input.result.edgeEvaluations.flatMap((one) => one.finding === undefined ? [] : [[wireFinding(one.finding), one.finding]]);
|
|
202
218
|
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
203
219
|
.sort((a, b) => compare(a.id, b.id))
|
|
204
220
|
.map((one) => ({ id: one.id, role: "consumer", reason: one.reason }));
|
|
@@ -228,6 +244,9 @@ export function buildReport(input) {
|
|
|
228
244
|
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
229
245
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
230
246
|
findings,
|
|
247
|
+
...(input.listsComponents === true
|
|
248
|
+
? { components: componentsOf([...paired, ...evaluated]) }
|
|
249
|
+
: {}),
|
|
231
250
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
232
251
|
stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
|
|
233
252
|
// Every gap shares the one component of the canonical key a gap supplies, the analyzer's
|
|
@@ -114,7 +114,7 @@ function lineHashCache(read) {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
/** Every position a finding names beyond its own, in the mapping's order, the first hundred. */
|
|
117
|
-
function relatedOf(found) {
|
|
117
|
+
function relatedOf(found, members) {
|
|
118
118
|
const related = [];
|
|
119
119
|
for (const one of found.details.implementations ?? []) {
|
|
120
120
|
related.push({ label: "implementation", at: one.position, endLine: one.position.end_line });
|
|
@@ -126,7 +126,7 @@ function relatedOf(found) {
|
|
|
126
126
|
const at = found.details.name_literal;
|
|
127
127
|
related.push({ label: "name", at, endLine: at.end_line });
|
|
128
128
|
}
|
|
129
|
-
for (const one of found.component.
|
|
129
|
+
for (const one of members(found.component.id)) {
|
|
130
130
|
if (declarationKey(one.ref, one.position.path) !==
|
|
131
131
|
declarationKey(found.symbol.ref, found.position.path)) {
|
|
132
132
|
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
@@ -143,7 +143,9 @@ function messageWithLinks(message, related) {
|
|
|
143
143
|
return `${message} (see ${links.join(", ")})`;
|
|
144
144
|
}
|
|
145
145
|
/** Every member of the finding the mapping places nowhere else, under its schema name. */
|
|
146
|
-
|
|
146
|
+
/** A result's member bag, its component holding the members the report lists for it once. */
|
|
147
|
+
function propertiesOf(found, members) {
|
|
148
|
+
const listed = members(found.component.id);
|
|
147
149
|
return {
|
|
148
150
|
language: found.language,
|
|
149
151
|
symbol: found.symbol,
|
|
@@ -154,7 +156,7 @@ function propertiesOf(found) {
|
|
|
154
156
|
: { liveness_relation: found.liveness_relation }),
|
|
155
157
|
test_only: found.test_only,
|
|
156
158
|
generated: found.generated,
|
|
157
|
-
component: found.component,
|
|
159
|
+
component: listed.length === 0 ? found.component : { ...found.component, members: listed },
|
|
158
160
|
retained_by: found.retained_by,
|
|
159
161
|
configurations: found.configurations,
|
|
160
162
|
consumers_loaded: found.consumers_loaded,
|
|
@@ -169,8 +171,8 @@ function ruleIndex(index, code) {
|
|
|
169
171
|
}
|
|
170
172
|
return at;
|
|
171
173
|
}
|
|
172
|
-
function findingResult(found, index, hashOf) {
|
|
173
|
-
const related = relatedOf(found);
|
|
174
|
+
function findingResult(found, index, hashOf, members) {
|
|
175
|
+
const related = relatedOf(found, members);
|
|
174
176
|
return {
|
|
175
177
|
ruleId: found.code,
|
|
176
178
|
ruleIndex: ruleIndex(index, found.code),
|
|
@@ -190,7 +192,7 @@ function findingResult(found, index, hashOf) {
|
|
|
190
192
|
primaryLocationLineHash: hashOf(found.position),
|
|
191
193
|
"deadsetSymbolRef/v1": symbolFingerprint(found.code, found.symbol.ref),
|
|
192
194
|
},
|
|
193
|
-
properties: propertiesOf(found),
|
|
195
|
+
properties: propertiesOf(found, members),
|
|
194
196
|
};
|
|
195
197
|
}
|
|
196
198
|
/** A stale suppression at the failing level, at its own site; it is no finding, so it carries no member bag. */
|
|
@@ -218,8 +220,10 @@ export function sarif(report, options) {
|
|
|
218
220
|
const ruleList = rules(kinds);
|
|
219
221
|
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
220
222
|
const hashOf = lineHashCache(options.readSource);
|
|
223
|
+
const listed = new Map((report.components ?? []).map((one) => [one.id, one.members]));
|
|
224
|
+
const members = (id) => listed.get(id) ?? [];
|
|
221
225
|
const results = [
|
|
222
|
-
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
226
|
+
...report.findings.map((found) => findingResult(found, index, hashOf, members)),
|
|
223
227
|
...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
|
|
224
228
|
];
|
|
225
229
|
const languages = [...report.analyzer.languages].sort().join("+");
|
package/dist/src/roots.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* decides; the published API only supposes a consumer, so a published export is live
|
|
8
8
|
* under reachability and still a candidate under reference counting.
|
|
9
9
|
*/
|
|
10
|
-
import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isPropertyDeclaration, isVariableDeclaration, isVariableStatement, } from "@typescript/native/ast";
|
|
10
|
+
import { isArrowFunction, isAssertionExpression, isExpressionWithTypeArguments, isFunctionLikeDeclaration, isIdentifier, isMethodDeclaration, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAssignment, isReturnStatement, isSatisfiesExpression, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isPropertyDeclaration, isVariableDeclaration, isVariableStatement, SyntaxKind, } from "@typescript/native/ast";
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
12
|
import { filesNamedBy, outsideChain, relativeSpecifiers, } from "./configuration-files.js";
|
|
13
13
|
import { conventionEntries } from "./conventions.js";
|
|
@@ -260,6 +260,10 @@ export function roots(project, held, targetRoot, options) {
|
|
|
260
260
|
published.push(...entryFiles(entry));
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
+
if (options.publishedAPI) {
|
|
264
|
+
// A library's bundle hands what its entry exports to every program that loads it.
|
|
265
|
+
published.push(...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file));
|
|
266
|
+
}
|
|
263
267
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
264
268
|
const patterned = [];
|
|
265
269
|
for (const pattern of options.entryFiles) {
|
|
@@ -485,14 +489,23 @@ function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
|
|
|
485
489
|
const symbol = byId.get(id);
|
|
486
490
|
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
487
491
|
});
|
|
488
|
-
|
|
492
|
+
const inferred = inferredTypeSymbols(project, round.flatMap((id) => {
|
|
489
493
|
const symbol = byId.get(id);
|
|
490
494
|
return symbol === undefined ? [] : nodes.nodesOf(symbol);
|
|
491
|
-
}))
|
|
495
|
+
}));
|
|
496
|
+
for (const symbol of inferred.types) {
|
|
492
497
|
for (const target of chains.chainOf(symbol)) {
|
|
493
498
|
reach(target.id);
|
|
494
499
|
}
|
|
495
500
|
}
|
|
501
|
+
for (const symbol of inferred.members) {
|
|
502
|
+
for (const target of chains.chainOf(symbol)) {
|
|
503
|
+
const member = byId.get(target.id);
|
|
504
|
+
if (member?.visibility === "public" || member?.visibility === "protected") {
|
|
505
|
+
reach(target.id);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
496
509
|
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
497
510
|
if (symbol === UNANSWERED) {
|
|
498
511
|
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
@@ -544,11 +557,101 @@ function declarationNodes(held, byPath) {
|
|
|
544
557
|
/** How deep the type arguments and constituents of an inferred type are followed. */
|
|
545
558
|
const INFERRED_DEPTH = 4;
|
|
546
559
|
/**
|
|
547
|
-
* The
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
|
|
551
|
-
|
|
560
|
+
* The values one function body returns, each through the type assertions around it: the
|
|
561
|
+
* value a caller receives carries the operand's type whatever type the assertion or the
|
|
562
|
+
* signature names. A nested function's returns are its own.
|
|
563
|
+
*/
|
|
564
|
+
function returnedValues(body) {
|
|
565
|
+
const found = [];
|
|
566
|
+
const add = (value) => {
|
|
567
|
+
for (let at = value; at !== undefined;) {
|
|
568
|
+
found.push(at);
|
|
569
|
+
at =
|
|
570
|
+
isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
|
|
571
|
+
? at.expression
|
|
572
|
+
: undefined;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
|
|
576
|
+
return found;
|
|
577
|
+
}
|
|
578
|
+
if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
|
|
579
|
+
add(body);
|
|
580
|
+
return found;
|
|
581
|
+
}
|
|
582
|
+
const visit = (node) => {
|
|
583
|
+
if (isFunctionLikeDeclaration(node)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (isReturnStatement(node) && node.expression !== undefined) {
|
|
587
|
+
add(node.expression);
|
|
588
|
+
}
|
|
589
|
+
node.forEachChild(visit);
|
|
590
|
+
};
|
|
591
|
+
body.forEachChild(visit);
|
|
592
|
+
return found;
|
|
593
|
+
}
|
|
594
|
+
/** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
|
|
595
|
+
function returnedLiteral(value, body) {
|
|
596
|
+
if (isObjectLiteralExpression(value)) {
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
if (!isIdentifier(value)) {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
let found;
|
|
603
|
+
const visit = (node) => {
|
|
604
|
+
if (isVariableDeclaration(node) &&
|
|
605
|
+
isIdentifier(node.name) &&
|
|
606
|
+
node.name.text === value.text &&
|
|
607
|
+
node.initializer !== undefined) {
|
|
608
|
+
let at = node.initializer;
|
|
609
|
+
while (isAssertionExpression(at) ||
|
|
610
|
+
isSatisfiesExpression(at) ||
|
|
611
|
+
isParenthesizedExpression(at)) {
|
|
612
|
+
at = at.expression;
|
|
613
|
+
}
|
|
614
|
+
found = isObjectLiteralExpression(at) ? at : found;
|
|
615
|
+
}
|
|
616
|
+
if (!isFunctionLikeDeclaration(node)) {
|
|
617
|
+
node.forEachChild(visit);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
body.forEachChild(visit);
|
|
621
|
+
return found;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* The values one function body returns, and those each method of an object literal it
|
|
625
|
+
* returns returns, to a fixed depth: a caller holding the literal calls its methods, and
|
|
626
|
+
* receives what they return.
|
|
627
|
+
*/
|
|
628
|
+
function returnedThroughLiterals(body, depth = 0) {
|
|
629
|
+
const values = returnedValues(body);
|
|
630
|
+
if (depth >= INFERRED_DEPTH) {
|
|
631
|
+
return values;
|
|
632
|
+
}
|
|
633
|
+
const found = [...values];
|
|
634
|
+
for (const value of values) {
|
|
635
|
+
for (const property of returnedLiteral(value, body)?.properties ?? []) {
|
|
636
|
+
const fn = isMethodDeclaration(property)
|
|
637
|
+
? property
|
|
638
|
+
: isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
|
|
639
|
+
? property.initializer
|
|
640
|
+
: undefined;
|
|
641
|
+
if (fn?.body !== undefined) {
|
|
642
|
+
found.push(...returnedThroughLiterals(fn.body, depth + 1));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return found;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* The declarations of the types the compiler infers where a declaration writes none, and
|
|
650
|
+
* of the values a function returns: the type of a variable or a property with no
|
|
651
|
+
* annotation, the result of a function or a method with no result annotation, and the type
|
|
652
|
+
* of each value its body returns, each with the types its type arguments and its union or
|
|
653
|
+
* intersection constituents name, to a fixed depth; and as `members` the properties of each
|
|
654
|
+
* returned value's type. A question the checker leaves unanswered names nothing.
|
|
552
655
|
*/
|
|
553
656
|
function inferredTypeSymbols(project, declarations) {
|
|
554
657
|
const { queries } = project;
|
|
@@ -557,12 +660,24 @@ function inferredTypeSymbols(project, declarations) {
|
|
|
557
660
|
!isFunctionLikeDeclaration(node.initializer ?? node)
|
|
558
661
|
? [node.name]
|
|
559
662
|
: []);
|
|
663
|
+
const returned = declarations.flatMap((node) => {
|
|
664
|
+
const signature = isFunctionLikeDeclaration(node)
|
|
665
|
+
? node
|
|
666
|
+
: isVariableDeclaration(node) &&
|
|
667
|
+
node.initializer !== undefined &&
|
|
668
|
+
isFunctionLikeDeclaration(node.initializer)
|
|
669
|
+
? node.initializer
|
|
670
|
+
: undefined;
|
|
671
|
+
return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
|
|
672
|
+
});
|
|
560
673
|
const types = [];
|
|
561
|
-
|
|
674
|
+
const returnedTypes = [];
|
|
675
|
+
queries.typesAt([...valued, ...returned]).forEach((type, at) => {
|
|
562
676
|
if (type !== UNANSWERED && type !== undefined) {
|
|
563
|
-
types.push(type);
|
|
677
|
+
(at < valued.length ? types : returnedTypes).push(type);
|
|
564
678
|
}
|
|
565
679
|
});
|
|
680
|
+
types.push(...returnedTypes);
|
|
566
681
|
for (const node of declarations) {
|
|
567
682
|
const signature = isFunctionLikeDeclaration(node)
|
|
568
683
|
? node
|
|
@@ -604,7 +719,19 @@ function inferredTypeSymbols(project, declarations) {
|
|
|
604
719
|
types.forEach((type) => {
|
|
605
720
|
visit(type, 0);
|
|
606
721
|
});
|
|
607
|
-
|
|
722
|
+
// A returned value carries each property of its type, one an anonymous type declares or
|
|
723
|
+
// an intersection merges included, which no type name reaches.
|
|
724
|
+
const members = new Map();
|
|
725
|
+
for (const type of returnedTypes) {
|
|
726
|
+
const parts = queries.constituents(type);
|
|
727
|
+
for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
|
|
728
|
+
const properties = queries.propertiesOf(one);
|
|
729
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
730
|
+
members.set(property.id, property);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return { types: [...found.values()], members: [...members.values()] };
|
|
608
735
|
}
|
|
609
736
|
/**
|
|
610
737
|
* The names of types one declaration's own syntax writes: in a type reference, a
|