@cplieger/deadset-ts 3.3.1 → 3.5.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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The framework-lifecycle class: a member a `ts.lifecycle_contracts` entry lists is held
3
+ * back on each class that is the entry's component, being a class a decorator or a call
4
+ * the entry's `components` name is attached to or passes, or one extending, directly or
5
+ * through a chain, a class its `bases` name. A component's members are its type's, so a
6
+ * member a base class declares answers for every subclass.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isClassExpression,
12
+ isHeritageClause,
13
+ SyntaxKind,
14
+ type CallExpression,
15
+ type Node,
16
+ } from "@typescript/native/unstable/ast";
17
+ import type { Type } from "@typescript/native/unstable/sync";
18
+ import { aliasChains, type AliasChains } from "./alias-chain.ts";
19
+ import { calleeName, callsOf, classesPassed, decoratedClasses, decoratorCallee } from "./calls.ts";
20
+ import { classMembers, memberComponent, typesAt, type ClassSide } from "./class-members.ts";
21
+ import type { DeclarationEntry, LifecycleContract } from "./config.ts";
22
+ import {
23
+ entriesAt,
24
+ names,
25
+ namesADeclaration,
26
+ resolveEntries,
27
+ spelledEntry,
28
+ type ResolvedEntry,
29
+ } from "./configured-declarations.ts";
30
+ import type { DetectorInput, Evidence } from "./exempt.ts";
31
+ import { nodeKey, type Inventory } from "./inventory.ts";
32
+ import { renderPosition } from "./position.ts";
33
+ import type { ProjectView } from "./session.ts";
34
+
35
+ /** One class that is a component of one contract, and what says it is. */
36
+ interface Component {
37
+ readonly contract: LifecycleContract;
38
+ readonly id: string;
39
+ readonly site: Node;
40
+ readonly detail: string;
41
+ }
42
+
43
+ /** One contract's declarations, resolved in the project. */
44
+ interface ResolvedContract {
45
+ readonly contract: LifecycleContract;
46
+ readonly components: readonly ResolvedEntry[];
47
+ readonly bases: readonly ResolvedEntry[];
48
+ }
49
+
50
+ /** Every contract's components and bases, resolved in one pass over the entries. */
51
+ function resolveContracts<Brand>(
52
+ project: ProjectView<Brand>,
53
+ held: Inventory,
54
+ contracts: readonly LifecycleContract[],
55
+ ): readonly ResolvedContract[] {
56
+ const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
57
+ ...contract.components,
58
+ ...contract.bases,
59
+ ]);
60
+ const resolved = resolveEntries(project, held, entries);
61
+ let next = 0;
62
+ const take = (count: number): readonly ResolvedEntry[] => {
63
+ const taken = resolved.slice(next, next + count).filter(namesADeclaration);
64
+ next += count;
65
+ return taken;
66
+ };
67
+ return contracts.map((contract) => ({
68
+ contract,
69
+ components: take(contract.components.length),
70
+ bases: take(contract.bases.length),
71
+ }));
72
+ }
73
+
74
+ /** The classes a decorator or a call the contracts' components name makes a component. */
75
+ function byComponents<Brand>(
76
+ project: ProjectView<Brand>,
77
+ held: Inventory,
78
+ chains: AliasChains,
79
+ contracts: readonly ResolvedContract[],
80
+ ): readonly Component[] {
81
+ const routed = contracts.filter((one) => one.components.length > 0);
82
+ if (routed.length === 0) {
83
+ return [];
84
+ }
85
+ const contractOf = new Map(
86
+ routed.flatMap((one) => one.components.map((entry) => [entry, one.contract] as const)),
87
+ );
88
+ const calls = callsOf(project);
89
+ const decorated = decoratedClasses(project, held);
90
+ const named = entriesAt(
91
+ project,
92
+ chains,
93
+ [
94
+ ...calls.flatMap((call) => calleeName(call.expression) ?? []),
95
+ ...decorated.flatMap((one) =>
96
+ one.decorators.flatMap((decorator) => decoratorCallee(decorator) ?? []),
97
+ ),
98
+ ],
99
+ [...contractOf.keys()],
100
+ );
101
+ const naming = (node: Node | undefined): { contract: LifecycleContract; detail: string }[] =>
102
+ (node === undefined ? [] : (named.get(node) ?? [])).flatMap((entry) => {
103
+ const contract = contractOf.get(entry);
104
+ return contract === undefined ? [] : [{ contract, detail: spelledEntry(entry.entry, held) }];
105
+ });
106
+
107
+ const found: Component[] = [];
108
+ for (const one of decorated) {
109
+ for (const decorator of one.decorators) {
110
+ for (const { contract, detail } of naming(decoratorCallee(decorator))) {
111
+ found.push({ contract, id: one.id, site: decorator, detail: `decorated by @${detail}` });
112
+ }
113
+ }
114
+ }
115
+ const matched = new Map<CallExpression, { contract: LifecycleContract; detail: string }[]>();
116
+ for (const call of calls) {
117
+ const routes = naming(calleeName(call.expression));
118
+ if (routes.length > 0) {
119
+ matched.set(call, routes);
120
+ }
121
+ }
122
+ for (const [call, uses] of classesPassed(project, held, chains, [...matched.keys()])) {
123
+ for (const route of matched.get(call) ?? []) {
124
+ for (const use of uses) {
125
+ found.push({
126
+ contract: route.contract,
127
+ id: use.id,
128
+ site: use.site,
129
+ detail: `registered by ${route.detail}`,
130
+ });
131
+ }
132
+ }
133
+ }
134
+ return found;
135
+ }
136
+
137
+ /** Each class declaration of the target, with its name and the class its `extends` names. */
138
+ interface Declared {
139
+ readonly id: string;
140
+ readonly name: Node;
141
+ readonly extended: Node;
142
+ }
143
+
144
+ /** The class declarations of the target that extend another class. */
145
+ function extendingClasses<Brand>(
146
+ project: ProjectView<Brand>,
147
+ held: Inventory,
148
+ ): readonly Declared[] {
149
+ const found: Declared[] = [];
150
+ for (const file of project.ownSourceFiles()) {
151
+ const visit = (node: Node): void => {
152
+ if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
153
+ const id = held.declarations.get(nodeKey(file, node));
154
+ const clause = (node.heritageClauses ?? []).find(
155
+ (one) => isHeritageClause(one) && one.token === SyntaxKind.ExtendsKeyword,
156
+ );
157
+ const extended =
158
+ clause !== undefined && isHeritageClause(clause) ? clause.types[0] : undefined;
159
+ if (id !== undefined && extended !== undefined) {
160
+ found.push({ id, name: node.name, extended });
161
+ }
162
+ }
163
+ node.forEachChild(visit);
164
+ };
165
+ file.forEachChild(visit);
166
+ }
167
+ return found;
168
+ }
169
+
170
+ /** The classes that extend, directly or through a chain, a class the contracts' bases name. */
171
+ function byBases<Brand>(
172
+ project: ProjectView<Brand>,
173
+ held: Inventory,
174
+ chains: AliasChains,
175
+ contracts: readonly ResolvedContract[],
176
+ ): readonly Component[] {
177
+ const based = contracts.filter((one) => one.bases.length > 0);
178
+ if (based.length === 0) {
179
+ return [];
180
+ }
181
+ const classes = extendingClasses(project, held);
182
+ if (classes.length === 0) {
183
+ return [];
184
+ }
185
+ const checker = project.checker;
186
+ const ancestry = new Map<number, readonly Type[]>();
187
+ const ancestorsOf = (type: Type, seen: Set<number>): readonly Type[] => {
188
+ const known = ancestry.get(type.id);
189
+ if (known !== undefined) {
190
+ return known;
191
+ }
192
+ const declared = type.isClassOrInterface()
193
+ ? type
194
+ : type.isTypeReference()
195
+ ? type.getTarget()
196
+ : undefined;
197
+ if (declared === undefined || !declared.isClassOrInterface() || seen.has(declared.id)) {
198
+ return [];
199
+ }
200
+ seen.add(declared.id);
201
+ const found = checker
202
+ .getBaseTypes(declared)
203
+ .flatMap((base) => [base, ...ancestorsOf(base, seen)]);
204
+ ancestry.set(type.id, found);
205
+ return found;
206
+ };
207
+ const types = typesAt(
208
+ project,
209
+ classes.map((one) => one.name),
210
+ );
211
+
212
+ const found: Component[] = [];
213
+ classes.forEach((one, index) => {
214
+ const type = types[index];
215
+ if (type === undefined) {
216
+ return;
217
+ }
218
+ const ancestors = ancestorsOf(type, new Set()).flatMap((base) => base.getSymbol() ?? []);
219
+ for (const contract of based) {
220
+ for (const entry of contract.bases) {
221
+ if (ancestors.some((symbol) => names(entry, symbol, chains))) {
222
+ found.push({
223
+ contract: contract.contract,
224
+ id: one.id,
225
+ site: one.extended,
226
+ detail: `extends ${spelledEntry(entry.entry, held)}`,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ });
232
+ return found;
233
+ }
234
+
235
+ /**
236
+ * The framework-lifecycle detector: each member a contract lists, on each class that
237
+ * is a component of that contract's framework, recorded where the class was found to
238
+ * be one.
239
+ */
240
+ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
241
+ const { project, held, targetRoot } = input;
242
+ if (input.ts.lifecycleContracts.length === 0) {
243
+ return [];
244
+ }
245
+ const chains = aliasChains(project, held);
246
+ const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
247
+ const components = [
248
+ ...byComponents(project, held, chains, contracts),
249
+ ...byBases(project, held, chains, contracts),
250
+ ];
251
+ if (components.length === 0) {
252
+ return [];
253
+ }
254
+ const ids = new Set(components.map((one) => one.id));
255
+ const sides: readonly ClassSide[] = input.ts.lifecycleContracts.some((contract) =>
256
+ contract.members.some((name) => name.endsWith(":static")),
257
+ )
258
+ ? ["instance", "static"]
259
+ : ["instance"];
260
+ const members = sides.map((side) => classMembers(project, held, ids, side));
261
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
262
+
263
+ const found: Evidence[] = [];
264
+ for (const component of components) {
265
+ const listed = new Set(component.contract.members);
266
+ const site = renderPosition(
267
+ component.site.getSourceFile(),
268
+ targetRoot,
269
+ component.site.getStart(),
270
+ );
271
+ for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
272
+ const symbol = byId.get(member.id);
273
+ const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
274
+ if (spelled !== undefined && listed.has(spelled)) {
275
+ found.push({ id: member.id, detail: component.detail, site });
276
+ }
277
+ }
278
+ }
279
+ return found;
280
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The injection-container class: a container constructs a class passed to a call
3
+ * `ts.injection_registrations` names, or one a parameter of whose constructor carries a
4
+ * decorator, and populates each of its properties and accessors that carries one, a base
5
+ * class's included. A constructor and its parameters are no declarations of the
6
+ * inventory, so the class stands for them and the registration referencing it keeps it.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isClassExpression,
12
+ isConstructorDeclaration,
13
+ isGetAccessorDeclaration,
14
+ isPropertyDeclaration,
15
+ isSetAccessorDeclaration,
16
+ type CallExpression,
17
+ type Node,
18
+ } from "@typescript/native/unstable/ast";
19
+ import { aliasChains } from "./alias-chain.ts";
20
+ import { calleeName, callsOf, classesPassed, decoratorsOf, spelledDecorator } from "./calls.ts";
21
+ import { classMembers, type ClassMember } from "./class-members.ts";
22
+ import {
23
+ entriesAt,
24
+ namesADeclaration,
25
+ resolveEntries,
26
+ spelledEntry,
27
+ type ResolvedEntry,
28
+ } from "./configured-declarations.ts";
29
+ import type { DetectorInput, Evidence } from "./exempt.ts";
30
+ import { nodeKey } from "./inventory.ts";
31
+ import { renderPosition } from "./position.ts";
32
+
33
+ /** One class a container constructs, by the registration that says so. */
34
+ interface Registration {
35
+ readonly id: string;
36
+ readonly site: Node;
37
+ readonly detail: string;
38
+ }
39
+
40
+ /** Whether one member declaration is a property or an accessor carrying a decorator. */
41
+ function isInjected(node: Node): boolean {
42
+ return (
43
+ (isPropertyDeclaration(node) ||
44
+ isGetAccessorDeclaration(node) ||
45
+ isSetAccessorDeclaration(node)) &&
46
+ decoratorsOf(node).length > 0
47
+ );
48
+ }
49
+
50
+ /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
51
+ function registrations<Brand>(input: DetectorInput<Brand>): readonly Registration[] {
52
+ const { project, held } = input;
53
+ const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
54
+ if (!resolved.some(namesADeclaration)) {
55
+ return [];
56
+ }
57
+ const chains = aliasChains(project, held);
58
+ const calls = callsOf(project);
59
+ const named = entriesAt(
60
+ project,
61
+ chains,
62
+ calls.flatMap((call) => calleeName(call.expression) ?? []),
63
+ resolved,
64
+ );
65
+ const matched = new Map<CallExpression, ResolvedEntry>();
66
+ for (const call of calls) {
67
+ const name = calleeName(call.expression);
68
+ const [entry] = name === undefined ? [] : (named.get(name) ?? []);
69
+ if (entry !== undefined) {
70
+ matched.set(call, entry);
71
+ }
72
+ }
73
+ return [...classesPassed(project, held, chains, [...matched.keys()])].flatMap(([call, uses]) => {
74
+ const entry = matched.get(call);
75
+ const detail = entry === undefined ? "" : `registered by ${spelledEntry(entry.entry, held)}`;
76
+ return uses.map((use) => ({ id: use.id, site: use.site, detail }));
77
+ });
78
+ }
79
+
80
+ /** The classes of the target a parameter of whose constructor carries a decorator. */
81
+ function constructedForParameters<Brand>(input: DetectorInput<Brand>): ReadonlySet<string> {
82
+ const { project, held } = input;
83
+ const found = new Set<string>();
84
+ for (const file of project.ownSourceFiles()) {
85
+ const visit = (node: Node): void => {
86
+ if (isClassDeclaration(node) || isClassExpression(node)) {
87
+ const id = held.declarations.get(nodeKey(file, node));
88
+ const asks = node.members.some(
89
+ (member) =>
90
+ isConstructorDeclaration(member) &&
91
+ member.parameters.some((parameter) => decoratorsOf(parameter).length > 0),
92
+ );
93
+ if (id !== undefined && asks) {
94
+ found.add(id);
95
+ }
96
+ }
97
+ node.forEachChild(visit);
98
+ };
99
+ file.forEachChild(visit);
100
+ }
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * The injection-container detector: each decorated property or accessor of a class a
106
+ * container constructs, recorded at the registration that names the class, or, for a
107
+ * class whose constructor asks for its parameters, at the member's own decorator.
108
+ */
109
+ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
110
+ const { project, held, targetRoot } = input;
111
+ const registered = registrations(input);
112
+ const constructed = constructedForParameters(input);
113
+ const classes = new Set([...registered.map((one) => one.id), ...constructed]);
114
+ if (classes.size === 0) {
115
+ return [];
116
+ }
117
+ const members = classMembers(project, held, classes, "instance");
118
+ const injected = (id: string): readonly ClassMember[] =>
119
+ (members.get(id) ?? []).filter((member) => isInjected(member.node));
120
+ const at = (node: Node) => renderPosition(node.getSourceFile(), targetRoot, node.getStart());
121
+
122
+ const found: Evidence[] = [];
123
+ for (const registration of registered) {
124
+ for (const member of injected(registration.id)) {
125
+ found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
126
+ }
127
+ }
128
+ for (const id of constructed) {
129
+ for (const member of injected(id)) {
130
+ const [decorator] = decoratorsOf(member.node);
131
+ if (decorator !== undefined) {
132
+ found.push({
133
+ id: member.id,
134
+ detail: `injected by ${spelledDecorator(decorator)}`,
135
+ site: at(decorator),
136
+ });
137
+ }
138
+ }
139
+ }
140
+ return found;
141
+ }
package/src/kinds.ts CHANGED
@@ -10,3 +10,44 @@
10
10
  * code the Contract fixes arrives here as a failing test.
11
11
  */
12
12
  export const FIXED_SEVERITY_CODES: readonly string[] = ["DS1703", "DS1704"];
13
+
14
+ /**
15
+ * The code of every live issue kind, in ascending order. A severity key names one of
16
+ * them or a family prefix whose range holds one; a key naming a retired code, an
17
+ * unassigned code or a range holding no live kind is an unimplemented key, so every
18
+ * severity a configuration sets is one some analyzer reports under. A test compares
19
+ * this list against the Contract release the analyzer is written against.
20
+ */
21
+ export const LIVE_CODES: readonly string[] = [
22
+ "DS1001",
23
+ "DS1002",
24
+ "DS1003",
25
+ "DS1004",
26
+ "DS1005",
27
+ "DS1006",
28
+ "DS1101",
29
+ "DS1102",
30
+ "DS1103",
31
+ "DS1104",
32
+ "DS1201",
33
+ "DS1203",
34
+ "DS1204",
35
+ "DS1301",
36
+ "DS1302",
37
+ "DS1303",
38
+ "DS1501",
39
+ "DS1502",
40
+ "DS1601",
41
+ "DS1605",
42
+ "DS1701",
43
+ "DS1702",
44
+ "DS1703",
45
+ "DS1704",
46
+ "DS1705",
47
+ "DS1801",
48
+ "DS1802",
49
+ "DS1803",
50
+ "DS1805",
51
+ "DS1807",
52
+ "DS1809",
53
+ ];
package/src/print.ts CHANGED
@@ -1,4 +1,31 @@
1
- import { renderOrigin, type Config, type Provenance } from "./config.ts";
1
+ import {
2
+ renderOrigin,
3
+ type Config,
4
+ type DeclarationEntry,
5
+ type Provenance,
6
+ type Provider,
7
+ } from "./config.ts";
8
+
9
+ /** One declaration entry as a document writes it: the members of its shape alone. */
10
+ function declarationOf(entry: DeclarationEntry): Record<string, string> {
11
+ switch (entry.shape) {
12
+ case "symbol":
13
+ return { symbol: entry.symbol };
14
+ case "module":
15
+ return { module: entry.module, name: entry.name };
16
+ case "global":
17
+ return { global: entry.global };
18
+ }
19
+ }
20
+
21
+ /** One provider entry as a document writes it: the members of its shape alone. */
22
+ function providerOf(entry: Provider): Record<string, unknown> {
23
+ const installed = { name: entry.name, languages: entry.languages, command: entry.command };
24
+ if (entry.shape === "installed") {
25
+ return installed;
26
+ }
27
+ return { ...installed, source: entry.source, version: entry.version, digest: entry.digest };
28
+ }
2
29
 
3
30
  /**
4
31
  * The resolved configuration and its provenance as one JSON object, indented,
@@ -43,10 +70,18 @@ export function printConfig(config: Config, provenance: Provenance): string {
43
70
  max_findings: config.reporters.maxFindings,
44
71
  fail_on: config.reporters.failOn,
45
72
  },
73
+ providers: { analyzers: config.providers.analyzers.map(providerOf) },
46
74
  go: {},
47
75
  ts: {
48
76
  test_files: config.ts.testFiles,
49
77
  entry_files: config.ts.entryFiles,
78
+ injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
79
+ lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
80
+ components: entry.components.map(declarationOf),
81
+ bases: entry.bases.map(declarationOf),
82
+ members: entry.members,
83
+ })),
84
+ serializers: config.ts.serializers.map(declarationOf),
50
85
  },
51
86
  provenance: Object.fromEntries(
52
87
  [...provenance]