@cplieger/deadset-ts 5.0.0 → 5.1.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/dist/src/analysis.js +77 -10
- package/dist/src/configuration-files.js +107 -3
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +21 -17
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +4 -1
- package/dist/src/kinds.js +4 -3
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +199 -10
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +1 -1
- package/dist/src/stores.js +3 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +92 -13
- package/src/configuration-files.ts +136 -1
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +48 -17
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +4 -1
- package/src/kinds.ts +5 -3
- package/src/query.ts +26 -0
- package/src/references.ts +254 -21
- package/src/resolve.ts +14 -13
- package/src/roots.ts +11 -1
- package/src/stores.ts +9 -0
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
|
@@ -14,33 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
isArrayLiteralExpression,
|
|
18
|
-
isArrowFunction,
|
|
19
|
-
isAsExpression,
|
|
20
|
-
isBinaryExpression,
|
|
21
|
-
isCallExpression,
|
|
22
17
|
isClassDeclaration,
|
|
23
18
|
isClassExpression,
|
|
24
19
|
isClassStaticBlockDeclaration,
|
|
25
|
-
isConditionalExpression,
|
|
26
20
|
isFunctionDeclaration,
|
|
27
21
|
isFunctionExpression,
|
|
28
22
|
isGetAccessorDeclaration,
|
|
29
23
|
isHeritageClause,
|
|
30
|
-
isIdentifier,
|
|
31
24
|
isMethodDeclaration,
|
|
32
|
-
isNewExpression,
|
|
33
25
|
isObjectLiteralExpression,
|
|
34
|
-
isParameterDeclaration,
|
|
35
|
-
isParenthesizedExpression,
|
|
36
|
-
isPropertyAssignment,
|
|
37
|
-
isPropertyDeclaration,
|
|
38
|
-
isReturnStatement,
|
|
39
|
-
isSatisfiesExpression,
|
|
40
26
|
isSetAccessorDeclaration,
|
|
41
|
-
isShorthandPropertyAssignment,
|
|
42
|
-
isSpreadElement,
|
|
43
|
-
isVariableDeclaration,
|
|
44
27
|
ModifierFlags,
|
|
45
28
|
SyntaxKind,
|
|
46
29
|
type Expression,
|
|
@@ -53,6 +36,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
|
53
36
|
import { nodeKey } from "./inventory.ts";
|
|
54
37
|
import { renderPosition } from "./position.ts";
|
|
55
38
|
import { must } from "./query.ts";
|
|
39
|
+
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
56
40
|
|
|
57
41
|
/** One class the inventory holds, by its declaration. */
|
|
58
42
|
interface ClassAt {
|
|
@@ -85,14 +69,6 @@ interface Pair {
|
|
|
85
69
|
readonly sites: Node[];
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
89
|
-
const STORES: ReadonlySet<SyntaxKind> = new Set([
|
|
90
|
-
SyntaxKind.EqualsToken,
|
|
91
|
-
SyntaxKind.QuestionQuestionEqualsToken,
|
|
92
|
-
SyntaxKind.BarBarEqualsToken,
|
|
93
|
-
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
72
|
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
97
73
|
function isStatic(node: Node): boolean {
|
|
98
74
|
const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
|
|
@@ -109,73 +85,6 @@ function isObjectLiteralMethod(node: Node): boolean {
|
|
|
109
85
|
);
|
|
110
86
|
}
|
|
111
87
|
|
|
112
|
-
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
113
|
-
function writesType(node: Node): boolean {
|
|
114
|
-
return (node as { readonly type?: Node }).type !== undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The values one written expression hands on: the expression itself, or what it
|
|
119
|
-
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
120
|
-
* one of their operands.
|
|
121
|
-
*/
|
|
122
|
-
function valuesOf(expression: Expression): readonly Expression[] {
|
|
123
|
-
if (isParenthesizedExpression(expression)) {
|
|
124
|
-
return valuesOf(expression.expression);
|
|
125
|
-
}
|
|
126
|
-
if (isConditionalExpression(expression)) {
|
|
127
|
-
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
128
|
-
}
|
|
129
|
-
if (isBinaryExpression(expression)) {
|
|
130
|
-
const operator = expression.operatorToken.kind;
|
|
131
|
-
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
132
|
-
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
133
|
-
}
|
|
134
|
-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
135
|
-
return valuesOf(expression.right);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return [expression];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The expressions one node hands to a position typed by its context. */
|
|
142
|
-
function handedOn(node: Node): readonly Expression[] {
|
|
143
|
-
if (
|
|
144
|
-
(isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
145
|
-
node.initializer !== undefined &&
|
|
146
|
-
writesType(node)
|
|
147
|
-
) {
|
|
148
|
-
return [node.initializer];
|
|
149
|
-
}
|
|
150
|
-
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
151
|
-
return [node.right];
|
|
152
|
-
}
|
|
153
|
-
if (isCallExpression(node) || isNewExpression(node)) {
|
|
154
|
-
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
155
|
-
}
|
|
156
|
-
if (isReturnStatement(node)) {
|
|
157
|
-
return node.expression === undefined ? [] : [node.expression];
|
|
158
|
-
}
|
|
159
|
-
if (isArrowFunction(node)) {
|
|
160
|
-
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
161
|
-
}
|
|
162
|
-
if (isArrayLiteralExpression(node)) {
|
|
163
|
-
return node.elements.filter(
|
|
164
|
-
(element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
if (isPropertyAssignment(node)) {
|
|
168
|
-
return [node.initializer];
|
|
169
|
-
}
|
|
170
|
-
if (isShorthandPropertyAssignment(node)) {
|
|
171
|
-
return isIdentifier(node.name) ? [node.name] : [];
|
|
172
|
-
}
|
|
173
|
-
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
174
|
-
return [node.expression];
|
|
175
|
-
}
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
88
|
/** What one project's walk found to resolve. */
|
|
180
89
|
interface Walked {
|
|
181
90
|
readonly classes: readonly ClassAt[];
|
package/src/inventory.ts
CHANGED
|
@@ -303,7 +303,10 @@ function typeParametersOf(node: Node): readonly Node[] {
|
|
|
303
303
|
if (
|
|
304
304
|
isFunctionDeclaration(node) ||
|
|
305
305
|
isMethodDeclaration(node) ||
|
|
306
|
-
isMethodSignatureDeclaration(node)
|
|
306
|
+
isMethodSignatureDeclaration(node) ||
|
|
307
|
+
isClassDeclaration(node) ||
|
|
308
|
+
isInterfaceDeclaration(node) ||
|
|
309
|
+
isTypeAliasDeclaration(node)
|
|
307
310
|
) {
|
|
308
311
|
return node.typeParameters ?? [];
|
|
309
312
|
}
|
package/src/kinds.ts
CHANGED
|
@@ -60,6 +60,8 @@ const ROWS: readonly KindRow[] = [
|
|
|
60
60
|
name: "unused-member",
|
|
61
61
|
languages: ["go", "ts"],
|
|
62
62
|
rule: "A struct field, class member or type member with no reference, a private member included. A member referenced only from its own declaration site counts as unreferenced.",
|
|
63
|
+
precondition:
|
|
64
|
+
"A TypeScript member that no value writes and nothing reads, whose type names a type parameter of its declaring interface, class or type alias, is not reported when every part of that declaration that names the type parameter is a member no value writes and nothing reads. TypeScript compares object types by their members, so such members are what make two instantiations of their type distinct types, and deleting them leaves the type parameter phantom and lets one instantiation stand for another. A member a value writes is decided by the rule of DS1301 instead, whatever its type names.",
|
|
63
65
|
defaultSeverity: "deny",
|
|
64
66
|
maxClass: "certain",
|
|
65
67
|
fixability: "deletable",
|
|
@@ -101,7 +103,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
101
103
|
languages: ["go", "ts"],
|
|
102
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, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which 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.",
|
|
103
105
|
precondition:
|
|
104
|
-
"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.",
|
|
106
|
+
"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 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.",
|
|
105
107
|
defaultSeverity: "warn",
|
|
106
108
|
maxClass: "certain",
|
|
107
109
|
fixability: "narrowable",
|
|
@@ -113,7 +115,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
113
115
|
languages: ["go"],
|
|
114
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, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which 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.",
|
|
115
117
|
precondition:
|
|
116
|
-
"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.",
|
|
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.",
|
|
117
119
|
defaultSeverity: "warn",
|
|
118
120
|
maxClass: "certain",
|
|
119
121
|
fixability: "narrowable",
|
|
@@ -177,7 +179,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
177
179
|
code: "DS1301",
|
|
178
180
|
name: "write-only-symbol",
|
|
179
181
|
languages: ["go", "ts"],
|
|
180
|
-
rule: "A package-level or module-level variable, struct field, class member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. The finding names each write position, so the deletion set is visible.",
|
|
182
|
+
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, or an import binding the writes alone use. The finding's message names each such declaration.",
|
|
181
183
|
defaultSeverity: "deny",
|
|
182
184
|
maxClass: "certain",
|
|
183
185
|
fixability: "deletable",
|
package/src/query.ts
CHANGED
|
@@ -127,6 +127,32 @@ interface Assignability {
|
|
|
127
127
|
readonly target: Type;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Each type's properties, asked of the checker once per type for every pass that shares
|
|
132
|
+
* the tables: the client keeps a type's properties once fetched, so a second pass asking
|
|
133
|
+
* for the same type makes no request, and only the first ask is counted as one.
|
|
134
|
+
*/
|
|
135
|
+
export interface PropertyTables {
|
|
136
|
+
/** One type's properties, and whether this call was the one that asked for them. */
|
|
137
|
+
of(type: Type): { readonly properties: Answer<readonly TSSymbol[]>; readonly asked: boolean };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The property tables over one project's questions. */
|
|
141
|
+
export function propertyTables(queries: Pick<Queries, "propertiesOf">): PropertyTables {
|
|
142
|
+
const held = new Map<number, Answer<readonly TSSymbol[]>>();
|
|
143
|
+
return {
|
|
144
|
+
of: (type) => {
|
|
145
|
+
const known = held.get(type.id);
|
|
146
|
+
if (known !== undefined) {
|
|
147
|
+
return { properties: known, asked: false };
|
|
148
|
+
}
|
|
149
|
+
const properties = queries.propertiesOf(type);
|
|
150
|
+
held.set(type.id, properties);
|
|
151
|
+
return { properties, asked: true };
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
/** The questions one project's analysis asks, each guarded. */
|
|
131
157
|
export interface Queries {
|
|
132
158
|
/** The symbol each node resolves to. */
|
package/src/references.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isArrayLiteralExpression,
|
|
3
|
+
isAsExpression,
|
|
3
4
|
isBinaryExpression,
|
|
4
5
|
isCallExpression,
|
|
5
6
|
isDecorator,
|
|
@@ -32,14 +33,16 @@ import {
|
|
|
32
33
|
} from "@typescript/native/unstable/ast";
|
|
33
34
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
34
35
|
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
36
|
+
import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.ts";
|
|
35
37
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
36
38
|
import { globExpression } from "./glob.ts";
|
|
37
39
|
import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
38
40
|
import { relativePath } from "./paths.ts";
|
|
39
41
|
import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
|
|
40
|
-
import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
|
|
42
|
+
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
|
|
41
43
|
import type { ProjectView } from "./session.ts";
|
|
42
44
|
import { typeQueryAliases } from "./type-query-alias.ts";
|
|
45
|
+
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Every use one project's own files make of the project's own declarations.
|
|
@@ -68,15 +71,22 @@ import { typeQueryAliases } from "./type-query-alias.ts";
|
|
|
68
71
|
export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
|
-
* Which path answered for one reference: `batch`, the batched lookup over a file's
|
|
72
|
-
* `resolved-symbol`, the per-node lookup for
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* `by-name`, every declaration
|
|
74
|
+
* Which path answered for one reference: `batch`, the batched lookup over a file's names;
|
|
75
|
+
* `resolved-symbol`, the per-node lookup for a batch's residue; `shorthand`, the value
|
|
76
|
+
* `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the tree alone
|
|
77
|
+
* (a decorator's target, a component's markup); `destructured`, the property a pattern
|
|
78
|
+
* names; `contextual`, the member a literal property writes or a value's position reads;
|
|
79
|
+
* `by-name`, every declaration spelled as the site is, where the checker failed.
|
|
77
80
|
*/
|
|
78
81
|
export type Resolution =
|
|
79
|
-
|
|
82
|
+
| "batch"
|
|
83
|
+
| "resolved-symbol"
|
|
84
|
+
| "shorthand"
|
|
85
|
+
| "alias"
|
|
86
|
+
| "syntax"
|
|
87
|
+
| "destructured"
|
|
88
|
+
| "contextual"
|
|
89
|
+
| "by-name";
|
|
80
90
|
|
|
81
91
|
/** One use of one declaration by one declaration. */
|
|
82
92
|
export interface Reference {
|
|
@@ -102,6 +112,12 @@ export interface Reference {
|
|
|
102
112
|
* the consumer's root.
|
|
103
113
|
*/
|
|
104
114
|
readonly consumer?: string;
|
|
115
|
+
/**
|
|
116
|
+
* On a write, the local names of the writing file's import bindings that only the
|
|
117
|
+
* values its file writes into the same declaration use, which deleting the
|
|
118
|
+
* declaration with its writes deletes too.
|
|
119
|
+
*/
|
|
120
|
+
readonly alone?: readonly string[];
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
/** The consumer one reference pass reads the files of, beside the target it names. */
|
|
@@ -148,6 +164,13 @@ export interface ReferenceCost {
|
|
|
148
164
|
* pattern destructures, and each step a nested assignment target takes into its value.
|
|
149
165
|
*/
|
|
150
166
|
readonly patternLookups: number;
|
|
167
|
+
/**
|
|
168
|
+
* Batched lookups for the contextual uses: the contextual types of the object literals
|
|
169
|
+
* and of the values that carry a member, and the values' types, each per capped run.
|
|
170
|
+
*/
|
|
171
|
+
readonly contextualBatches: number;
|
|
172
|
+
/** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
|
|
173
|
+
readonly contextualLookups: number;
|
|
151
174
|
}
|
|
152
175
|
|
|
153
176
|
/** One project's references, in position order, and what reading them cost. */
|
|
@@ -280,7 +303,11 @@ function markWrite(target: Node, writes: Set<number>): void {
|
|
|
280
303
|
return;
|
|
281
304
|
}
|
|
282
305
|
if (isElementAccessExpression(held)) {
|
|
283
|
-
|
|
306
|
+
// A store through a member's value lands in storage every holder of the value
|
|
307
|
+
// reads, so it reads the member; a variable only ever stored into stays written.
|
|
308
|
+
if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
|
|
309
|
+
markWrite(held.expression, writes);
|
|
310
|
+
}
|
|
284
311
|
return;
|
|
285
312
|
}
|
|
286
313
|
if (isArrayLiteralExpression(held)) {
|
|
@@ -407,6 +434,8 @@ interface Read extends PropertyRead {
|
|
|
407
434
|
/** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
|
|
408
435
|
interface FileSites {
|
|
409
436
|
readonly uses: readonly Site[];
|
|
437
|
+
readonly literals: readonly LiteralAt[];
|
|
438
|
+
readonly flows: readonly FlowAt[];
|
|
410
439
|
readonly reads: readonly Read[];
|
|
411
440
|
readonly links: readonly Link[];
|
|
412
441
|
readonly evaluations: readonly Evaluation[];
|
|
@@ -491,6 +520,30 @@ function linksOf(
|
|
|
491
520
|
return found;
|
|
492
521
|
}
|
|
493
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Whether one object literal is a destructuring target, written on the left of an
|
|
525
|
+
* assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
|
|
526
|
+
*/
|
|
527
|
+
function isAssignmentPattern(node: Node): boolean {
|
|
528
|
+
let child = node;
|
|
529
|
+
let parent = node.parent;
|
|
530
|
+
while (
|
|
531
|
+
isParenthesizedExpression(parent) ||
|
|
532
|
+
isArrayLiteralExpression(parent) ||
|
|
533
|
+
isObjectLiteralExpression(parent) ||
|
|
534
|
+
isSpreadElement(parent) ||
|
|
535
|
+
isSpreadAssignment(parent) ||
|
|
536
|
+
(isPropertyAssignment(parent) && parent.initializer === child)
|
|
537
|
+
) {
|
|
538
|
+
child = parent;
|
|
539
|
+
parent = parent.parent;
|
|
540
|
+
}
|
|
541
|
+
if (isBinaryExpression(parent)) {
|
|
542
|
+
return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
|
|
543
|
+
}
|
|
544
|
+
return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
|
|
545
|
+
}
|
|
546
|
+
|
|
494
547
|
/**
|
|
495
548
|
* Every name node one file holds that is a use rather than a declaration, in source
|
|
496
549
|
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
@@ -510,6 +563,8 @@ function sitesOf(
|
|
|
510
563
|
const links: Link[] = [];
|
|
511
564
|
const evaluations: Evaluation[] = [];
|
|
512
565
|
const decorated: Decorated[] = [];
|
|
566
|
+
const literals: LiteralAt[] = [];
|
|
567
|
+
const flows: FlowAt[] = [];
|
|
513
568
|
const writes = new Set<number>();
|
|
514
569
|
const alsoRead = new Set<number>();
|
|
515
570
|
const declared = new Set<number>();
|
|
@@ -552,6 +607,13 @@ function sitesOf(
|
|
|
552
607
|
}
|
|
553
608
|
markNames(node);
|
|
554
609
|
markStores(node, writes, alsoRead);
|
|
610
|
+
if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
|
|
611
|
+
literals.push({ literal: node, from: enclosing });
|
|
612
|
+
}
|
|
613
|
+
// An assertion needs only comparability, so no member of its operand is read by it.
|
|
614
|
+
for (const written of isAsExpression(node) ? [] : handedOn(node)) {
|
|
615
|
+
flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
|
|
616
|
+
}
|
|
555
617
|
for (const read of propertyReadsOf(node)) {
|
|
556
618
|
reads.push({ ...read, from: enclosing });
|
|
557
619
|
}
|
|
@@ -575,9 +637,18 @@ function sitesOf(
|
|
|
575
637
|
};
|
|
576
638
|
|
|
577
639
|
file.forEachChild(visit);
|
|
578
|
-
return { uses: found, reads, links, evaluations, decorated };
|
|
640
|
+
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
579
641
|
}
|
|
580
642
|
|
|
643
|
+
/** The declaration kinds of a property an object literal declares. */
|
|
644
|
+
const LITERAL_PROPERTY_KINDS: ReadonlySet<SyntaxKind> = new Set([
|
|
645
|
+
SyntaxKind.PropertyAssignment,
|
|
646
|
+
SyntaxKind.ShorthandPropertyAssignment,
|
|
647
|
+
SyntaxKind.MethodDeclaration,
|
|
648
|
+
SyntaxKind.GetAccessor,
|
|
649
|
+
SyntaxKind.SetAccessor,
|
|
650
|
+
]);
|
|
651
|
+
|
|
581
652
|
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
582
653
|
const TEST_SUPPORT_RULE = "test-support";
|
|
583
654
|
|
|
@@ -691,6 +762,70 @@ function importBindings(file: SourceFile): readonly { node: Node; whole: boolean
|
|
|
691
762
|
return found;
|
|
692
763
|
}
|
|
693
764
|
|
|
765
|
+
/** The value one assignment stores through the name it writes, where it is a plain assignment. */
|
|
766
|
+
function storedValue(name: Node): Node | undefined {
|
|
767
|
+
let target: Node = name;
|
|
768
|
+
if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
|
|
769
|
+
target = name.parent;
|
|
770
|
+
}
|
|
771
|
+
while (isParenthesizedExpression(target.parent)) {
|
|
772
|
+
target = target.parent;
|
|
773
|
+
}
|
|
774
|
+
const parent = target.parent;
|
|
775
|
+
return isBinaryExpression(parent) &&
|
|
776
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
777
|
+
parent.left === target
|
|
778
|
+
? parent.right
|
|
779
|
+
: undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* The import bindings of one file that only the values written into one declaration
|
|
784
|
+
* use, for each write into it. A binding is named by an identifier that spells its
|
|
785
|
+
* local name and is not the member a property access names.
|
|
786
|
+
*/
|
|
787
|
+
function aloneIn(
|
|
788
|
+
file: SourceFile,
|
|
789
|
+
uses: readonly Site[],
|
|
790
|
+
writes: readonly { index: number; value: Node }[],
|
|
791
|
+
found: readonly Reference[],
|
|
792
|
+
): readonly { index: number; names: readonly string[] }[] {
|
|
793
|
+
const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
|
|
794
|
+
if (bindings.size === 0) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
const occurrences = new Map<string, Node[]>();
|
|
798
|
+
for (const site of uses) {
|
|
799
|
+
const text = site.node.getText();
|
|
800
|
+
const parent = site.node.parent;
|
|
801
|
+
if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (!occurrences.get(text)?.includes(site.node)) {
|
|
805
|
+
occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const inside = (node: Node, value: Node): boolean =>
|
|
809
|
+
node.pos >= value.pos && node.end <= value.end;
|
|
810
|
+
const result: { index: number; names: readonly string[] }[] = [];
|
|
811
|
+
for (const write of writes) {
|
|
812
|
+
const to = found[write.index]?.to;
|
|
813
|
+
const sameTarget = writes.filter((one) => found[one.index]?.to === to);
|
|
814
|
+
const names = [...occurrences]
|
|
815
|
+
.filter(
|
|
816
|
+
([, nodes]) =>
|
|
817
|
+
nodes.some((node) => inside(node, write.value)) &&
|
|
818
|
+
nodes.every((node) => sameTarget.some((one) => inside(node, one.value))),
|
|
819
|
+
)
|
|
820
|
+
.map(([name]) => name)
|
|
821
|
+
.sort(compare);
|
|
822
|
+
if (names.length > 0) {
|
|
823
|
+
result.push({ index: write.index, names });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
|
|
694
829
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
695
830
|
function compare(a: string, b: string): number {
|
|
696
831
|
if (a === b) {
|
|
@@ -727,7 +862,8 @@ export function references<Brand>(
|
|
|
727
862
|
const found: Reference[] = [];
|
|
728
863
|
const tests: string[] = [];
|
|
729
864
|
const chains = aliasChains(project, held);
|
|
730
|
-
const
|
|
865
|
+
const tables = propertyTables(project.queries);
|
|
866
|
+
const destructured = destructuring(project, cap, tables);
|
|
731
867
|
let batched = 0;
|
|
732
868
|
let fileBatches = 0;
|
|
733
869
|
let residueFallbacks = 0;
|
|
@@ -745,6 +881,7 @@ export function references<Brand>(
|
|
|
745
881
|
relativePath(consumer.root, file.fileName) !== undefined &&
|
|
746
882
|
relativePath(targetRoot, file.fileName) === undefined,
|
|
747
883
|
);
|
|
884
|
+
const ownPaths = project.ownPaths();
|
|
748
885
|
const fileIds = new Set(
|
|
749
886
|
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
750
887
|
);
|
|
@@ -859,19 +996,99 @@ export function references<Brand>(
|
|
|
859
996
|
return project.resolvedSymbolAt(project.handle(site.node));
|
|
860
997
|
};
|
|
861
998
|
|
|
862
|
-
|
|
999
|
+
const walked = files.map((file) => {
|
|
1000
|
+
const fileId = declaredAt(file) ?? "";
|
|
1001
|
+
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
1002
|
+
});
|
|
1003
|
+
const contextual = contextualUses(
|
|
1004
|
+
project,
|
|
1005
|
+
held,
|
|
1006
|
+
chains,
|
|
1007
|
+
walked.flatMap((one) => one.literals),
|
|
1008
|
+
walked.flatMap((one) => one.flows),
|
|
1009
|
+
cap,
|
|
1010
|
+
tables,
|
|
1011
|
+
);
|
|
1012
|
+
const writtenBy = new Map<string, readonly string[]>();
|
|
1013
|
+
for (const write of contextual.writes) {
|
|
1014
|
+
const property = write.at.parent;
|
|
1015
|
+
writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
|
|
1016
|
+
}
|
|
1017
|
+
const forwardedCache = new Map<number, readonly ChainTarget[]>();
|
|
1018
|
+
/**
|
|
1019
|
+
* The members a literal property one symbol declares writes: a use of the property
|
|
1020
|
+
* reaches them, because the literal's type carries the data the member describes.
|
|
1021
|
+
*/
|
|
1022
|
+
const forwarded = (symbol: TSSymbol): readonly ChainTarget[] => {
|
|
1023
|
+
const known = forwardedCache.get(symbol.id);
|
|
1024
|
+
if (known !== undefined) {
|
|
1025
|
+
return known;
|
|
1026
|
+
}
|
|
1027
|
+
const found: ChainTarget[] = [];
|
|
1028
|
+
if (writtenBy.size > 0) {
|
|
1029
|
+
for (const handle of symbol.declarations) {
|
|
1030
|
+
if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
const node = project.declarationAt(handle)?.node;
|
|
1034
|
+
const ids =
|
|
1035
|
+
node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
|
|
1036
|
+
for (const id of ids ?? []) {
|
|
1037
|
+
if (!found.some((target) => target.id === id)) {
|
|
1038
|
+
found.push({ id, stepped: true, guessed: false });
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
forwardedCache.set(symbol.id, found);
|
|
1044
|
+
return found;
|
|
1045
|
+
};
|
|
1046
|
+
/** One resolved symbol's declarations along its chain, and the members its literal properties write. */
|
|
1047
|
+
const targetsOf = (symbol: TSSymbol): readonly ChainTarget[] => {
|
|
1048
|
+
const through = forwarded(symbol);
|
|
1049
|
+
return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
|
|
1050
|
+
};
|
|
1051
|
+
const testFile = new Map<SourceFile, boolean>();
|
|
1052
|
+
for (const { file } of walked) {
|
|
863
1053
|
const path = renderPosition(file, root, 0).path;
|
|
864
1054
|
const test = patterns.some((pattern) => pattern.test(path));
|
|
1055
|
+
testFile.set(file, test);
|
|
865
1056
|
if (test) {
|
|
866
1057
|
tests.push(path);
|
|
867
1058
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1059
|
+
}
|
|
1060
|
+
/** Each write reference's index in `found`, by its file, with the value it stores. */
|
|
1061
|
+
const valued = new Map<SourceFile, { index: number; value: Node }[]>();
|
|
1062
|
+
const holdsValue = (file: SourceFile, value: Node | undefined): void => {
|
|
1063
|
+
if (value !== undefined) {
|
|
1064
|
+
valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
for (const [use, sites] of [
|
|
1068
|
+
["write", contextual.writes],
|
|
1069
|
+
["read", contextual.reads],
|
|
1070
|
+
] as const) {
|
|
1071
|
+
for (const site of sites) {
|
|
1072
|
+
const file = site.at.getSourceFile();
|
|
1073
|
+
const position = renderPosition(file, root, site.at.getStart());
|
|
1074
|
+
for (const id of site.ids) {
|
|
1075
|
+
found.push({
|
|
1076
|
+
from: site.from,
|
|
1077
|
+
to: id,
|
|
1078
|
+
position,
|
|
1079
|
+
use,
|
|
1080
|
+
resolution: "contextual",
|
|
1081
|
+
test: testFile.get(file) === true,
|
|
1082
|
+
});
|
|
1083
|
+
if (use === "write") {
|
|
1084
|
+
holdsValue(file, site.at.parent);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
1091
|
+
const test = testFile.get(file) === true;
|
|
875
1092
|
// A component file's markup may use any binding its blocks declare at the top level.
|
|
876
1093
|
const markup = isComponentFile(file);
|
|
877
1094
|
const bindings = markup ? importBindings(file) : [];
|
|
@@ -1022,7 +1239,7 @@ export function references<Brand>(
|
|
|
1022
1239
|
stepped: false,
|
|
1023
1240
|
guessed: true,
|
|
1024
1241
|
}))
|
|
1025
|
-
:
|
|
1242
|
+
: targetsOf(symbol);
|
|
1026
1243
|
for (const target of targets) {
|
|
1027
1244
|
found.push({
|
|
1028
1245
|
from: read.from,
|
|
@@ -1052,7 +1269,7 @@ export function references<Brand>(
|
|
|
1052
1269
|
const targets = throughAliases(
|
|
1053
1270
|
symbol === UNANSWERED
|
|
1054
1271
|
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
1055
|
-
:
|
|
1272
|
+
: targetsOf(symbol),
|
|
1056
1273
|
);
|
|
1057
1274
|
for (const target of targets) {
|
|
1058
1275
|
found.push({
|
|
@@ -1063,6 +1280,20 @@ export function references<Brand>(
|
|
|
1063
1280
|
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
1064
1281
|
test,
|
|
1065
1282
|
});
|
|
1283
|
+
if (site.use === "write") {
|
|
1284
|
+
holdsValue(file, storedValue(site.node));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1291
|
+
for (const [file, writes] of valued) {
|
|
1292
|
+
const uses = usesOf.get(file) ?? [];
|
|
1293
|
+
for (const { index, names } of aloneIn(file, uses, writes, found)) {
|
|
1294
|
+
const reference = found[index];
|
|
1295
|
+
if (reference !== undefined) {
|
|
1296
|
+
found[index] = { ...reference, alone: names };
|
|
1066
1297
|
}
|
|
1067
1298
|
}
|
|
1068
1299
|
}
|
|
@@ -1090,6 +1321,8 @@ export function references<Brand>(
|
|
|
1090
1321
|
aliasSteps: chains.steps,
|
|
1091
1322
|
patternBatches: destructured.cost.patternBatches,
|
|
1092
1323
|
patternLookups: destructured.cost.patternLookups,
|
|
1324
|
+
contextualBatches: contextual.batches,
|
|
1325
|
+
contextualLookups: contextual.lookups,
|
|
1093
1326
|
},
|
|
1094
1327
|
};
|
|
1095
1328
|
}
|