@aoelite/refpact-ts 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/baseline.d.ts +26 -0
- package/dist/baseline.js +65 -0
- package/dist/core/index.d.ts +88 -0
- package/dist/core/index.js +105 -0
- package/dist/core/model.d.ts +340 -0
- package/dist/core/model.js +1 -0
- package/dist/core/results.d.ts +268 -0
- package/dist/core/results.js +1 -0
- package/dist/core/wasm.d.ts +7 -0
- package/dist/core/wasm.js +67 -0
- package/dist/custom.d.ts +166 -0
- package/dist/custom.js +239 -0
- package/dist/filters.d.ts +73 -0
- package/dist/filters.js +39 -0
- package/dist/frontend/checks.d.ts +15 -0
- package/dist/frontend/checks.js +12 -0
- package/dist/frontend/classic.d.ts +3 -0
- package/dist/frontend/classic.js +105 -0
- package/dist/frontend/compiler.d.ts +93 -0
- package/dist/frontend/compiler.js +116 -0
- package/dist/frontend/components.d.ts +29 -0
- package/dist/frontend/components.js +111 -0
- package/dist/frontend/declarations.d.ts +45 -0
- package/dist/frontend/declarations.js +158 -0
- package/dist/frontend/dependencies.d.ts +53 -0
- package/dist/frontend/dependencies.js +337 -0
- package/dist/frontend/imports.d.ts +149 -0
- package/dist/frontend/imports.js +765 -0
- package/dist/frontend/native.d.ts +3 -0
- package/dist/frontend/native.js +165 -0
- package/dist/frontend/paths.d.ts +10 -0
- package/dist/frontend/paths.js +18 -0
- package/dist/frontend/project.d.ts +118 -0
- package/dist/frontend/project.js +775 -0
- package/dist/group.d.ts +74 -0
- package/dist/group.js +106 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +27 -0
- package/dist/items.d.ts +133 -0
- package/dist/items.js +179 -0
- package/dist/layers.d.ts +112 -0
- package/dist/layers.js +215 -0
- package/dist/onion.d.ts +78 -0
- package/dist/onion.js +96 -0
- package/dist/patterns.d.ts +14 -0
- package/dist/patterns.js +15 -0
- package/dist/rules.d.ts +408 -0
- package/dist/rules.js +665 -0
- package/package.json +52 -4
- package/wasm/refpact_core.wasm +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core of refpact, the Rust crate `refpact-core`, as WebAssembly (SPEC section 8).
|
|
3
|
+
*
|
|
4
|
+
* The module is `wasm/refpact_core.wasm` beside `src` and `dist`, built by `cargo xtask wasm`.
|
|
5
|
+
* It is compiled once, on the first call, and speaks JSON requests. This is the only file of
|
|
6
|
+
* the core that reads a file.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
/** The version of the wire format that this package speaks. */
|
|
11
|
+
const VERSION = 1;
|
|
12
|
+
let core;
|
|
13
|
+
/** Counts the instances. A handle of a fact set is valid only in the instance that made it. */
|
|
14
|
+
let generation = 0;
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
function instance() {
|
|
18
|
+
if (core !== undefined) {
|
|
19
|
+
return core;
|
|
20
|
+
}
|
|
21
|
+
const url = new URL("../../wasm/refpact_core.wasm", import.meta.url);
|
|
22
|
+
let bytes;
|
|
23
|
+
try {
|
|
24
|
+
bytes = readFileSync(url);
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
throw new Error(`refpact: the core is missing at ${fileURLToPath(url)}. Build it with \`cargo xtask wasm\`.`, { cause });
|
|
28
|
+
}
|
|
29
|
+
const made = new WebAssembly.Instance(new WebAssembly.Module(bytes), {}).exports;
|
|
30
|
+
core = made;
|
|
31
|
+
generation += 1;
|
|
32
|
+
const { version } = JSON.parse(call('{"op":"version"}'));
|
|
33
|
+
if (version !== VERSION) {
|
|
34
|
+
core = undefined;
|
|
35
|
+
throw new Error(`refpact: the core speaks version ${version} of the wire format, not ${VERSION}. Build it again with \`cargo xtask wasm\`.`);
|
|
36
|
+
}
|
|
37
|
+
return made;
|
|
38
|
+
}
|
|
39
|
+
/** The instance that the handles of fact sets belong to. */
|
|
40
|
+
export function currentGeneration() {
|
|
41
|
+
instance();
|
|
42
|
+
return generation;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sends one request and returns the answer as JSON text. Throws when the core finds the
|
|
46
|
+
* request not valid, which is a bug of this package and not of the rule.
|
|
47
|
+
*/
|
|
48
|
+
export function call(request) {
|
|
49
|
+
const exports = instance();
|
|
50
|
+
const bytes = encoder.encode(request);
|
|
51
|
+
const address = exports.refpact_alloc(bytes.length);
|
|
52
|
+
new Uint8Array(exports.memory.buffer, address, bytes.length).set(bytes);
|
|
53
|
+
let status;
|
|
54
|
+
try {
|
|
55
|
+
status = exports.refpact_call(address, bytes.length);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// A trap leaves the instance in an unknown state. The next call starts a new one.
|
|
59
|
+
core = undefined;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const answer = decoder.decode(new Uint8Array(exports.memory.buffer, exports.refpact_answer(), exports.refpact_answer_len()));
|
|
63
|
+
if (status !== 0) {
|
|
64
|
+
throw new Error(`refpact: the core rejected a request: ${answer}`);
|
|
65
|
+
}
|
|
66
|
+
return answer;
|
|
67
|
+
}
|
package/dist/custom.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Checks of the user: what a rule sees, and how its violations are made (SPEC 4.15). */
|
|
2
|
+
import type { Declaration, Detail, Fact, FactSet, Path, Reference, GraphResult, SelectResult, Step, TanglesResult, Violation } from "./core/index.ts";
|
|
3
|
+
/** An import that the rule sees: the filters and the exemptions of the rule let it pass. */
|
|
4
|
+
export interface SeenImport {
|
|
5
|
+
/** The fact of the import. */
|
|
6
|
+
readonly fact: Fact;
|
|
7
|
+
/** The reference of the fact. */
|
|
8
|
+
readonly reference: Reference;
|
|
9
|
+
/** The file that imports. */
|
|
10
|
+
readonly file: Path;
|
|
11
|
+
/** Whether the rule selects the file that imports. */
|
|
12
|
+
readonly subject: boolean;
|
|
13
|
+
/** The paths of the import, `[target, ...aliases]`. */
|
|
14
|
+
readonly paths: readonly Path[];
|
|
15
|
+
/** For each path, the file that has it, or `undefined` for a package or a module of Node. */
|
|
16
|
+
readonly owners: readonly (Path | undefined)[];
|
|
17
|
+
/**
|
|
18
|
+
* For each list of targets that the check names: the first path of the import that the
|
|
19
|
+
* list matches. A list that matches no path of the import has no entry.
|
|
20
|
+
*/
|
|
21
|
+
readonly hits: Readonly<Record<string, Path>>;
|
|
22
|
+
}
|
|
23
|
+
/** A marker of a selected file that the rule sees. */
|
|
24
|
+
export interface SeenMarker {
|
|
25
|
+
/** The fact of the marker. */
|
|
26
|
+
readonly fact: Fact;
|
|
27
|
+
/** The file. */
|
|
28
|
+
readonly file: Path;
|
|
29
|
+
/** The marker. */
|
|
30
|
+
readonly marker: string;
|
|
31
|
+
}
|
|
32
|
+
/** An edge of the graph of files, with the first import that makes it. */
|
|
33
|
+
export interface ImportEdge {
|
|
34
|
+
/** The file that the edge leads to. */
|
|
35
|
+
readonly to: Path;
|
|
36
|
+
/** The fact of the import. */
|
|
37
|
+
readonly fact: Fact;
|
|
38
|
+
/** The path of the import that leads to the file. */
|
|
39
|
+
readonly target: Path;
|
|
40
|
+
}
|
|
41
|
+
/** Files that all reach each other by imports, with one of their cycles: see {@link Graph.tangles}. */
|
|
42
|
+
export interface Tangle {
|
|
43
|
+
/** The files of the tangle, in the order of the files of the project. */
|
|
44
|
+
readonly members: readonly Path[];
|
|
45
|
+
/** A shortest cycle through the first member. The last step leads back to it. */
|
|
46
|
+
readonly cycle: readonly Step[];
|
|
47
|
+
}
|
|
48
|
+
/** The names of the lists of patterns of a check, in the order of the lists in the request. @internal */
|
|
49
|
+
export interface ListNames {
|
|
50
|
+
readonly targets: readonly string[];
|
|
51
|
+
readonly files: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
type Selection = Extract<SelectResult, {
|
|
54
|
+
ok: true;
|
|
55
|
+
}>;
|
|
56
|
+
/** A graph of the core. With `subjects`, it is the graph of the selected files. @internal */
|
|
57
|
+
export type CoreGraph = Extract<GraphResult, {
|
|
58
|
+
ok: true;
|
|
59
|
+
}>;
|
|
60
|
+
type CoreTangles = Extract<TanglesResult, {
|
|
61
|
+
ok: true;
|
|
62
|
+
}>["tangles"];
|
|
63
|
+
/** Asks the core for a graph or for its tangles. The core makes each only for a check that uses it. @internal */
|
|
64
|
+
export interface CoreGraphs {
|
|
65
|
+
readonly graph: (subjects: boolean) => CoreGraph;
|
|
66
|
+
readonly tangles: (subjects: boolean) => CoreTangles;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a check of the user gets: the files that the rule selects and what the rule sees. The
|
|
70
|
+
* rule has applied its filters and its exemptions.
|
|
71
|
+
*/
|
|
72
|
+
export declare class Scope {
|
|
73
|
+
/** The name of the rule. */
|
|
74
|
+
readonly ruleName: string;
|
|
75
|
+
/** The facts of the project. */
|
|
76
|
+
readonly facts: FactSet;
|
|
77
|
+
/** The files that the rule selects, in order. */
|
|
78
|
+
readonly subjects: readonly Path[];
|
|
79
|
+
/** The imports that the rule sees, of all files, in the order of the facts. */
|
|
80
|
+
readonly imports: readonly SeenImport[];
|
|
81
|
+
/** The markers of the selected files that the rule sees. */
|
|
82
|
+
readonly markers: readonly SeenMarker[];
|
|
83
|
+
/** The declarations of the selected files that the rule sees. */
|
|
84
|
+
readonly declarations: readonly Declaration[];
|
|
85
|
+
/** @internal */
|
|
86
|
+
protected readonly selection: Selection;
|
|
87
|
+
private readonly selected;
|
|
88
|
+
private readonly lists;
|
|
89
|
+
private firstFiles;
|
|
90
|
+
private readonly core;
|
|
91
|
+
private readonly made;
|
|
92
|
+
/** @internal */
|
|
93
|
+
constructor(ruleName: string, facts: FactSet, selection: Selection, names: ListNames, core: CoreGraphs);
|
|
94
|
+
/** Whether the rule selects the file. */
|
|
95
|
+
selects(file: Path): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* The files that a list of file patterns of the check matches, by the name of the list.
|
|
98
|
+
* The files match as the files of a rule do. Throws when the check names no such list.
|
|
99
|
+
*/
|
|
100
|
+
matched(list: string): readonly Path[];
|
|
101
|
+
/**
|
|
102
|
+
* The graph of all files, selected or not, from the imports that the rule sees. An import
|
|
103
|
+
* leads to the file of each of its paths, so an import through a barrel file leads to the
|
|
104
|
+
* barrel and to the file that declares the name. An import of the own file is no edge.
|
|
105
|
+
*/
|
|
106
|
+
graph(): Graph;
|
|
107
|
+
/**
|
|
108
|
+
* The graph of the selected files only, from the imports that the rule sees: the graph
|
|
109
|
+
* whose tangles `mustBeAcyclic` reports. An import of a file that the rule does not select
|
|
110
|
+
* is no edge, so a cycle through such a file is no tangle.
|
|
111
|
+
*/
|
|
112
|
+
subjectGraph(): Graph;
|
|
113
|
+
/**
|
|
114
|
+
* A violation at the place of a fact, with its note. A message gives a `custom` detail,
|
|
115
|
+
* which a baseline counts by `key`, or by the message when there is no key.
|
|
116
|
+
*/
|
|
117
|
+
violationAt(fact: Fact, detail: string | Detail, key?: string): Violation;
|
|
118
|
+
/** A violation at the place of a declaration. */
|
|
119
|
+
violationOfItem(declaration: Declaration, detail: string | Detail, baselineKey?: string): Violation;
|
|
120
|
+
/** A violation about a file as a whole, with no line. */
|
|
121
|
+
violationOfFile(file: Path, detail: string | Detail, baselineKey?: string): Violation;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* What a check of items gets: the {@link Scope} of the rule, and the items that the filters
|
|
125
|
+
* of the rule select.
|
|
126
|
+
*/
|
|
127
|
+
export declare class ItemScope extends Scope {
|
|
128
|
+
/** The items that the rule selects, in order. */
|
|
129
|
+
readonly items: readonly Declaration[];
|
|
130
|
+
/**
|
|
131
|
+
* The imports that the rule sees inside the selected items. A front end that does not say
|
|
132
|
+
* which item holds an import gives none: the TypeScript front end is one.
|
|
133
|
+
*/
|
|
134
|
+
readonly itemImports: readonly SeenImport[];
|
|
135
|
+
/** @internal */
|
|
136
|
+
constructor(ruleName: string, facts: FactSet, selection: Selection, names: ListNames, core: CoreGraphs);
|
|
137
|
+
}
|
|
138
|
+
/** A graph of files: see {@link Scope.graph} and {@link Scope.subjectGraph}. */
|
|
139
|
+
export declare class Graph {
|
|
140
|
+
/** The file of each node. */
|
|
141
|
+
private readonly units;
|
|
142
|
+
private readonly numbers;
|
|
143
|
+
/** For each file, by its number: the edges, each with the number of the file that it leads to. */
|
|
144
|
+
private readonly edges;
|
|
145
|
+
/** Asks the core for the tangles, on the first use of them. */
|
|
146
|
+
private readonly coreTangles;
|
|
147
|
+
private found;
|
|
148
|
+
/** @internal */
|
|
149
|
+
constructor(facts: FactSet, graph: CoreGraph, coreTangles: () => CoreTangles);
|
|
150
|
+
/** The edges of a file, in the order of their first imports. A path that is no file of the graph has none. */
|
|
151
|
+
edgesOf(file: Path): readonly ImportEdge[];
|
|
152
|
+
/**
|
|
153
|
+
* The tangles: each group of two or more files that all reach each other, once, in the
|
|
154
|
+
* order of their first files.
|
|
155
|
+
*/
|
|
156
|
+
tangles(): Tangle[];
|
|
157
|
+
/** An edge as the step of a report. */
|
|
158
|
+
private step;
|
|
159
|
+
/**
|
|
160
|
+
* A shortest chain of imports from a file to a file that is an end, as the steps of a
|
|
161
|
+
* report. No steps: the file itself is an end. `undefined`: no chain leads to an end. The
|
|
162
|
+
* search is breadth-first with the edges of each file in their order.
|
|
163
|
+
*/
|
|
164
|
+
chain(from: Path, isEnd: (file: Path) => boolean): Step[] | undefined;
|
|
165
|
+
}
|
|
166
|
+
export {};
|
package/dist/custom.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a check of the user gets: the files that the rule selects and what the rule sees. The
|
|
3
|
+
* rule has applied its filters and its exemptions.
|
|
4
|
+
*/
|
|
5
|
+
export class Scope {
|
|
6
|
+
/** The name of the rule. */
|
|
7
|
+
ruleName;
|
|
8
|
+
/** The facts of the project. */
|
|
9
|
+
facts;
|
|
10
|
+
/** The files that the rule selects, in order. */
|
|
11
|
+
subjects;
|
|
12
|
+
/** The imports that the rule sees, of all files, in the order of the facts. */
|
|
13
|
+
imports;
|
|
14
|
+
/** The markers of the selected files that the rule sees. */
|
|
15
|
+
markers;
|
|
16
|
+
/** The declarations of the selected files that the rule sees. */
|
|
17
|
+
declarations;
|
|
18
|
+
/** @internal */
|
|
19
|
+
selection;
|
|
20
|
+
selected;
|
|
21
|
+
lists;
|
|
22
|
+
firstFiles;
|
|
23
|
+
core;
|
|
24
|
+
made = {};
|
|
25
|
+
/** @internal */
|
|
26
|
+
constructor(ruleName, facts, selection, names, core) {
|
|
27
|
+
this.ruleName = ruleName;
|
|
28
|
+
this.facts = facts;
|
|
29
|
+
this.selection = selection;
|
|
30
|
+
this.core = core;
|
|
31
|
+
const units = facts.units;
|
|
32
|
+
this.subjects = selection.subjects.map((unit) => units[unit]);
|
|
33
|
+
this.selected = new Set(this.subjects.map(key));
|
|
34
|
+
this.lists = new Map(names.files.map((name, list) => [name, (selection.units[list] ?? []).map((unit) => units[unit])]));
|
|
35
|
+
const subjectSet = new Set(selection.subjects);
|
|
36
|
+
this.imports = selection.references.map((seen) => {
|
|
37
|
+
const fact = facts.facts[seen.fact];
|
|
38
|
+
const reference = fact.what;
|
|
39
|
+
const paths = [reference.target, ...reference.aliases];
|
|
40
|
+
const hits = {};
|
|
41
|
+
seen.hits?.forEach((hit, list) => {
|
|
42
|
+
if (hit !== null) {
|
|
43
|
+
hits[names.targets[list]] = paths[hit];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
fact,
|
|
48
|
+
reference,
|
|
49
|
+
file: units[seen.unit],
|
|
50
|
+
subject: subjectSet.has(seen.unit),
|
|
51
|
+
paths,
|
|
52
|
+
owners: seen.owners.map((owner) => (owner === null ? undefined : units[owner])),
|
|
53
|
+
hits,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
this.markers = selection.markers.map((position) => {
|
|
57
|
+
const fact = facts.facts[position];
|
|
58
|
+
return { fact, file: fact.unit, marker: fact.what.marker };
|
|
59
|
+
});
|
|
60
|
+
const declarations = facts.declarations ?? [];
|
|
61
|
+
this.declarations = selection.declarations.map((position) => declarations[position]);
|
|
62
|
+
}
|
|
63
|
+
/** Whether the rule selects the file. */
|
|
64
|
+
selects(file) {
|
|
65
|
+
return this.selected.has(key(file));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The files that a list of file patterns of the check matches, by the name of the list.
|
|
69
|
+
* The files match as the files of a rule do. Throws when the check names no such list.
|
|
70
|
+
*/
|
|
71
|
+
matched(list) {
|
|
72
|
+
const files = this.lists.get(list);
|
|
73
|
+
if (files === undefined) {
|
|
74
|
+
throw new TypeError(`refpact: rule \`${this.ruleName}\`: the check names no list of files \`${list}\``);
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The graph of all files, selected or not, from the imports that the rule sees. An import
|
|
80
|
+
* leads to the file of each of its paths, so an import through a barrel file leads to the
|
|
81
|
+
* barrel and to the file that declares the name. An import of the own file is no edge.
|
|
82
|
+
*/
|
|
83
|
+
graph() {
|
|
84
|
+
this.made.all ??= new Graph(this.facts, this.core.graph(false), () => this.core.tangles(false));
|
|
85
|
+
return this.made.all;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The graph of the selected files only, from the imports that the rule sees: the graph
|
|
89
|
+
* whose tangles `mustBeAcyclic` reports. An import of a file that the rule does not select
|
|
90
|
+
* is no edge, so a cycle through such a file is no tangle.
|
|
91
|
+
*/
|
|
92
|
+
subjectGraph() {
|
|
93
|
+
this.made.subjects ??= new Graph(this.facts, this.core.graph(true), () => this.core.tangles(true));
|
|
94
|
+
return this.made.subjects;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A violation at the place of a fact, with its note. A message gives a `custom` detail,
|
|
98
|
+
* which a baseline counts by `key`, or by the message when there is no key.
|
|
99
|
+
*/
|
|
100
|
+
violationAt(fact, detail, key) {
|
|
101
|
+
return {
|
|
102
|
+
rule: this.ruleName,
|
|
103
|
+
unit: fact.unit,
|
|
104
|
+
file: fact.file,
|
|
105
|
+
line: fact.line,
|
|
106
|
+
column: fact.column,
|
|
107
|
+
...(fact.note !== undefined ? { note: fact.note } : {}),
|
|
108
|
+
detail: custom(detail, key),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** A violation at the place of a declaration. */
|
|
112
|
+
violationOfItem(declaration, detail, baselineKey) {
|
|
113
|
+
const { unit, file, line, column } = declaration;
|
|
114
|
+
return { rule: this.ruleName, unit, file, line, column, detail: custom(detail, baselineKey) };
|
|
115
|
+
}
|
|
116
|
+
/** A violation about a file as a whole, with no line. */
|
|
117
|
+
violationOfFile(file, detail, baselineKey) {
|
|
118
|
+
if (this.firstFiles === undefined) {
|
|
119
|
+
this.firstFiles = new Map();
|
|
120
|
+
for (const fact of this.facts.facts) {
|
|
121
|
+
const unit = key(fact.unit);
|
|
122
|
+
if (!this.firstFiles.has(unit)) {
|
|
123
|
+
this.firstFiles.set(unit, fact.file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { rule: this.ruleName, unit: file, file: this.firstFiles.get(key(file)) ?? "", line: 0, column: 0, detail: custom(detail, baselineKey) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What a check of items gets: the {@link Scope} of the rule, and the items that the filters
|
|
132
|
+
* of the rule select.
|
|
133
|
+
*/
|
|
134
|
+
export class ItemScope extends Scope {
|
|
135
|
+
/** The items that the rule selects, in order. */
|
|
136
|
+
items;
|
|
137
|
+
/**
|
|
138
|
+
* The imports that the rule sees inside the selected items. A front end that does not say
|
|
139
|
+
* which item holds an import gives none: the TypeScript front end is one.
|
|
140
|
+
*/
|
|
141
|
+
itemImports;
|
|
142
|
+
/** @internal */
|
|
143
|
+
constructor(ruleName, facts, selection, names, core) {
|
|
144
|
+
super(ruleName, facts, selection, names, core);
|
|
145
|
+
const declarations = facts.declarations ?? [];
|
|
146
|
+
this.items = (selection.items ?? []).map((position) => declarations[position]);
|
|
147
|
+
this.itemImports = this.imports.filter((_, index) => selection.references[index]?.inside === true);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** A graph of files: see {@link Scope.graph} and {@link Scope.subjectGraph}. */
|
|
151
|
+
export class Graph {
|
|
152
|
+
/** The file of each node. */
|
|
153
|
+
units;
|
|
154
|
+
numbers;
|
|
155
|
+
/** For each file, by its number: the edges, each with the number of the file that it leads to. */
|
|
156
|
+
edges;
|
|
157
|
+
/** Asks the core for the tangles, on the first use of them. */
|
|
158
|
+
coreTangles;
|
|
159
|
+
found;
|
|
160
|
+
/** @internal */
|
|
161
|
+
constructor(facts, graph, coreTangles) {
|
|
162
|
+
this.coreTangles = coreTangles;
|
|
163
|
+
this.units = graph.nodes?.map((unit) => facts.units[unit]) ?? facts.units;
|
|
164
|
+
this.numbers = new Map();
|
|
165
|
+
this.units.forEach((unit, number) => {
|
|
166
|
+
if (!this.numbers.has(key(unit))) {
|
|
167
|
+
this.numbers.set(key(unit), number);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// The core makes the edges, as it does for the built-in rules.
|
|
171
|
+
this.edges = graph.edges.map((edges) => edges.map(([to, position, path]) => {
|
|
172
|
+
const fact = facts.facts[position];
|
|
173
|
+
const reference = fact.what;
|
|
174
|
+
return { to: this.units[to], node: to, fact, target: [reference.target, ...reference.aliases][path] };
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
/** The edges of a file, in the order of their first imports. A path that is no file of the graph has none. */
|
|
178
|
+
edgesOf(file) {
|
|
179
|
+
const node = this.numbers.get(key(file));
|
|
180
|
+
return node === undefined ? [] : this.edges[node];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The tangles: each group of two or more files that all reach each other, once, in the
|
|
184
|
+
* order of their first files.
|
|
185
|
+
*/
|
|
186
|
+
tangles() {
|
|
187
|
+
this.found ??= this.coreTangles();
|
|
188
|
+
return this.found.map(({ members, cycle }) => ({
|
|
189
|
+
members: members.map((node) => this.units[node]),
|
|
190
|
+
cycle: cycle.map(([node, edge]) => this.step(node, this.edges[node][edge])),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
/** An edge as the step of a report. */
|
|
194
|
+
step(from, edge) {
|
|
195
|
+
const { fact, target } = edge;
|
|
196
|
+
const kind = fact.what.kind;
|
|
197
|
+
return { from: this.units[from], to: this.units[edge.node], file: fact.file, line: fact.line, column: fact.column, target, kind };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A shortest chain of imports from a file to a file that is an end, as the steps of a
|
|
201
|
+
* report. No steps: the file itself is an end. `undefined`: no chain leads to an end. The
|
|
202
|
+
* search is breadth-first with the edges of each file in their order.
|
|
203
|
+
*/
|
|
204
|
+
chain(from, isEnd) {
|
|
205
|
+
const start = this.numbers.get(key(from));
|
|
206
|
+
if (start === undefined) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
// For each file that the search found: the file and the edge that led to it.
|
|
210
|
+
const foundBy = new Map();
|
|
211
|
+
const queue = [start];
|
|
212
|
+
for (let next = 0; next < queue.length; next++) {
|
|
213
|
+
const node = queue[next];
|
|
214
|
+
if (isEnd(this.units[node])) {
|
|
215
|
+
const steps = [];
|
|
216
|
+
for (let current = node, found = foundBy.get(current); found !== undefined; current = found.from, found = foundBy.get(current)) {
|
|
217
|
+
steps.push(this.step(found.from, found.edge));
|
|
218
|
+
}
|
|
219
|
+
return steps.reverse();
|
|
220
|
+
}
|
|
221
|
+
for (const edge of this.edges[node] ?? []) {
|
|
222
|
+
if (edge.node !== start && !foundBy.has(edge.node)) {
|
|
223
|
+
foundBy.set(edge.node, { from: node, edge });
|
|
224
|
+
queue.push(edge.node);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function key(path) {
|
|
232
|
+
return path.join("\u0000");
|
|
233
|
+
}
|
|
234
|
+
function custom(detail, key) {
|
|
235
|
+
if (typeof detail !== "string") {
|
|
236
|
+
return detail;
|
|
237
|
+
}
|
|
238
|
+
return { type: "custom", message: detail, ...(key !== undefined ? { key } : {}) };
|
|
239
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filters of a rule: what it leaves out, why it exists and how much it weighs.
|
|
3
|
+
*
|
|
4
|
+
* A rule, layers and an onion architecture have the same filters. Each filter is written
|
|
5
|
+
* once, here: {@link addFilters} gives its method to all of them, so a new filter cannot be
|
|
6
|
+
* missing from one.
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule as CoreRule } from "./core/index.ts";
|
|
9
|
+
import type { Kind } from "./frontend/imports.ts";
|
|
10
|
+
import type { AnyOf } from "./rules.ts";
|
|
11
|
+
/** One filter, as a value that a set of rules keeps until its rules are made. @internal */
|
|
12
|
+
export interface Filter {
|
|
13
|
+
/**
|
|
14
|
+
* Whether the filter says which imports a rule sees. A rule that looks at no import, such
|
|
15
|
+
* as "every file is in a layer", does not get such a filter: there, an exemption that lets
|
|
16
|
+
* nothing pass would be an error.
|
|
17
|
+
*/
|
|
18
|
+
readonly aboutImports: boolean;
|
|
19
|
+
/** The rule of the core with the filter. */
|
|
20
|
+
readonly apply: (rule: CoreRule) => CoreRule;
|
|
21
|
+
}
|
|
22
|
+
/** What has filters: it gives itself again with one more filter. @internal */
|
|
23
|
+
export interface Filtered<Self> {
|
|
24
|
+
/** @internal */
|
|
25
|
+
filtered(filter: Filter): Self;
|
|
26
|
+
}
|
|
27
|
+
/** The filters of a rule, of layers and of an onion architecture. */
|
|
28
|
+
export interface Filters<Self> {
|
|
29
|
+
/**
|
|
30
|
+
* Says why this exists. The report of a violation prints the reason below the name of the
|
|
31
|
+
* rule, so whoever breaks the rule learns what it protects.
|
|
32
|
+
*/
|
|
33
|
+
because(reason: string): Self;
|
|
34
|
+
/**
|
|
35
|
+
* Makes this a warning: the violations are reported with `console.warn`, and the check
|
|
36
|
+
* passes. A rule that cannot fail as written is still an error, and so is a stale
|
|
37
|
+
* baseline. {@link ImportRule.evaluate} gives the violations of a warning as of any rule.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* rule("new code does not print").files(all()).mustNotContain("console").asWarning().check(app);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
asWarning(): Self;
|
|
44
|
+
/** Counts only these kinds of import. */
|
|
45
|
+
onlyKinds(...kinds: Kind[]): Self;
|
|
46
|
+
/** Counts all kinds of import except these. */
|
|
47
|
+
ignoringKinds(...kinds: Kind[]): Self;
|
|
48
|
+
/**
|
|
49
|
+
* Leaves out the imports that the analysis could not prove: the files that a computed
|
|
50
|
+
* specifier such as `` import(`./locales/${name}.ts`) `` can name.
|
|
51
|
+
*/
|
|
52
|
+
ignoringCandidates(): Self;
|
|
53
|
+
/** Leaves out `import type`, type-only names, `import("m")` in types, and `export type .. from`. */
|
|
54
|
+
ignoringTypeImports(): Self;
|
|
55
|
+
/**
|
|
56
|
+
* The imports from these files of these targets are not seen: one known import that is
|
|
57
|
+
* allowed. Files match as the files of a rule do; targets as in `mustNotImport`. For a
|
|
58
|
+
* cycle or a chain, the import is no edge.
|
|
59
|
+
*
|
|
60
|
+
* An exemption that lets no import pass is an error, so it goes when the code that it
|
|
61
|
+
* allows goes. Call this again for more exemptions.
|
|
62
|
+
*/
|
|
63
|
+
ignoringImport(from: AnyOf<string>, to: AnyOf): Self;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Gives a class the methods of {@link Filters}. The class says that it has them with
|
|
67
|
+
* `interface Name extends Filters<Name> {}`.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export declare function addFilters(target: {
|
|
72
|
+
readonly prototype: Filtered<unknown>;
|
|
73
|
+
}): void;
|
package/dist/filters.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { filePattern, targetPattern } from "./patterns.js";
|
|
2
|
+
const about = (apply) => ({ aboutImports: true, apply });
|
|
3
|
+
/** The methods of {@link Filters}, written once for every class that has them. */
|
|
4
|
+
const methods = {
|
|
5
|
+
because(reason) {
|
|
6
|
+
return this.filtered({ aboutImports: false, apply: (rule) => ({ ...rule, reason }) });
|
|
7
|
+
},
|
|
8
|
+
asWarning() {
|
|
9
|
+
return this.filtered({ aboutImports: false, apply: (rule) => ({ ...rule, severity: "warning" }) });
|
|
10
|
+
},
|
|
11
|
+
onlyKinds(...kinds) {
|
|
12
|
+
return this.filtered(about((rule) => ({ ...rule, kinds: { type: "only", kinds } })));
|
|
13
|
+
},
|
|
14
|
+
ignoringKinds(...kinds) {
|
|
15
|
+
return this.filtered(about((rule) => ({ ...rule, kinds: { type: "except", kinds } })));
|
|
16
|
+
},
|
|
17
|
+
ignoringCandidates() {
|
|
18
|
+
return this.filtered(about((rule) => ({ ...rule, candidates: "ignore" })));
|
|
19
|
+
},
|
|
20
|
+
ignoringTypeImports() {
|
|
21
|
+
return this.filtered(about((rule) => ({ ...rule, kinds: { type: "except", kinds: ["import-type", "export-type-from"] } })));
|
|
22
|
+
},
|
|
23
|
+
ignoringImport(from, to) {
|
|
24
|
+
const exemption = { from: from.patterns.map(filePattern), to: to.patterns.map(targetPattern) };
|
|
25
|
+
return this.filtered(about((rule) => ({ ...rule, exemptions: [...(rule.exemptions ?? []), exemption] })));
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Gives a class the methods of {@link Filters}. The class says that it has them with
|
|
30
|
+
* `interface Name extends Filters<Name> {}`.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function addFilters(target) {
|
|
35
|
+
Object.defineProperties(target.prototype, Object.getOwnPropertyDescriptors(methods));
|
|
36
|
+
for (const name of Object.keys(methods)) {
|
|
37
|
+
Object.defineProperty(target.prototype, name, { enumerable: false });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switches for the tests of this package. Each one makes a short way of the front end also
|
|
3
|
+
* take the long way, and a difference between the two is an error.
|
|
4
|
+
*
|
|
5
|
+
* - `aliases`: the first step of an alias is read from the exports of the module. With the
|
|
6
|
+
* switch, it is compared with the answer of the compiler for that name.
|
|
7
|
+
* - `walk`: a node whose text has no word that can name a module is not visited. With the
|
|
8
|
+
* switch, all nodes are also visited, and the two visits must find the same.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const checks: {
|
|
13
|
+
aliases: boolean;
|
|
14
|
+
walk: boolean;
|
|
15
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switches for the tests of this package. Each one makes a short way of the front end also
|
|
3
|
+
* take the long way, and a difference between the two is an error.
|
|
4
|
+
*
|
|
5
|
+
* - `aliases`: the first step of an alias is read from the exports of the module. With the
|
|
6
|
+
* switch, it is compared with the answer of the compiler for that name.
|
|
7
|
+
* - `walk`: a node whose text has no word that can name a module is not visited. With the
|
|
8
|
+
* switch, all nodes are also visited, and the two visits must find the same.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const checks = { aliases: false, walk: false };
|