@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/src/config.ts
CHANGED
|
@@ -97,10 +97,38 @@ export interface Reporters {
|
|
|
97
97
|
readonly failOn: Severity;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* One declaration a configuration entry names, in exactly one of three shapes: a
|
|
102
|
+
* declaration of the analyzed program by its stable symbol reference, a declaration a
|
|
103
|
+
* module exports by the bare specifier an import writes for the module and the
|
|
104
|
+
* declaration's path from its exports, or a declaration of the global scope by its
|
|
105
|
+
* path from that scope. A path joins the name and each member with dots and spells a
|
|
106
|
+
* static member with `:static` after it.
|
|
107
|
+
*/
|
|
108
|
+
export type DeclarationEntry =
|
|
109
|
+
| { readonly shape: "symbol"; readonly symbol: string }
|
|
110
|
+
| { readonly shape: "module"; readonly module: string; readonly name: string }
|
|
111
|
+
| { readonly shape: "global"; readonly global: string };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One framework's lifecycle contract: the declarations whose use makes a class one of
|
|
115
|
+
* the framework's components, the classes whose subclasses are its components, and the
|
|
116
|
+
* names of the members the framework calls on a component, each spelled as the last
|
|
117
|
+
* component of a member's stable symbol reference.
|
|
118
|
+
*/
|
|
119
|
+
export interface LifecycleContract {
|
|
120
|
+
readonly components: readonly DeclarationEntry[];
|
|
121
|
+
readonly bases: readonly DeclarationEntry[];
|
|
122
|
+
readonly members: readonly string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
/** The section the TypeScript analyzer owns. */
|
|
101
126
|
export interface TSSection {
|
|
102
127
|
readonly testFiles: readonly string[];
|
|
103
128
|
readonly entryFiles: readonly string[];
|
|
129
|
+
readonly injectionRegistrations: readonly DeclarationEntry[];
|
|
130
|
+
readonly lifecycleContracts: readonly LifecycleContract[];
|
|
131
|
+
readonly serializers: readonly DeclarationEntry[];
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
/**
|
|
@@ -159,6 +187,9 @@ export function defaultConfig(): Config {
|
|
|
159
187
|
ts: {
|
|
160
188
|
testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
|
|
161
189
|
entryFiles: [],
|
|
190
|
+
injectionRegistrations: [],
|
|
191
|
+
lifecycleContracts: [],
|
|
192
|
+
serializers: [],
|
|
162
193
|
},
|
|
163
194
|
};
|
|
164
195
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
|
|
8
|
+
import {
|
|
9
|
+
isCallExpression,
|
|
10
|
+
isExportDeclaration,
|
|
11
|
+
isExternalModuleReference,
|
|
12
|
+
isImportDeclaration,
|
|
13
|
+
isImportEqualsDeclaration,
|
|
14
|
+
isNoSubstitutionTemplateLiteral,
|
|
15
|
+
isStringLiteral,
|
|
16
|
+
SyntaxKind,
|
|
17
|
+
type Node,
|
|
18
|
+
} from "@typescript/native/unstable/ast";
|
|
19
|
+
import {
|
|
20
|
+
SymbolFlags,
|
|
21
|
+
type NodeHandle,
|
|
22
|
+
type Symbol as TSSymbol,
|
|
23
|
+
} from "@typescript/native/unstable/sync";
|
|
24
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
25
|
+
import { resolvedTargets } from "./calls.ts";
|
|
26
|
+
import type { DeclarationEntry } from "./config.ts";
|
|
27
|
+
import type { Inventory } from "./inventory.ts";
|
|
28
|
+
import { nameComponent } from "./ref.ts";
|
|
29
|
+
import type { ProjectView } from "./session.ts";
|
|
30
|
+
|
|
31
|
+
/** The meanings a name of the global scope is looked up under: any of them. */
|
|
32
|
+
const ANY_MEANING: SymbolFlags = SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace;
|
|
33
|
+
|
|
34
|
+
/** The suffix that marks a static member at the end of a path. */
|
|
35
|
+
const STATIC = ":static";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One entry and the declaration it names in one project. Both sets are empty where
|
|
39
|
+
* the entry names no declaration there, which is how an entry that names nothing is
|
|
40
|
+
* told from one that names something no call reaches.
|
|
41
|
+
*/
|
|
42
|
+
export interface ResolvedEntry {
|
|
43
|
+
readonly entry: DeclarationEntry;
|
|
44
|
+
/** The declarations a module or global entry resolves to, each as its handle's key. */
|
|
45
|
+
readonly handles: ReadonlySet<string>;
|
|
46
|
+
/** The inventory's identifier of the declaration a symbol entry names. */
|
|
47
|
+
readonly ids: ReadonlySet<string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The key one declaration handle is compared by: its file and its index in that file. */
|
|
51
|
+
function handleKey(handle: NodeHandle): string {
|
|
52
|
+
return `${handle.path}:${String(handle.index)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The components of one declaration path and whether its last one is a static member.
|
|
57
|
+
* A path is split at the dots outside a quoted or a bracketed component, and each
|
|
58
|
+
* component keeps the spelling a reference gives it.
|
|
59
|
+
*/
|
|
60
|
+
function componentsOf(path: string): { readonly components: string[]; readonly static: boolean } {
|
|
61
|
+
const isStatic = path.endsWith(STATIC);
|
|
62
|
+
const body = isStatic ? path.slice(0, -STATIC.length) : path;
|
|
63
|
+
const components: string[] = [];
|
|
64
|
+
let current = "";
|
|
65
|
+
let closing = "";
|
|
66
|
+
for (let at = 0; at < body.length; at += 1) {
|
|
67
|
+
const ch = body.charAt(at);
|
|
68
|
+
if (closing === "'" && ch === "\\") {
|
|
69
|
+
current += ch + body.charAt(at + 1);
|
|
70
|
+
at += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (closing !== "") {
|
|
74
|
+
closing = ch === closing ? "" : closing;
|
|
75
|
+
} else if (ch === "'" || ch === "[") {
|
|
76
|
+
closing = ch === "'" ? "'" : "]";
|
|
77
|
+
} else if (ch === ".") {
|
|
78
|
+
components.push(current);
|
|
79
|
+
current = "";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
current += ch;
|
|
83
|
+
}
|
|
84
|
+
components.push(current);
|
|
85
|
+
return { components, static: isStatic };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What a symbol is once every alias it stands for is followed. */
|
|
89
|
+
function unaliased<Brand>(project: ProjectView<Brand>, symbol: TSSymbol): TSSymbol | undefined {
|
|
90
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
91
|
+
return symbol;
|
|
92
|
+
}
|
|
93
|
+
const target = project.checker.getAliasedSymbol(symbol);
|
|
94
|
+
return target.declarations.length === 0 ? undefined : target;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether one symbol is a member a class declares: a property, a method or an accessor. */
|
|
98
|
+
function isClassMember(symbol: TSSymbol): boolean {
|
|
99
|
+
return (symbol.flags & SymbolFlags.ClassMember) !== 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The members of one container a path component may name. A class keeps its static
|
|
104
|
+
* members among its exports beside what a merged namespace exports, so a component
|
|
105
|
+
* with the static suffix names one of the first and a component without it names an
|
|
106
|
+
* instance member or one of the second.
|
|
107
|
+
*/
|
|
108
|
+
function candidatesOf(container: TSSymbol, isStatic: boolean): readonly TSSymbol[] {
|
|
109
|
+
const exported = [...container.getExports().values()];
|
|
110
|
+
if (isStatic) {
|
|
111
|
+
return exported.filter(isClassMember);
|
|
112
|
+
}
|
|
113
|
+
const isClass = (container.flags & SymbolFlags.Class) !== 0;
|
|
114
|
+
return [
|
|
115
|
+
...container.getMembers().values(),
|
|
116
|
+
...exported.filter((member) => !isClass || !isClassMember(member)),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The member of one container a path component spells, followed through any alias. */
|
|
121
|
+
function memberOf<Brand>(
|
|
122
|
+
project: ProjectView<Brand>,
|
|
123
|
+
container: TSSymbol,
|
|
124
|
+
component: string,
|
|
125
|
+
isStatic: boolean,
|
|
126
|
+
): TSSymbol | undefined {
|
|
127
|
+
const member = candidatesOf(container, isStatic).find(
|
|
128
|
+
(candidate) => nameComponent(candidate.name) === component,
|
|
129
|
+
);
|
|
130
|
+
return member === undefined ? undefined : unaliased(project, member);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The declaration a path names from one starting symbol, component by component. */
|
|
134
|
+
function walkPath<Brand>(
|
|
135
|
+
project: ProjectView<Brand>,
|
|
136
|
+
start: TSSymbol | undefined,
|
|
137
|
+
rest: readonly string[],
|
|
138
|
+
isStatic: boolean,
|
|
139
|
+
): TSSymbol | undefined {
|
|
140
|
+
let at = start;
|
|
141
|
+
rest.forEach((component, index) => {
|
|
142
|
+
if (at !== undefined) {
|
|
143
|
+
at = memberOf(project, at, component, isStatic && index === rest.length - 1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return at;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether one node is literal text: a string literal or a template with no substitution. */
|
|
150
|
+
function isLiteralText(node: Node | undefined): node is Node & { readonly text: string } {
|
|
151
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The module specifier one node writes, for each form that imports or re-exports a module. */
|
|
155
|
+
function specifierOf(node: Node): Node | undefined {
|
|
156
|
+
if (isImportDeclaration(node) || isExportDeclaration(node)) {
|
|
157
|
+
return node.moduleSpecifier;
|
|
158
|
+
}
|
|
159
|
+
if (isImportEqualsDeclaration(node) && isExternalModuleReference(node.moduleReference)) {
|
|
160
|
+
return node.moduleReference.expression;
|
|
161
|
+
}
|
|
162
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
163
|
+
return node.arguments[0];
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The module each specifier the entries name resolves to in one project, read from a
|
|
170
|
+
* specifier the project's own files write, so the module is the one the target's own
|
|
171
|
+
* import resolves. A specifier no file writes resolves to no module.
|
|
172
|
+
*/
|
|
173
|
+
function modulesNamed<Brand>(
|
|
174
|
+
project: ProjectView<Brand>,
|
|
175
|
+
specifiers: ReadonlySet<string>,
|
|
176
|
+
): ReadonlyMap<string, TSSymbol> {
|
|
177
|
+
const written = new Map<string, Node>();
|
|
178
|
+
for (const file of project.ownSourceFiles()) {
|
|
179
|
+
const visit = (node: Node): void => {
|
|
180
|
+
const specifier = specifierOf(node);
|
|
181
|
+
if (
|
|
182
|
+
isLiteralText(specifier) &&
|
|
183
|
+
specifiers.has(specifier.text) &&
|
|
184
|
+
!written.has(specifier.text)
|
|
185
|
+
) {
|
|
186
|
+
written.set(specifier.text, specifier);
|
|
187
|
+
}
|
|
188
|
+
node.forEachChild(visit);
|
|
189
|
+
};
|
|
190
|
+
file.forEachChild(visit);
|
|
191
|
+
}
|
|
192
|
+
const specified = [...written];
|
|
193
|
+
const symbols = project.symbolsAt(specified.map(([, node]) => project.handle(node)));
|
|
194
|
+
const modules = new Map<string, TSSymbol>();
|
|
195
|
+
specified.forEach(([name], index) => {
|
|
196
|
+
const symbol = symbols[index];
|
|
197
|
+
if (symbol !== undefined) {
|
|
198
|
+
modules.set(name, symbol);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return modules;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The declaration each entry names in one project. A symbol entry names the
|
|
206
|
+
* inventory's declaration whose reference it spells exactly; a module entry names the
|
|
207
|
+
* declaration its path reaches from the exports of the module its specifier resolves
|
|
208
|
+
* to; a global entry names the declaration its path reaches from the global scope.
|
|
209
|
+
* Every alias along a path is followed to the declaration it stands for.
|
|
210
|
+
*/
|
|
211
|
+
export function resolveEntries<Brand>(
|
|
212
|
+
project: ProjectView<Brand>,
|
|
213
|
+
held: Inventory,
|
|
214
|
+
entries: readonly DeclarationEntry[],
|
|
215
|
+
): readonly ResolvedEntry[] {
|
|
216
|
+
if (entries.length === 0) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const byRef = new Map(held.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
220
|
+
const modules = modulesNamed(
|
|
221
|
+
project,
|
|
222
|
+
new Set(entries.flatMap((entry) => (entry.shape === "module" ? [entry.module] : []))),
|
|
223
|
+
);
|
|
224
|
+
return entries.map((entry) => {
|
|
225
|
+
if (entry.shape === "symbol") {
|
|
226
|
+
const id = byRef.get(entry.symbol);
|
|
227
|
+
return { entry, handles: new Set<string>(), ids: new Set(id === undefined ? [] : [id]) };
|
|
228
|
+
}
|
|
229
|
+
const { components, static: isStatic } = componentsOf(
|
|
230
|
+
entry.shape === "module" ? entry.name : entry.global,
|
|
231
|
+
);
|
|
232
|
+
const [first = "", ...rest] = components;
|
|
233
|
+
let start: TSSymbol | undefined;
|
|
234
|
+
if (isStatic && rest.length === 0) {
|
|
235
|
+
// Only a member is static, and a path of one component names no member.
|
|
236
|
+
start = undefined;
|
|
237
|
+
} else if (entry.shape === "module") {
|
|
238
|
+
const module = modules.get(entry.module);
|
|
239
|
+
start = module === undefined ? undefined : memberOf(project, module, first, false);
|
|
240
|
+
} else if (nameComponent(first) === first) {
|
|
241
|
+
const global = project.checker.resolveName(first, ANY_MEANING, undefined, false);
|
|
242
|
+
start = global === undefined ? undefined : unaliased(project, global);
|
|
243
|
+
}
|
|
244
|
+
const declared = walkPath(project, start, rest, isStatic);
|
|
245
|
+
return {
|
|
246
|
+
entry,
|
|
247
|
+
handles: new Set(declared?.declarations.map(handleKey) ?? []),
|
|
248
|
+
ids: new Set<string>(),
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Whether one symbol is the declaration a resolved entry names: it shares one of the
|
|
255
|
+
* entry's declarations, or, for an entry naming a declaration of the program, it is
|
|
256
|
+
* declared there. `chains` reads the symbol's declarations against the inventory the
|
|
257
|
+
* entries were resolved with.
|
|
258
|
+
*/
|
|
259
|
+
export function names(resolved: ResolvedEntry, symbol: TSSymbol, chains: AliasChains): boolean {
|
|
260
|
+
if (symbol.declarations.some((handle) => resolved.handles.has(handleKey(handle)))) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return resolved.ids.size > 0 && chains.declarationsOf(symbol).some((id) => resolved.ids.has(id));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Whether a resolved entry names a declaration, so that some symbol can be it. */
|
|
267
|
+
export function namesADeclaration(resolved: ResolvedEntry): boolean {
|
|
268
|
+
return resolved.handles.size > 0 || resolved.ids.size > 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The entries each of a batch of name nodes is: what the node resolves to, every alias
|
|
273
|
+
* followed, is the declaration each of them names. A node that is none is absent.
|
|
274
|
+
*/
|
|
275
|
+
export function entriesAt<Brand>(
|
|
276
|
+
project: ProjectView<Brand>,
|
|
277
|
+
chains: AliasChains,
|
|
278
|
+
nodes: readonly Node[],
|
|
279
|
+
resolved: readonly ResolvedEntry[],
|
|
280
|
+
): ReadonlyMap<Node, readonly ResolvedEntry[]> {
|
|
281
|
+
const naming = resolved.filter(namesADeclaration);
|
|
282
|
+
const found = new Map<Node, readonly ResolvedEntry[]>();
|
|
283
|
+
if (naming.length === 0) {
|
|
284
|
+
return found;
|
|
285
|
+
}
|
|
286
|
+
for (const [node, target] of resolvedTargets(project, nodes)) {
|
|
287
|
+
const entries = naming.filter((entry) => names(entry, target, chains));
|
|
288
|
+
if (entries.length > 0) {
|
|
289
|
+
found.set(node, entries);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* How one entry is spelled in a record's detail: its path, or for a declaration of the
|
|
297
|
+
* program the display name the inventory gives it.
|
|
298
|
+
*/
|
|
299
|
+
export function spelledEntry(entry: DeclarationEntry, held: Inventory): string {
|
|
300
|
+
switch (entry.shape) {
|
|
301
|
+
case "symbol":
|
|
302
|
+
return held.symbols.find((symbol) => symbol.ref === entry.symbol)?.name ?? entry.symbol;
|
|
303
|
+
case "module":
|
|
304
|
+
return entry.name;
|
|
305
|
+
case "global":
|
|
306
|
+
return entry.global;
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/decorator.ts
CHANGED
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
* the inventory holds for the class, static members included.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
isDecorator,
|
|
17
|
-
type Decorator,
|
|
18
|
-
type Node,
|
|
19
|
-
} from "@typescript/native/unstable/ast";
|
|
14
|
+
import type { Node } from "@typescript/native/unstable/ast";
|
|
15
|
+
import { decoratorsOf, spelledDecorator } from "./calls.ts";
|
|
20
16
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
21
17
|
import { nodeKey, type InventorySymbol, type SymbolKind } from "./inventory.ts";
|
|
22
18
|
import { renderPosition } from "./position.ts";
|
|
@@ -24,13 +20,6 @@ import { renderPosition } from "./position.ts";
|
|
|
24
20
|
/** The kinds of declaration a decorator is attached to and holds back. */
|
|
25
21
|
const DECORATED: ReadonlySet<SymbolKind> = new Set(["class", "method", "class-member"]);
|
|
26
22
|
|
|
27
|
-
/** How one decorator is spelled in a record: its expression, without a call's arguments. */
|
|
28
|
-
function spelled(decorator: Decorator): string {
|
|
29
|
-
const expression = decorator.expression;
|
|
30
|
-
const named = isCallExpression(expression) ? expression.expression : expression;
|
|
31
|
-
return `@${named.getText().replace(/\s+/gu, "")}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
/**
|
|
35
24
|
* The decorator detector: each decorated member, and each member of a decorated
|
|
36
25
|
* class, recorded at the decorator.
|
|
@@ -56,9 +45,8 @@ export function decorator<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
56
45
|
const id = held.declarations.get(nodeKey(file, node));
|
|
57
46
|
const symbol = id === undefined ? undefined : byId.get(id);
|
|
58
47
|
if (symbol !== undefined && DECORATED.has(symbol.kind)) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const detail = `decorated by ${spelled(attached)}`;
|
|
48
|
+
for (const attached of decoratorsOf(node)) {
|
|
49
|
+
const detail = `decorated by ${spelledDecorator(attached)}`;
|
|
62
50
|
const site = renderPosition(file, targetRoot, attached.getStart());
|
|
63
51
|
const reached = symbol.kind === "class" ? (membersOf.get(symbol.id) ?? []) : [symbol];
|
|
64
52
|
for (const member of reached) {
|
package/src/exempt.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* hands it the table of the classes it has.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Config } from "./config.ts";
|
|
10
|
+
import type { Config, TSSection } from "./config.ts";
|
|
11
11
|
import { ConfigError } from "./config.ts";
|
|
12
12
|
import {
|
|
13
13
|
EXEMPTION_CLASSES,
|
|
@@ -41,6 +41,10 @@ export interface DetectorInput<Brand> {
|
|
|
41
41
|
readonly targetRoot: string;
|
|
42
42
|
/** The configured templates, read once for the run. */
|
|
43
43
|
readonly templates: Templates;
|
|
44
|
+
/** The `ts` section, whose declarations the configured classes read. */
|
|
45
|
+
readonly ts: TSSection;
|
|
46
|
+
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
47
|
+
readonly consumers: readonly string[];
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RunSweep } from "../analysis.ts";
|
|
2
|
+
import type { Config } from "../config.ts";
|
|
3
|
+
import type { Finding } from "../finding.ts";
|
|
4
|
+
|
|
5
|
+
/** What every emitter reads: the configuration the run resolved and the run's sweep. */
|
|
6
|
+
export interface EmitterInput {
|
|
7
|
+
readonly config: Config;
|
|
8
|
+
/** The production sweep a report is built from, beside the matrix it judged. */
|
|
9
|
+
readonly swept: RunSweep;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One kind family's rule over one run: the findings of the codes in its family's range
|
|
14
|
+
* of the Contract's code space, in no particular order.
|
|
15
|
+
*/
|
|
16
|
+
export type Emitter = (input: EmitterInput) => readonly Finding[];
|
|
17
|
+
|
|
18
|
+
/** The emitter of each kind family, by the family's name in the Contract, in code order. */
|
|
19
|
+
export type Emitters = ReadonlyMap<string, Emitter>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
|
|
2
|
+
import type { Emitter, Emitters } from "./emitter.ts";
|
|
3
|
+
import { interfaces } from "./interfaces.ts";
|
|
4
|
+
import { intraFunction } from "./intra-function.ts";
|
|
5
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.ts";
|
|
6
|
+
import { readsAndWrites } from "./reads-and-writes.ts";
|
|
7
|
+
import { selfCheck } from "./self-check.ts";
|
|
8
|
+
import { unusedDeclarations } from "./unused-declarations.ts";
|
|
9
|
+
import { visibilityNarrowing } from "./visibility-narrowing.ts";
|
|
10
|
+
|
|
11
|
+
/** Every kind family this analyzer reports, each by its emitter, in code order. */
|
|
12
|
+
export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
13
|
+
["unused-declarations", unusedDeclarations],
|
|
14
|
+
["visibility-narrowing", visibilityNarrowing],
|
|
15
|
+
["interfaces", interfaces],
|
|
16
|
+
["reads-and-writes", readsAndWrites],
|
|
17
|
+
["non-code-artifacts", nonCodeArtifacts],
|
|
18
|
+
["dependencies-and-module-machinery", dependenciesAndModuleMachinery],
|
|
19
|
+
["self-check", selfCheck],
|
|
20
|
+
["intra-function", intraFunction],
|
|
21
|
+
]);
|