@cplieger/deadset-ts 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/analysis.js +8 -1
- package/dist/src/calls.js +154 -0
- package/dist/src/class-members.js +92 -0
- package/dist/src/config.js +3 -0
- package/dist/src/configured-declarations.js +232 -0
- package/dist/src/decorator.js +3 -10
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
- package/dist/src/findings/emitter.js +1 -0
- package/dist/src/findings/emitters.js +19 -0
- package/dist/src/findings/interfaces.js +2 -0
- package/dist/src/findings/intra-function.js +2 -0
- package/dist/src/findings/non-code-artifacts.js +2 -0
- package/dist/src/findings/reads-and-writes.js +2 -0
- package/dist/src/findings/self-check.js +2 -0
- package/dist/src/findings/unused-declarations.js +2 -0
- package/dist/src/findings/visibility-narrowing.js +2 -0
- package/dist/src/framework-lifecycle.js +192 -0
- package/dist/src/injection-container.js +101 -0
- package/dist/src/print.js +18 -0
- package/dist/src/resolve.js +98 -0
- package/dist/src/run.js +60 -176
- package/dist/src/schema.js +18 -1
- package/dist/src/serialization-contract.js +315 -0
- package/dist/src/value-reach.js +120 -0
- package/dist/src/verbs/print-config.js +9 -0
- package/dist/src/verbs/print-projects.js +34 -0
- package/dist/src/verbs/print-retained.js +23 -0
- package/dist/src/verbs/print-roots.js +36 -0
- package/dist/src/verbs/verb.js +5 -0
- package/dist/src/verbs/version.js +7 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +8 -1
- package/src/calls.ts +224 -0
- package/src/class-members.ts +136 -0
- package/src/config.ts +31 -0
- package/src/configured-declarations.ts +308 -0
- package/src/decorator.ts +4 -16
- package/src/exempt.ts +5 -1
- package/src/findings/dependencies-and-module-machinery.ts +4 -0
- package/src/findings/emitter.ts +19 -0
- package/src/findings/emitters.ts +21 -0
- package/src/findings/interfaces.ts +4 -0
- package/src/findings/intra-function.ts +4 -0
- package/src/findings/non-code-artifacts.ts +4 -0
- package/src/findings/reads-and-writes.ts +4 -0
- package/src/findings/self-check.ts +4 -0
- package/src/findings/unused-declarations.ts +4 -0
- package/src/findings/visibility-narrowing.ts +4 -0
- package/src/framework-lifecycle.ts +280 -0
- package/src/injection-container.ts +141 -0
- package/src/print.ts +20 -1
- package/src/resolve.ts +155 -0
- package/src/run.ts +64 -200
- package/src/schema.ts +19 -1
- package/src/serialization-contract.ts +492 -0
- package/src/value-reach.ts +152 -0
- package/src/verbs/print-config.ts +10 -0
- package/src/verbs/print-projects.ts +35 -0
- package/src/verbs/print-retained.ts +24 -0
- package/src/verbs/print-roots.ts +38 -0
- package/src/verbs/verb.ts +38 -0
- package/src/verbs/version.ts +8 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 3.
|
|
11
|
+
**Status: pre-release.** This version implements contract 3.1.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
package/dist/src/analysis.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { decorator } from "./decorator.js";
|
|
11
11
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
|
|
12
12
|
import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
|
|
13
|
+
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
14
|
+
import { injectionContainer } from "./injection-container.js";
|
|
13
15
|
import { interfaceSatisfaction } from "./interface-satisfaction.js";
|
|
14
16
|
import { inventory } from "./inventory.js";
|
|
15
17
|
import { readManifest } from "./manifest.js";
|
|
@@ -19,6 +21,7 @@ import { byPosition } from "./position.js";
|
|
|
19
21
|
import { reflectiveLookup } from "./reflective-lookup.js";
|
|
20
22
|
import { references } from "./references.js";
|
|
21
23
|
import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
|
|
24
|
+
import { serializationContract } from "./serialization-contract.js";
|
|
22
25
|
import { diagnosticsOf, runSession } from "./session.js";
|
|
23
26
|
import { readTemplates, templateField } from "./template-field.js";
|
|
24
27
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
@@ -128,6 +131,9 @@ const DETECTORS = new Map([
|
|
|
128
131
|
["template-field", templateField],
|
|
129
132
|
["reflective-lookup", reflectiveLookup],
|
|
130
133
|
["decorator", decorator],
|
|
134
|
+
["injection-container", injectionContainer],
|
|
135
|
+
["framework-lifecycle", frameworkLifecycle],
|
|
136
|
+
["serialization-contract", serializationContract],
|
|
131
137
|
]);
|
|
132
138
|
/**
|
|
133
139
|
* Sweeps the run the scope and the configuration describe. Each project's references
|
|
@@ -145,6 +151,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
145
151
|
const templates = disabled.has("template-field")
|
|
146
152
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
147
153
|
: readTemplates(host, targetRoot, config.analysis);
|
|
154
|
+
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
148
155
|
const read = readProjects(engine, host, scope, config, (project, projectRead) => {
|
|
149
156
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
150
157
|
testFiles: config.ts.testFiles,
|
|
@@ -156,7 +163,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
156
163
|
roots: projectRead.rooted.liveUnderReachability,
|
|
157
164
|
testFiles: resolved.testFilePaths,
|
|
158
165
|
};
|
|
159
|
-
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates }, detectors, {
|
|
166
|
+
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
|
|
160
167
|
disabled,
|
|
161
168
|
mode: input.mode,
|
|
162
169
|
testFiles: new Set(resolved.testFilePaths),
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calls and decorators of one project that the configured classes read: what each
|
|
3
|
+
* callee resolves to, and which of the target's classes a call passes or a decorator
|
|
4
|
+
* is attached to. A class is passed as an argument, or as an element of an array
|
|
5
|
+
* literal or a property value of an object literal written as one, at any depth, and a
|
|
6
|
+
* decorator written as a call is a call.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { nodeKey } from "./inventory.js";
|
|
11
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
12
|
+
/** The name node a callee or a decorator expression resolves through, where it has one. */
|
|
13
|
+
export function calleeName(expression) {
|
|
14
|
+
if (isParenthesizedExpression(expression)) {
|
|
15
|
+
return calleeName(expression.expression);
|
|
16
|
+
}
|
|
17
|
+
if (isPropertyAccessExpression(expression)) {
|
|
18
|
+
return expression.name;
|
|
19
|
+
}
|
|
20
|
+
return isIdentifier(expression) ? expression : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** The callee one decorator names: its expression, or the callee of the call it writes. */
|
|
23
|
+
export function decoratorCallee(decorator) {
|
|
24
|
+
const expression = decorator.expression;
|
|
25
|
+
return calleeName(isCallExpression(expression) ? expression.expression : expression);
|
|
26
|
+
}
|
|
27
|
+
/** How one decorator is spelled in a record: its expression, without a call's arguments. */
|
|
28
|
+
export function spelledDecorator(decorator) {
|
|
29
|
+
const expression = decorator.expression;
|
|
30
|
+
const named = isCallExpression(expression) ? expression.expression : expression;
|
|
31
|
+
return `@${named.getText().replace(/\s+/gu, "")}`;
|
|
32
|
+
}
|
|
33
|
+
/** The decorators written on one declaration. */
|
|
34
|
+
export function decoratorsOf(node) {
|
|
35
|
+
return (node.modifiers ?? []).filter(isDecorator);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What each of a batch of nodes resolves to, every alias followed to the declaration
|
|
39
|
+
* it stands for. A node the checker resolves to nothing, and an alias that resolves
|
|
40
|
+
* to nothing, are absent from the answer. Each alias is followed once.
|
|
41
|
+
*/
|
|
42
|
+
export function resolvedTargets(project, nodes) {
|
|
43
|
+
const followed = new Map();
|
|
44
|
+
const targets = new Map();
|
|
45
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
46
|
+
const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
|
|
47
|
+
project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
48
|
+
const node = run[index];
|
|
49
|
+
if (symbol === undefined || node === undefined) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let target = symbol;
|
|
53
|
+
if ((symbol.flags & SymbolFlags.Alias) !== 0) {
|
|
54
|
+
if (!followed.has(symbol.id)) {
|
|
55
|
+
const aliased = project.checker.getAliasedSymbol(symbol);
|
|
56
|
+
followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
|
|
57
|
+
}
|
|
58
|
+
target = followed.get(symbol.id);
|
|
59
|
+
}
|
|
60
|
+
if (target !== undefined) {
|
|
61
|
+
targets.set(node, target);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return targets;
|
|
66
|
+
}
|
|
67
|
+
/** The values one expression passes: itself, or what an array or object literal holds. */
|
|
68
|
+
function passedIn(expression, found) {
|
|
69
|
+
if (isParenthesizedExpression(expression)) {
|
|
70
|
+
passedIn(expression.expression, found);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (isArrayLiteralExpression(expression)) {
|
|
74
|
+
for (const element of expression.elements) {
|
|
75
|
+
passedIn(isSpreadElement(element) ? element.expression : element, found);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (isObjectLiteralExpression(expression)) {
|
|
80
|
+
for (const property of expression.properties) {
|
|
81
|
+
if (isPropertyAssignment(property)) {
|
|
82
|
+
passedIn(property.initializer, found);
|
|
83
|
+
}
|
|
84
|
+
else if (isShorthandPropertyAssignment(property)) {
|
|
85
|
+
found.push({ site: property.name, name: property, shorthand: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const name = calleeName(expression);
|
|
91
|
+
if (name !== undefined) {
|
|
92
|
+
found.push({ site: expression, name, shorthand: false });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The values one argument passes, a spread argument's included. */
|
|
96
|
+
export function valuesPassed(argument) {
|
|
97
|
+
const found = [];
|
|
98
|
+
passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The classes of the target each call passes, by the call. A value that names no
|
|
103
|
+
* class of the target, a class of another program included, passes nothing here.
|
|
104
|
+
*/
|
|
105
|
+
export function classesPassed(project, held, chains, calls) {
|
|
106
|
+
const passed = new Map(calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]));
|
|
107
|
+
const all = [...passed.values()].flat();
|
|
108
|
+
const targets = resolvedTargets(project, all.filter((value) => !value.shorthand).map((value) => value.name));
|
|
109
|
+
const classes = new Set(held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id));
|
|
110
|
+
const classesOf = (value) => {
|
|
111
|
+
const symbol = value.shorthand
|
|
112
|
+
? project.shorthandValueAt(project.handle(value.name))
|
|
113
|
+
: targets.get(value.name);
|
|
114
|
+
return symbol === undefined
|
|
115
|
+
? []
|
|
116
|
+
: chains.declarationsOf(symbol).filter((id) => classes.has(id));
|
|
117
|
+
};
|
|
118
|
+
return new Map([...passed].map(([call, values]) => [
|
|
119
|
+
call,
|
|
120
|
+
values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
|
|
121
|
+
]));
|
|
122
|
+
}
|
|
123
|
+
/** Every class declaration of the target that carries a decorator. */
|
|
124
|
+
export function decoratedClasses(project, held) {
|
|
125
|
+
const found = [];
|
|
126
|
+
for (const file of project.ownSourceFiles()) {
|
|
127
|
+
const visit = (node) => {
|
|
128
|
+
if (isClassDeclaration(node)) {
|
|
129
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
130
|
+
const decorators = decoratorsOf(node);
|
|
131
|
+
if (id !== undefined && decorators.length > 0) {
|
|
132
|
+
found.push({ id, decorators });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
node.forEachChild(visit);
|
|
136
|
+
};
|
|
137
|
+
file.forEachChild(visit);
|
|
138
|
+
}
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
/** Every call expression of the project's own files, decorators written as calls included. */
|
|
142
|
+
export function callsOf(project) {
|
|
143
|
+
const found = [];
|
|
144
|
+
for (const file of project.ownSourceFiles()) {
|
|
145
|
+
const visit = (node) => {
|
|
146
|
+
if (isCallExpression(node)) {
|
|
147
|
+
found.push(node);
|
|
148
|
+
}
|
|
149
|
+
node.forEachChild(visit);
|
|
150
|
+
};
|
|
151
|
+
file.forEachChild(visit);
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a value of one of the target's classes carries, as the checker answers
|
|
3
|
+
* for the class's type, so a member a base class declares is a member of every class
|
|
4
|
+
* that extends it and a member a subclass overrides is the subclass's own.
|
|
5
|
+
*/
|
|
6
|
+
import { isClassDeclaration, isClassExpression } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { nodeKey } from "./inventory.js";
|
|
8
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
9
|
+
/** The name node of each class declaration of the target, by the class's identifier. */
|
|
10
|
+
export function classNames(project, held) {
|
|
11
|
+
const names = new Map();
|
|
12
|
+
for (const file of project.ownSourceFiles()) {
|
|
13
|
+
const visit = (node) => {
|
|
14
|
+
if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
|
|
15
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
16
|
+
if (id !== undefined) {
|
|
17
|
+
names.set(id, node.name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
node.forEachChild(visit);
|
|
21
|
+
};
|
|
22
|
+
file.forEachChild(visit);
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
/** The types of a batch of nodes, asked in capped batches. */
|
|
27
|
+
export function typesAt(project, nodes) {
|
|
28
|
+
const types = [];
|
|
29
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
30
|
+
types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
|
|
31
|
+
}
|
|
32
|
+
return types;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The type one side of each class is: the instance type at the class's name, or the
|
|
36
|
+
* type of the class's own symbol, which is its constructor's.
|
|
37
|
+
*/
|
|
38
|
+
function sideTypes(project, nodes, side) {
|
|
39
|
+
if (side === "instance") {
|
|
40
|
+
return typesAt(project, nodes);
|
|
41
|
+
}
|
|
42
|
+
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
|
|
43
|
+
const present = symbols.filter((symbol) => symbol !== undefined);
|
|
44
|
+
const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
|
|
45
|
+
let next = 0;
|
|
46
|
+
return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The members one side of each class carries that the target declares, by the class.
|
|
50
|
+
* A member declared in a file outside the project's own, and the `prototype` a class
|
|
51
|
+
* carries on its static side, are not declarations of the target and are left out.
|
|
52
|
+
*/
|
|
53
|
+
export function classMembers(project, held, classIds, side) {
|
|
54
|
+
const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
|
|
55
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
56
|
+
const types = sideTypes(project, named.map(([, node]) => node), side);
|
|
57
|
+
const members = new Map();
|
|
58
|
+
named.forEach(([id], index) => {
|
|
59
|
+
const type = types[index];
|
|
60
|
+
if (type === undefined) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const property of project.checker.getPropertiesOfType(type)) {
|
|
65
|
+
for (const handle of property.declarations) {
|
|
66
|
+
if (!own.has(handle.path)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const node = project.declarationAt(handle)?.node;
|
|
70
|
+
const declared = node === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
73
|
+
if (node !== undefined && declared !== undefined) {
|
|
74
|
+
found.push({ id: declared, node, flags: property.flags });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
members.set(id, found);
|
|
79
|
+
});
|
|
80
|
+
return members;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The last component of one member's stable reference, `:static` included, which is
|
|
84
|
+
* how a configuration names a member; undefined for a declaration that is no member.
|
|
85
|
+
*/
|
|
86
|
+
export function memberComponent(symbol, byId) {
|
|
87
|
+
const parent = byId.get(symbol.parent);
|
|
88
|
+
if (parent === undefined || !symbol.ref.startsWith(`${parent.ref}.`)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return symbol.ref.slice(parent.ref.length + 1);
|
|
92
|
+
}
|
package/dist/src/config.js
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declarations the configuration's entries name, resolved in one project, and
|
|
3
|
+
* which of them a symbol is. Declarations are compared rather than symbols, because a
|
|
4
|
+
* member read through an instantiation of a generic type is a symbol of its own that
|
|
5
|
+
* keeps the declarations of the member it instantiates.
|
|
6
|
+
*/
|
|
7
|
+
import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
9
|
+
import { resolvedTargets } from "./calls.js";
|
|
10
|
+
import { nameComponent } from "./ref.js";
|
|
11
|
+
/** The meanings a name of the global scope is looked up under: any of them. */
|
|
12
|
+
const ANY_MEANING = SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace;
|
|
13
|
+
/** The suffix that marks a static member at the end of a path. */
|
|
14
|
+
const STATIC = ":static";
|
|
15
|
+
/** The key one declaration handle is compared by: its file and its index in that file. */
|
|
16
|
+
function handleKey(handle) {
|
|
17
|
+
return `${handle.path}:${String(handle.index)}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The components of one declaration path and whether its last one is a static member.
|
|
21
|
+
* A path is split at the dots outside a quoted or a bracketed component, and each
|
|
22
|
+
* component keeps the spelling a reference gives it.
|
|
23
|
+
*/
|
|
24
|
+
function componentsOf(path) {
|
|
25
|
+
const isStatic = path.endsWith(STATIC);
|
|
26
|
+
const body = isStatic ? path.slice(0, -STATIC.length) : path;
|
|
27
|
+
const components = [];
|
|
28
|
+
let current = "";
|
|
29
|
+
let closing = "";
|
|
30
|
+
for (let at = 0; at < body.length; at += 1) {
|
|
31
|
+
const ch = body.charAt(at);
|
|
32
|
+
if (closing === "'" && ch === "\\") {
|
|
33
|
+
current += ch + body.charAt(at + 1);
|
|
34
|
+
at += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (closing !== "") {
|
|
38
|
+
closing = ch === closing ? "" : closing;
|
|
39
|
+
}
|
|
40
|
+
else if (ch === "'" || ch === "[") {
|
|
41
|
+
closing = ch === "'" ? "'" : "]";
|
|
42
|
+
}
|
|
43
|
+
else if (ch === ".") {
|
|
44
|
+
components.push(current);
|
|
45
|
+
current = "";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
current += ch;
|
|
49
|
+
}
|
|
50
|
+
components.push(current);
|
|
51
|
+
return { components, static: isStatic };
|
|
52
|
+
}
|
|
53
|
+
/** What a symbol is once every alias it stands for is followed. */
|
|
54
|
+
function unaliased(project, symbol) {
|
|
55
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
56
|
+
return symbol;
|
|
57
|
+
}
|
|
58
|
+
const target = project.checker.getAliasedSymbol(symbol);
|
|
59
|
+
return target.declarations.length === 0 ? undefined : target;
|
|
60
|
+
}
|
|
61
|
+
/** Whether one symbol is a member a class declares: a property, a method or an accessor. */
|
|
62
|
+
function isClassMember(symbol) {
|
|
63
|
+
return (symbol.flags & SymbolFlags.ClassMember) !== 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The members of one container a path component may name. A class keeps its static
|
|
67
|
+
* members among its exports beside what a merged namespace exports, so a component
|
|
68
|
+
* with the static suffix names one of the first and a component without it names an
|
|
69
|
+
* instance member or one of the second.
|
|
70
|
+
*/
|
|
71
|
+
function candidatesOf(container, isStatic) {
|
|
72
|
+
const exported = [...container.getExports().values()];
|
|
73
|
+
if (isStatic) {
|
|
74
|
+
return exported.filter(isClassMember);
|
|
75
|
+
}
|
|
76
|
+
const isClass = (container.flags & SymbolFlags.Class) !== 0;
|
|
77
|
+
return [
|
|
78
|
+
...container.getMembers().values(),
|
|
79
|
+
...exported.filter((member) => !isClass || !isClassMember(member)),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
/** The member of one container a path component spells, followed through any alias. */
|
|
83
|
+
function memberOf(project, container, component, isStatic) {
|
|
84
|
+
const member = candidatesOf(container, isStatic).find((candidate) => nameComponent(candidate.name) === component);
|
|
85
|
+
return member === undefined ? undefined : unaliased(project, member);
|
|
86
|
+
}
|
|
87
|
+
/** The declaration a path names from one starting symbol, component by component. */
|
|
88
|
+
function walkPath(project, start, rest, isStatic) {
|
|
89
|
+
let at = start;
|
|
90
|
+
rest.forEach((component, index) => {
|
|
91
|
+
if (at !== undefined) {
|
|
92
|
+
at = memberOf(project, at, component, isStatic && index === rest.length - 1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return at;
|
|
96
|
+
}
|
|
97
|
+
/** Whether one node is literal text: a string literal or a template with no substitution. */
|
|
98
|
+
function isLiteralText(node) {
|
|
99
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
100
|
+
}
|
|
101
|
+
/** The module specifier one node writes, for each form that imports or re-exports a module. */
|
|
102
|
+
function specifierOf(node) {
|
|
103
|
+
if (isImportDeclaration(node) || isExportDeclaration(node)) {
|
|
104
|
+
return node.moduleSpecifier;
|
|
105
|
+
}
|
|
106
|
+
if (isImportEqualsDeclaration(node) && isExternalModuleReference(node.moduleReference)) {
|
|
107
|
+
return node.moduleReference.expression;
|
|
108
|
+
}
|
|
109
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
110
|
+
return node.arguments[0];
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The module each specifier the entries name resolves to in one project, read from a
|
|
116
|
+
* specifier the project's own files write, so the module is the one the target's own
|
|
117
|
+
* import resolves. A specifier no file writes resolves to no module.
|
|
118
|
+
*/
|
|
119
|
+
function modulesNamed(project, specifiers) {
|
|
120
|
+
const written = new Map();
|
|
121
|
+
for (const file of project.ownSourceFiles()) {
|
|
122
|
+
const visit = (node) => {
|
|
123
|
+
const specifier = specifierOf(node);
|
|
124
|
+
if (isLiteralText(specifier) &&
|
|
125
|
+
specifiers.has(specifier.text) &&
|
|
126
|
+
!written.has(specifier.text)) {
|
|
127
|
+
written.set(specifier.text, specifier);
|
|
128
|
+
}
|
|
129
|
+
node.forEachChild(visit);
|
|
130
|
+
};
|
|
131
|
+
file.forEachChild(visit);
|
|
132
|
+
}
|
|
133
|
+
const specified = [...written];
|
|
134
|
+
const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node)));
|
|
135
|
+
const modules = new Map();
|
|
136
|
+
specified.forEach(([name], index) => {
|
|
137
|
+
const symbol = symbols[index];
|
|
138
|
+
if (symbol !== undefined) {
|
|
139
|
+
modules.set(name, symbol);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return modules;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The declaration each entry names in one project. A symbol entry names the
|
|
146
|
+
* inventory's declaration whose reference it spells exactly; a module entry names the
|
|
147
|
+
* declaration its path reaches from the exports of the module its specifier resolves
|
|
148
|
+
* to; a global entry names the declaration its path reaches from the global scope.
|
|
149
|
+
* Every alias along a path is followed to the declaration it stands for.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveEntries(project, held, entries) {
|
|
152
|
+
if (entries.length === 0) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const byRef = new Map(held.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
156
|
+
const modules = modulesNamed(project, new Set(entries.flatMap((entry) => (entry.shape === "module" ? [entry.module] : []))));
|
|
157
|
+
return entries.map((entry) => {
|
|
158
|
+
if (entry.shape === "symbol") {
|
|
159
|
+
const id = byRef.get(entry.symbol);
|
|
160
|
+
return { entry, handles: new Set(), ids: new Set(id === undefined ? [] : [id]) };
|
|
161
|
+
}
|
|
162
|
+
const { components, static: isStatic } = componentsOf(entry.shape === "module" ? entry.name : entry.global);
|
|
163
|
+
const [first = "", ...rest] = components;
|
|
164
|
+
let start;
|
|
165
|
+
if (isStatic && rest.length === 0) {
|
|
166
|
+
// Only a member is static, and a path of one component names no member.
|
|
167
|
+
start = undefined;
|
|
168
|
+
}
|
|
169
|
+
else if (entry.shape === "module") {
|
|
170
|
+
const module = modules.get(entry.module);
|
|
171
|
+
start = module === undefined ? undefined : memberOf(project, module, first, false);
|
|
172
|
+
}
|
|
173
|
+
else if (nameComponent(first) === first) {
|
|
174
|
+
const global = project.checker.resolveName(first, ANY_MEANING, undefined, false);
|
|
175
|
+
start = global === undefined ? undefined : unaliased(project, global);
|
|
176
|
+
}
|
|
177
|
+
const declared = walkPath(project, start, rest, isStatic);
|
|
178
|
+
return {
|
|
179
|
+
entry,
|
|
180
|
+
handles: new Set(declared?.declarations.map(handleKey) ?? []),
|
|
181
|
+
ids: new Set(),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether one symbol is the declaration a resolved entry names: it shares one of the
|
|
187
|
+
* entry's declarations, or, for an entry naming a declaration of the program, it is
|
|
188
|
+
* declared there. `chains` reads the symbol's declarations against the inventory the
|
|
189
|
+
* entries were resolved with.
|
|
190
|
+
*/
|
|
191
|
+
export function names(resolved, symbol, chains) {
|
|
192
|
+
if (symbol.declarations.some((handle) => resolved.handles.has(handleKey(handle)))) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return resolved.ids.size > 0 && chains.declarationsOf(symbol).some((id) => resolved.ids.has(id));
|
|
196
|
+
}
|
|
197
|
+
/** Whether a resolved entry names a declaration, so that some symbol can be it. */
|
|
198
|
+
export function namesADeclaration(resolved) {
|
|
199
|
+
return resolved.handles.size > 0 || resolved.ids.size > 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The entries each of a batch of name nodes is: what the node resolves to, every alias
|
|
203
|
+
* followed, is the declaration each of them names. A node that is none is absent.
|
|
204
|
+
*/
|
|
205
|
+
export function entriesAt(project, chains, nodes, resolved) {
|
|
206
|
+
const naming = resolved.filter(namesADeclaration);
|
|
207
|
+
const found = new Map();
|
|
208
|
+
if (naming.length === 0) {
|
|
209
|
+
return found;
|
|
210
|
+
}
|
|
211
|
+
for (const [node, target] of resolvedTargets(project, nodes)) {
|
|
212
|
+
const entries = naming.filter((entry) => names(entry, target, chains));
|
|
213
|
+
if (entries.length > 0) {
|
|
214
|
+
found.set(node, entries);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* How one entry is spelled in a record's detail: its path, or for a declaration of the
|
|
221
|
+
* program the display name the inventory gives it.
|
|
222
|
+
*/
|
|
223
|
+
export function spelledEntry(entry, held) {
|
|
224
|
+
switch (entry.shape) {
|
|
225
|
+
case "symbol":
|
|
226
|
+
return held.symbols.find((symbol) => symbol.ref === entry.symbol)?.name ?? entry.symbol;
|
|
227
|
+
case "module":
|
|
228
|
+
return entry.name;
|
|
229
|
+
case "global":
|
|
230
|
+
return entry.global;
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/src/decorator.js
CHANGED
|
@@ -10,17 +10,11 @@
|
|
|
10
10
|
* declaration is held back, and the member set of a decorated class is every member
|
|
11
11
|
* the inventory holds for the class, static members included.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { decoratorsOf, spelledDecorator } from "./calls.js";
|
|
14
14
|
import { nodeKey } from "./inventory.js";
|
|
15
15
|
import { renderPosition } from "./position.js";
|
|
16
16
|
/** The kinds of declaration a decorator is attached to and holds back. */
|
|
17
17
|
const DECORATED = new Set(["class", "method", "class-member"]);
|
|
18
|
-
/** How one decorator is spelled in a record: its expression, without a call's arguments. */
|
|
19
|
-
function spelled(decorator) {
|
|
20
|
-
const expression = decorator.expression;
|
|
21
|
-
const named = isCallExpression(expression) ? expression.expression : expression;
|
|
22
|
-
return `@${named.getText().replace(/\s+/gu, "")}`;
|
|
23
|
-
}
|
|
24
18
|
/**
|
|
25
19
|
* The decorator detector: each decorated member, and each member of a decorated
|
|
26
20
|
* class, recorded at the decorator.
|
|
@@ -46,9 +40,8 @@ export function decorator(input) {
|
|
|
46
40
|
const id = held.declarations.get(nodeKey(file, node));
|
|
47
41
|
const symbol = id === undefined ? undefined : byId.get(id);
|
|
48
42
|
if (symbol !== undefined && DECORATED.has(symbol.kind)) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const detail = `decorated by ${spelled(attached)}`;
|
|
43
|
+
for (const attached of decoratorsOf(node)) {
|
|
44
|
+
const detail = `decorated by ${spelledDecorator(attached)}`;
|
|
52
45
|
const site = renderPosition(file, targetRoot, attached.getStart());
|
|
53
46
|
const reached = symbol.kind === "class" ? (membersOf.get(symbol.id) ?? []) : [symbol];
|
|
54
47
|
for (const member of reached) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
+
import { interfaces } from "./interfaces.js";
|
|
3
|
+
import { intraFunction } from "./intra-function.js";
|
|
4
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
5
|
+
import { readsAndWrites } from "./reads-and-writes.js";
|
|
6
|
+
import { selfCheck } from "./self-check.js";
|
|
7
|
+
import { unusedDeclarations } from "./unused-declarations.js";
|
|
8
|
+
import { visibilityNarrowing } from "./visibility-narrowing.js";
|
|
9
|
+
/** Every kind family this analyzer reports, each by its emitter, in code order. */
|
|
10
|
+
export const EMITTERS = new Map([
|
|
11
|
+
["unused-declarations", unusedDeclarations],
|
|
12
|
+
["visibility-narrowing", visibilityNarrowing],
|
|
13
|
+
["interfaces", interfaces],
|
|
14
|
+
["reads-and-writes", readsAndWrites],
|
|
15
|
+
["non-code-artifacts", nonCodeArtifacts],
|
|
16
|
+
["dependencies-and-module-machinery", dependenciesAndModuleMachinery],
|
|
17
|
+
["self-check", selfCheck],
|
|
18
|
+
["intra-function", intraFunction],
|
|
19
|
+
]);
|