@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
|
@@ -114,6 +114,43 @@ function messageOf(code, judged) {
|
|
|
114
114
|
}
|
|
115
115
|
return `${subject} has no reference in the target`;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The members nothing writes or reads that are brands: each names a type parameter of
|
|
119
|
+
* its declaring type that no other part of the declaration names, other than another
|
|
120
|
+
* such member. They are what make two instantiations of the type distinct types, so
|
|
121
|
+
* deleting them would let one instantiation stand for another.
|
|
122
|
+
*/
|
|
123
|
+
function brandMembers(input) {
|
|
124
|
+
const union = input.swept.matrix.union;
|
|
125
|
+
const unreferenced = new Map(input.swept.sweep.candidates
|
|
126
|
+
.filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
|
|
127
|
+
.map((candidate) => [candidate.id, candidate]));
|
|
128
|
+
const namedBy = new Map();
|
|
129
|
+
for (const reference of input.stores.references) {
|
|
130
|
+
const to = union.symbols[union.at(reference.to)];
|
|
131
|
+
if (to?.kind === "type-parameter") {
|
|
132
|
+
namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const brands = new Set();
|
|
136
|
+
for (const [parameter, from] of namedBy) {
|
|
137
|
+
const owner = union.symbols[union.at(parameter)]?.parent;
|
|
138
|
+
const members = [...from].map((id) => union.symbols[union.at(id)]);
|
|
139
|
+
if (members.every((member) => member !== undefined &&
|
|
140
|
+
member.parent === owner &&
|
|
141
|
+
BRANDS.has(member.kind) &&
|
|
142
|
+
unreferenced.has(member.id))) {
|
|
143
|
+
for (const member of members) {
|
|
144
|
+
if (member !== undefined) {
|
|
145
|
+
brands.add(member.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return brands;
|
|
151
|
+
}
|
|
152
|
+
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
153
|
+
const BRANDS = new Set(["type-member", "class-member"]);
|
|
117
154
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
118
155
|
export const unusedDeclarations = (input) => familyFindings(input, false);
|
|
119
156
|
/**
|
|
@@ -127,6 +164,7 @@ function familyFindings(input, fallen) {
|
|
|
127
164
|
const union = input.swept.matrix.union;
|
|
128
165
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
129
166
|
const unreachable = unreachableExports(input);
|
|
167
|
+
let brands;
|
|
130
168
|
const found = [];
|
|
131
169
|
for (const candidate of input.swept.sweep.candidates) {
|
|
132
170
|
const at = union.at(candidate.id);
|
|
@@ -148,7 +186,8 @@ function familyFindings(input, fallen) {
|
|
|
148
186
|
unreachable: unreachable.has(symbol.id),
|
|
149
187
|
};
|
|
150
188
|
const code = codeOf(judged);
|
|
151
|
-
if (code === undefined
|
|
189
|
+
if (code === undefined ||
|
|
190
|
+
(code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))) {
|
|
152
191
|
continue;
|
|
153
192
|
}
|
|
154
193
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated-file class on TypeScript: every declaration in a file below a directory
|
|
3
|
+
* an applied convention row names as generated is retained, because the framework owns
|
|
4
|
+
* the file and a deletion there is undone at the next generation.
|
|
5
|
+
*/
|
|
6
|
+
/** The generated-file detector, recording each file's first line. */
|
|
7
|
+
export function generatedFile(input) {
|
|
8
|
+
const generated = input.generated;
|
|
9
|
+
if (generated === undefined || generated.size === 0) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return input.held.symbols.flatMap((symbol) => {
|
|
13
|
+
const row = generated.get(symbol.position.path);
|
|
14
|
+
return symbol.kind === "file" || row === undefined
|
|
15
|
+
? []
|
|
16
|
+
: [
|
|
17
|
+
{
|
|
18
|
+
id: symbol.id,
|
|
19
|
+
detail: `generated below a directory the convention row ${row} names`,
|
|
20
|
+
site: { path: symbol.position.path, line: 1, column: 1 },
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -12,19 +12,13 @@
|
|
|
12
12
|
* the contextual lookup has none, so it is asked only of a value whose type is a class
|
|
13
13
|
* of the target, and each distinct pair, interface and class is asked once.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
16
16
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
17
17
|
import { aliasChains } from "./alias-chain.js";
|
|
18
18
|
import { nodeKey } from "./inventory.js";
|
|
19
19
|
import { renderPosition } from "./position.js";
|
|
20
20
|
import { must } from "./query.js";
|
|
21
|
-
|
|
22
|
-
const STORES = new Set([
|
|
23
|
-
SyntaxKind.EqualsToken,
|
|
24
|
-
SyntaxKind.QuestionQuestionEqualsToken,
|
|
25
|
-
SyntaxKind.BarBarEqualsToken,
|
|
26
|
-
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
27
|
-
]);
|
|
21
|
+
import { handedOn, valuesOf } from "./value-flow.js";
|
|
28
22
|
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
29
23
|
function isStatic(node) {
|
|
30
24
|
const flags = node.modifierFlags ?? 0;
|
|
@@ -37,66 +31,6 @@ function isObjectLiteralMethod(node) {
|
|
|
37
31
|
isSetAccessorDeclaration(node)) &&
|
|
38
32
|
isObjectLiteralExpression(node.parent));
|
|
39
33
|
}
|
|
40
|
-
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
41
|
-
function writesType(node) {
|
|
42
|
-
return node.type !== undefined;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* The values one written expression hands on: the expression itself, or what it
|
|
46
|
-
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
47
|
-
* one of their operands.
|
|
48
|
-
*/
|
|
49
|
-
function valuesOf(expression) {
|
|
50
|
-
if (isParenthesizedExpression(expression)) {
|
|
51
|
-
return valuesOf(expression.expression);
|
|
52
|
-
}
|
|
53
|
-
if (isConditionalExpression(expression)) {
|
|
54
|
-
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
55
|
-
}
|
|
56
|
-
if (isBinaryExpression(expression)) {
|
|
57
|
-
const operator = expression.operatorToken.kind;
|
|
58
|
-
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
59
|
-
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
60
|
-
}
|
|
61
|
-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
62
|
-
return valuesOf(expression.right);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return [expression];
|
|
66
|
-
}
|
|
67
|
-
/** The expressions one node hands to a position typed by its context. */
|
|
68
|
-
function handedOn(node) {
|
|
69
|
-
if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
70
|
-
node.initializer !== undefined &&
|
|
71
|
-
writesType(node)) {
|
|
72
|
-
return [node.initializer];
|
|
73
|
-
}
|
|
74
|
-
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
75
|
-
return [node.right];
|
|
76
|
-
}
|
|
77
|
-
if (isCallExpression(node) || isNewExpression(node)) {
|
|
78
|
-
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
79
|
-
}
|
|
80
|
-
if (isReturnStatement(node)) {
|
|
81
|
-
return node.expression === undefined ? [] : [node.expression];
|
|
82
|
-
}
|
|
83
|
-
if (isArrowFunction(node)) {
|
|
84
|
-
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
85
|
-
}
|
|
86
|
-
if (isArrayLiteralExpression(node)) {
|
|
87
|
-
return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
|
|
88
|
-
}
|
|
89
|
-
if (isPropertyAssignment(node)) {
|
|
90
|
-
return [node.initializer];
|
|
91
|
-
}
|
|
92
|
-
if (isShorthandPropertyAssignment(node)) {
|
|
93
|
-
return isIdentifier(node.name) ? [node.name] : [];
|
|
94
|
-
}
|
|
95
|
-
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
96
|
-
return [node.expression];
|
|
97
|
-
}
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
34
|
/**
|
|
101
35
|
* The classes, the `implements` entries and the flowing values of one project's own
|
|
102
36
|
* files. `this` is the class an instance member belongs to, so a function that is not
|
package/dist/src/inventory.js
CHANGED
|
@@ -69,7 +69,10 @@ function modifiersOf(node) {
|
|
|
69
69
|
function typeParametersOf(node) {
|
|
70
70
|
if (isFunctionDeclaration(node) ||
|
|
71
71
|
isMethodDeclaration(node) ||
|
|
72
|
-
isMethodSignatureDeclaration(node)
|
|
72
|
+
isMethodSignatureDeclaration(node) ||
|
|
73
|
+
isClassDeclaration(node) ||
|
|
74
|
+
isInterfaceDeclaration(node) ||
|
|
75
|
+
isTypeAliasDeclaration(node)) {
|
|
73
76
|
return node.typeParameters ?? [];
|
|
74
77
|
}
|
|
75
78
|
return [];
|
package/dist/src/kinds.js
CHANGED
|
@@ -30,6 +30,7 @@ const ROWS = [
|
|
|
30
30
|
name: "unused-member",
|
|
31
31
|
languages: ["go", "ts"],
|
|
32
32
|
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.",
|
|
33
|
+
precondition: "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.",
|
|
33
34
|
defaultSeverity: "deny",
|
|
34
35
|
maxClass: "certain",
|
|
35
36
|
fixability: "deletable",
|
|
@@ -70,7 +71,7 @@ const ROWS = [
|
|
|
70
71
|
name: "unnecessary-export",
|
|
71
72
|
languages: ["go", "ts"],
|
|
72
73
|
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.",
|
|
73
|
-
precondition: "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.",
|
|
74
|
+
precondition: "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.",
|
|
74
75
|
defaultSeverity: "warn",
|
|
75
76
|
maxClass: "certain",
|
|
76
77
|
fixability: "narrowable",
|
|
@@ -81,7 +82,7 @@ const ROWS = [
|
|
|
81
82
|
name: "unnecessary-exposure",
|
|
82
83
|
languages: ["go"],
|
|
83
84
|
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.",
|
|
84
|
-
precondition: "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.",
|
|
85
|
+
precondition: "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.",
|
|
85
86
|
defaultSeverity: "warn",
|
|
86
87
|
maxClass: "certain",
|
|
87
88
|
fixability: "narrowable",
|
|
@@ -143,7 +144,7 @@ const ROWS = [
|
|
|
143
144
|
code: "DS1301",
|
|
144
145
|
name: "write-only-symbol",
|
|
145
146
|
languages: ["go", "ts"],
|
|
146
|
-
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.",
|
|
147
|
+
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.",
|
|
147
148
|
defaultSeverity: "deny",
|
|
148
149
|
maxClass: "certain",
|
|
149
150
|
fixability: "deletable",
|
package/dist/src/query.js
CHANGED
|
@@ -73,6 +73,21 @@ function symbolLocation(symbol) {
|
|
|
73
73
|
function typeLocation(type) {
|
|
74
74
|
return `type ${String(type.id)}`;
|
|
75
75
|
}
|
|
76
|
+
/** The property tables over one project's questions. */
|
|
77
|
+
export function propertyTables(queries) {
|
|
78
|
+
const held = new Map();
|
|
79
|
+
return {
|
|
80
|
+
of: (type) => {
|
|
81
|
+
const known = held.get(type.id);
|
|
82
|
+
if (known !== undefined) {
|
|
83
|
+
return { properties: known, asked: false };
|
|
84
|
+
}
|
|
85
|
+
const properties = queries.propertiesOf(type);
|
|
86
|
+
held.set(type.id, properties);
|
|
87
|
+
return { properties, asked: true };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
76
91
|
/**
|
|
77
92
|
* The guarded questions over one project's checker. `configFile` names the project
|
|
78
93
|
* in the log, `batch` sends many questions in one round trip, `log` is the run's,
|
package/dist/src/references.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
|
+
import { contextualUses } from "./contextual-uses.js";
|
|
3
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
4
5
|
import { globExpression } from "./glob.js";
|
|
5
6
|
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
6
7
|
import { relativePath } from "./paths.js";
|
|
7
8
|
import { byPosition, isComponentFile, renderPosition } from "./position.js";
|
|
8
|
-
import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
|
|
9
|
+
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
|
|
9
10
|
import { typeQueryAliases } from "./type-query-alias.js";
|
|
11
|
+
import { handedOn, valuesOf } from "./value-flow.js";
|
|
10
12
|
/**
|
|
11
13
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
12
14
|
* among them: where its value is discarded it reads its target only to write the result
|
|
@@ -94,7 +96,11 @@ function markWrite(target, writes) {
|
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
if (isElementAccessExpression(held)) {
|
|
97
|
-
|
|
99
|
+
// A store through a member's value lands in storage every holder of the value
|
|
100
|
+
// reads, so it reads the member; a variable only ever stored into stays written.
|
|
101
|
+
if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
|
|
102
|
+
markWrite(held.expression, writes);
|
|
103
|
+
}
|
|
98
104
|
return;
|
|
99
105
|
}
|
|
100
106
|
if (isArrayLiteralExpression(held)) {
|
|
@@ -232,6 +238,27 @@ function linksOf(file, node, declarations, enclosing) {
|
|
|
232
238
|
}
|
|
233
239
|
return found;
|
|
234
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Whether one object literal is a destructuring target, written on the left of an
|
|
243
|
+
* assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
|
|
244
|
+
*/
|
|
245
|
+
function isAssignmentPattern(node) {
|
|
246
|
+
let child = node;
|
|
247
|
+
let parent = node.parent;
|
|
248
|
+
while (isParenthesizedExpression(parent) ||
|
|
249
|
+
isArrayLiteralExpression(parent) ||
|
|
250
|
+
isObjectLiteralExpression(parent) ||
|
|
251
|
+
isSpreadElement(parent) ||
|
|
252
|
+
isSpreadAssignment(parent) ||
|
|
253
|
+
(isPropertyAssignment(parent) && parent.initializer === child)) {
|
|
254
|
+
child = parent;
|
|
255
|
+
parent = parent.parent;
|
|
256
|
+
}
|
|
257
|
+
if (isBinaryExpression(parent)) {
|
|
258
|
+
return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
|
|
259
|
+
}
|
|
260
|
+
return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
|
|
261
|
+
}
|
|
235
262
|
/**
|
|
236
263
|
* Every name node one file holds that is a use rather than a declaration, in source
|
|
237
264
|
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
@@ -246,6 +273,8 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
246
273
|
const links = [];
|
|
247
274
|
const evaluations = [];
|
|
248
275
|
const decorated = [];
|
|
276
|
+
const literals = [];
|
|
277
|
+
const flows = [];
|
|
249
278
|
const writes = new Set();
|
|
250
279
|
const alsoRead = new Set();
|
|
251
280
|
const declared = new Set();
|
|
@@ -286,6 +315,13 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
286
315
|
}
|
|
287
316
|
markNames(node);
|
|
288
317
|
markStores(node, writes, alsoRead);
|
|
318
|
+
if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
|
|
319
|
+
literals.push({ literal: node, from: enclosing });
|
|
320
|
+
}
|
|
321
|
+
// An assertion needs only comparability, so no member of its operand is read by it.
|
|
322
|
+
for (const written of isAsExpression(node) ? [] : handedOn(node)) {
|
|
323
|
+
flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
|
|
324
|
+
}
|
|
289
325
|
for (const read of propertyReadsOf(node)) {
|
|
290
326
|
reads.push({ ...read, from: enclosing });
|
|
291
327
|
}
|
|
@@ -308,8 +344,16 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
308
344
|
enclosing = outer;
|
|
309
345
|
};
|
|
310
346
|
file.forEachChild(visit);
|
|
311
|
-
return { uses: found, reads, links, evaluations, decorated };
|
|
347
|
+
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
312
348
|
}
|
|
349
|
+
/** The declaration kinds of a property an object literal declares. */
|
|
350
|
+
const LITERAL_PROPERTY_KINDS = new Set([
|
|
351
|
+
SyntaxKind.PropertyAssignment,
|
|
352
|
+
SyntaxKind.ShorthandPropertyAssignment,
|
|
353
|
+
SyntaxKind.MethodDeclaration,
|
|
354
|
+
SyntaxKind.GetAccessor,
|
|
355
|
+
SyntaxKind.SetAccessor,
|
|
356
|
+
]);
|
|
313
357
|
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
314
358
|
const TEST_SUPPORT_RULE = "test-support";
|
|
315
359
|
/**
|
|
@@ -412,6 +456,59 @@ function importBindings(file) {
|
|
|
412
456
|
}
|
|
413
457
|
return found;
|
|
414
458
|
}
|
|
459
|
+
/** The value one assignment stores through the name it writes, where it is a plain assignment. */
|
|
460
|
+
function storedValue(name) {
|
|
461
|
+
let target = name;
|
|
462
|
+
if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
|
|
463
|
+
target = name.parent;
|
|
464
|
+
}
|
|
465
|
+
while (isParenthesizedExpression(target.parent)) {
|
|
466
|
+
target = target.parent;
|
|
467
|
+
}
|
|
468
|
+
const parent = target.parent;
|
|
469
|
+
return isBinaryExpression(parent) &&
|
|
470
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
471
|
+
parent.left === target
|
|
472
|
+
? parent.right
|
|
473
|
+
: undefined;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* The import bindings of one file that only the values written into one declaration
|
|
477
|
+
* use, for each write into it. A binding is named by an identifier that spells its
|
|
478
|
+
* local name and is not the member a property access names.
|
|
479
|
+
*/
|
|
480
|
+
function aloneIn(file, uses, writes, found) {
|
|
481
|
+
const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
|
|
482
|
+
if (bindings.size === 0) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
const occurrences = new Map();
|
|
486
|
+
for (const site of uses) {
|
|
487
|
+
const text = site.node.getText();
|
|
488
|
+
const parent = site.node.parent;
|
|
489
|
+
if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!occurrences.get(text)?.includes(site.node)) {
|
|
493
|
+
occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const inside = (node, value) => node.pos >= value.pos && node.end <= value.end;
|
|
497
|
+
const result = [];
|
|
498
|
+
for (const write of writes) {
|
|
499
|
+
const to = found[write.index]?.to;
|
|
500
|
+
const sameTarget = writes.filter((one) => found[one.index]?.to === to);
|
|
501
|
+
const names = [...occurrences]
|
|
502
|
+
.filter(([, nodes]) => nodes.some((node) => inside(node, write.value)) &&
|
|
503
|
+
nodes.every((node) => sameTarget.some((one) => inside(node, one.value))))
|
|
504
|
+
.map(([name]) => name)
|
|
505
|
+
.sort(compare);
|
|
506
|
+
if (names.length > 0) {
|
|
507
|
+
result.push({ index: write.index, names });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
415
512
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
416
513
|
function compare(a, b) {
|
|
417
514
|
if (a === b) {
|
|
@@ -442,7 +539,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
442
539
|
const found = [];
|
|
443
540
|
const tests = [];
|
|
444
541
|
const chains = aliasChains(project, held);
|
|
445
|
-
const
|
|
542
|
+
const tables = propertyTables(project.queries);
|
|
543
|
+
const destructured = destructuring(project, cap, tables);
|
|
446
544
|
let batched = 0;
|
|
447
545
|
let fileBatches = 0;
|
|
448
546
|
let residueFallbacks = 0;
|
|
@@ -455,6 +553,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
455
553
|
.ownSourceFiles()
|
|
456
554
|
.filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
|
|
457
555
|
relativePath(targetRoot, file.fileName) === undefined);
|
|
556
|
+
const ownPaths = project.ownPaths();
|
|
458
557
|
const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
|
|
459
558
|
let topLevel;
|
|
460
559
|
/** The declarations one file's top level holds. */
|
|
@@ -551,14 +650,89 @@ export function references(project, held, targetRoot, options) {
|
|
|
551
650
|
residueFallbacks += 1;
|
|
552
651
|
return project.resolvedSymbolAt(project.handle(site.node));
|
|
553
652
|
};
|
|
554
|
-
|
|
653
|
+
const walked = files.map((file) => {
|
|
654
|
+
const fileId = declaredAt(file) ?? "";
|
|
655
|
+
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
656
|
+
});
|
|
657
|
+
const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
|
|
658
|
+
const writtenBy = new Map();
|
|
659
|
+
for (const write of contextual.writes) {
|
|
660
|
+
const property = write.at.parent;
|
|
661
|
+
writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
|
|
662
|
+
}
|
|
663
|
+
const forwardedCache = new Map();
|
|
664
|
+
/**
|
|
665
|
+
* The members a literal property one symbol declares writes: a use of the property
|
|
666
|
+
* reaches them, because the literal's type carries the data the member describes.
|
|
667
|
+
*/
|
|
668
|
+
const forwarded = (symbol) => {
|
|
669
|
+
const known = forwardedCache.get(symbol.id);
|
|
670
|
+
if (known !== undefined) {
|
|
671
|
+
return known;
|
|
672
|
+
}
|
|
673
|
+
const found = [];
|
|
674
|
+
if (writtenBy.size > 0) {
|
|
675
|
+
for (const handle of symbol.declarations) {
|
|
676
|
+
if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const node = project.declarationAt(handle)?.node;
|
|
680
|
+
const ids = node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
|
|
681
|
+
for (const id of ids ?? []) {
|
|
682
|
+
if (!found.some((target) => target.id === id)) {
|
|
683
|
+
found.push({ id, stepped: true, guessed: false });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
forwardedCache.set(symbol.id, found);
|
|
689
|
+
return found;
|
|
690
|
+
};
|
|
691
|
+
/** One resolved symbol's declarations along its chain, and the members its literal properties write. */
|
|
692
|
+
const targetsOf = (symbol) => {
|
|
693
|
+
const through = forwarded(symbol);
|
|
694
|
+
return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
|
|
695
|
+
};
|
|
696
|
+
const testFile = new Map();
|
|
697
|
+
for (const { file } of walked) {
|
|
555
698
|
const path = renderPosition(file, root, 0).path;
|
|
556
699
|
const test = patterns.some((pattern) => pattern.test(path));
|
|
700
|
+
testFile.set(file, test);
|
|
557
701
|
if (test) {
|
|
558
702
|
tests.push(path);
|
|
559
703
|
}
|
|
560
|
-
|
|
561
|
-
|
|
704
|
+
}
|
|
705
|
+
/** Each write reference's index in `found`, by its file, with the value it stores. */
|
|
706
|
+
const valued = new Map();
|
|
707
|
+
const holdsValue = (file, value) => {
|
|
708
|
+
if (value !== undefined) {
|
|
709
|
+
valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
for (const [use, sites] of [
|
|
713
|
+
["write", contextual.writes],
|
|
714
|
+
["read", contextual.reads],
|
|
715
|
+
]) {
|
|
716
|
+
for (const site of sites) {
|
|
717
|
+
const file = site.at.getSourceFile();
|
|
718
|
+
const position = renderPosition(file, root, site.at.getStart());
|
|
719
|
+
for (const id of site.ids) {
|
|
720
|
+
found.push({
|
|
721
|
+
from: site.from,
|
|
722
|
+
to: id,
|
|
723
|
+
position,
|
|
724
|
+
use,
|
|
725
|
+
resolution: "contextual",
|
|
726
|
+
test: testFile.get(file) === true,
|
|
727
|
+
});
|
|
728
|
+
if (use === "write") {
|
|
729
|
+
holdsValue(file, site.at.parent);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
735
|
+
const test = testFile.get(file) === true;
|
|
562
736
|
// A component file's markup may use any binding its blocks declare at the top level.
|
|
563
737
|
const markup = isComponentFile(file);
|
|
564
738
|
const bindings = markup ? importBindings(file) : [];
|
|
@@ -697,7 +871,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
697
871
|
stepped: false,
|
|
698
872
|
guessed: true,
|
|
699
873
|
}))
|
|
700
|
-
:
|
|
874
|
+
: targetsOf(symbol);
|
|
701
875
|
for (const target of targets) {
|
|
702
876
|
found.push({
|
|
703
877
|
from: read.from,
|
|
@@ -724,7 +898,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
724
898
|
// the use names every link of the chain its name stands at the head of.
|
|
725
899
|
const targets = throughAliases(symbol === UNANSWERED
|
|
726
900
|
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
727
|
-
:
|
|
901
|
+
: targetsOf(symbol));
|
|
728
902
|
for (const target of targets) {
|
|
729
903
|
found.push({
|
|
730
904
|
from: site.from,
|
|
@@ -734,6 +908,19 @@ export function references(project, held, targetRoot, options) {
|
|
|
734
908
|
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
735
909
|
test,
|
|
736
910
|
});
|
|
911
|
+
if (site.use === "write") {
|
|
912
|
+
holdsValue(file, storedValue(site.node));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
918
|
+
for (const [file, writes] of valued) {
|
|
919
|
+
const uses = usesOf.get(file) ?? [];
|
|
920
|
+
for (const { index, names } of aloneIn(file, uses, writes, found)) {
|
|
921
|
+
const reference = found[index];
|
|
922
|
+
if (reference !== undefined) {
|
|
923
|
+
found[index] = { ...reference, alone: names };
|
|
737
924
|
}
|
|
738
925
|
}
|
|
739
926
|
}
|
|
@@ -755,6 +942,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
755
942
|
aliasSteps: chains.steps,
|
|
756
943
|
patternBatches: destructured.cost.patternBatches,
|
|
757
944
|
patternLookups: destructured.cost.patternLookups,
|
|
945
|
+
contextualBatches: contextual.batches,
|
|
946
|
+
contextualLookups: contextual.lookups,
|
|
758
947
|
},
|
|
759
948
|
};
|
|
760
949
|
}
|
package/dist/src/resolve.js
CHANGED
|
@@ -133,18 +133,19 @@ function readStrings(parent, name, path, label, minItems, allowed) {
|
|
|
133
133
|
throw malformed(label, path, `holds ${String(value.length)} entries, want at least ${String(minItems)}`);
|
|
134
134
|
}
|
|
135
135
|
const seen = new Set();
|
|
136
|
-
for (const entry of value) {
|
|
136
|
+
for (const [index, entry] of value.entries()) {
|
|
137
|
+
const at = `${path}[${String(index)}]`;
|
|
137
138
|
if (typeof entry !== "string") {
|
|
138
|
-
throw malformed(label,
|
|
139
|
+
throw malformed(label, at, "is not a string");
|
|
139
140
|
}
|
|
140
141
|
if (entry === "") {
|
|
141
|
-
throw malformed(label,
|
|
142
|
+
throw malformed(label, at, "is empty");
|
|
142
143
|
}
|
|
143
144
|
if (seen.has(entry)) {
|
|
144
|
-
throw malformed(label,
|
|
145
|
+
throw malformed(label, at, `names ${JSON.stringify(entry)} again`);
|
|
145
146
|
}
|
|
146
147
|
if (allowed !== undefined && !allowed.includes(entry)) {
|
|
147
|
-
throw malformed(label,
|
|
148
|
+
throw malformed(label, at, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
|
|
148
149
|
}
|
|
149
150
|
seen.add(entry);
|
|
150
151
|
}
|
|
@@ -154,9 +155,9 @@ function readStrings(parent, name, path, label, minItems, allowed) {
|
|
|
154
155
|
const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
|
|
155
156
|
function readComponentExtensions(ts, label) {
|
|
156
157
|
const extensions = readStrings(ts, "component_extensions", "ts.component_extensions", label, 0);
|
|
157
|
-
const wrong = extensions?.
|
|
158
|
-
if (wrong !==
|
|
159
|
-
throw malformed(label,
|
|
158
|
+
const wrong = extensions?.findIndex((one) => !COMPONENT_EXTENSION.test(one)) ?? -1;
|
|
159
|
+
if (extensions !== undefined && wrong !== -1) {
|
|
160
|
+
throw malformed(label, `ts.component_extensions[${String(wrong)}]`, `${JSON.stringify(extensions[wrong])} is not a full stop followed by letters and digits`);
|
|
160
161
|
}
|
|
161
162
|
return extensions;
|
|
162
163
|
}
|
|
@@ -164,9 +165,9 @@ function readComponentExtensions(ts, label) {
|
|
|
164
165
|
const CONVENTION_NAMES = new Set(CONVENTION_ROWS.map((row) => row.name));
|
|
165
166
|
function readDisabledConventions(ts, label) {
|
|
166
167
|
const names = readStrings(ts, "disabled_conventions", "ts.disabled_conventions", label, 0);
|
|
167
|
-
const unknown = names?.
|
|
168
|
-
if (unknown !==
|
|
169
|
-
throw malformed(label,
|
|
168
|
+
const unknown = names?.findIndex((one) => !CONVENTION_NAMES.has(one)) ?? -1;
|
|
169
|
+
if (names !== undefined && unknown !== -1) {
|
|
170
|
+
throw malformed(label, `ts.disabled_conventions[${String(unknown)}]`, `${JSON.stringify(names[unknown])} names no convention row this analyzer carries`);
|
|
170
171
|
}
|
|
171
172
|
return names;
|
|
172
173
|
}
|
package/dist/src/roots.js
CHANGED
|
@@ -158,7 +158,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
158
158
|
: conventionEntries(options.conventions, files)) {
|
|
159
159
|
enter(file, "convention", row, true);
|
|
160
160
|
}
|
|
161
|
-
for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot)) {
|
|
161
|
+
for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse)) {
|
|
162
162
|
enter(point.file, point.rule, point.source, point.exports);
|
|
163
163
|
}
|
|
164
164
|
for (const file of files.byPath.values()) {
|
package/dist/src/stores.js
CHANGED
|
@@ -26,6 +26,9 @@ export function accessorsOf(project, held) {
|
|
|
26
26
|
export function storesOf(per, exempt, mode) {
|
|
27
27
|
return {
|
|
28
28
|
references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
|
|
29
|
+
readInTests: new Set(per.flatMap((one) => one.references
|
|
30
|
+
.filter((reference) => reference.test && reference.use !== "write")
|
|
31
|
+
.map((reference) => reference.to))),
|
|
29
32
|
accessors: new Set(per.flatMap((one) => one.accessors)),
|
|
30
33
|
exempt: new Set(exempt.map((record) => record.id)),
|
|
31
34
|
};
|