@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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/dist/baseline.d.ts +26 -0
  4. package/dist/baseline.js +65 -0
  5. package/dist/core/index.d.ts +88 -0
  6. package/dist/core/index.js +105 -0
  7. package/dist/core/model.d.ts +340 -0
  8. package/dist/core/model.js +1 -0
  9. package/dist/core/results.d.ts +268 -0
  10. package/dist/core/results.js +1 -0
  11. package/dist/core/wasm.d.ts +7 -0
  12. package/dist/core/wasm.js +67 -0
  13. package/dist/custom.d.ts +166 -0
  14. package/dist/custom.js +239 -0
  15. package/dist/filters.d.ts +73 -0
  16. package/dist/filters.js +39 -0
  17. package/dist/frontend/checks.d.ts +15 -0
  18. package/dist/frontend/checks.js +12 -0
  19. package/dist/frontend/classic.d.ts +3 -0
  20. package/dist/frontend/classic.js +105 -0
  21. package/dist/frontend/compiler.d.ts +93 -0
  22. package/dist/frontend/compiler.js +116 -0
  23. package/dist/frontend/components.d.ts +29 -0
  24. package/dist/frontend/components.js +111 -0
  25. package/dist/frontend/declarations.d.ts +45 -0
  26. package/dist/frontend/declarations.js +158 -0
  27. package/dist/frontend/dependencies.d.ts +53 -0
  28. package/dist/frontend/dependencies.js +337 -0
  29. package/dist/frontend/imports.d.ts +149 -0
  30. package/dist/frontend/imports.js +765 -0
  31. package/dist/frontend/native.d.ts +3 -0
  32. package/dist/frontend/native.js +165 -0
  33. package/dist/frontend/paths.d.ts +10 -0
  34. package/dist/frontend/paths.js +18 -0
  35. package/dist/frontend/project.d.ts +118 -0
  36. package/dist/frontend/project.js +775 -0
  37. package/dist/group.d.ts +74 -0
  38. package/dist/group.js +106 -0
  39. package/dist/index.d.ts +35 -0
  40. package/dist/index.js +27 -0
  41. package/dist/items.d.ts +133 -0
  42. package/dist/items.js +179 -0
  43. package/dist/layers.d.ts +112 -0
  44. package/dist/layers.js +215 -0
  45. package/dist/onion.d.ts +78 -0
  46. package/dist/onion.js +96 -0
  47. package/dist/patterns.d.ts +14 -0
  48. package/dist/patterns.js +15 -0
  49. package/dist/rules.d.ts +408 -0
  50. package/dist/rules.js +665 -0
  51. package/package.json +52 -4
  52. package/wasm/refpact_core.wasm +0 -0
package/dist/rules.js ADDED
@@ -0,0 +1,665 @@
1
+ /** Rules for TypeScript projects, written as plain values in a test file. */
2
+ import { isBuiltin } from "node:module";
3
+ import { applyBaseline, diagram, evaluate, graph, renderErrors, renderStale, renderViolations, select, tangles } from "./core/index.js";
4
+ import { ItemScope, Scope } from "./custom.js";
5
+ import { addFilters } from "./filters.js";
6
+ import { filePattern, targetPattern } from "./patterns.js";
7
+ import { KINDS, MARKERS } from "./frontend/imports.js";
8
+ import { ItemSelection } from "./items.js";
9
+ import { exportSegment, globalSegment, nodeSegment, packageSegment } from "./frontend/paths.js";
10
+ import { DEPENDENCY_KINDS, NAMED_DEPENDENCY_KINDS, PACKAGE_ITEM_KIND } from "./frontend/dependencies.js";
11
+ import path from "node:path";
12
+ /** A package, a subpath of a package, or one export of it, as a target of a rule. */
13
+ export class PackageTarget {
14
+ /** @internal */
15
+ segments;
16
+ /** @internal */
17
+ constructor(segments) {
18
+ this.segments = segments;
19
+ }
20
+ /** One export of the package: `pkg("lodash").export("debounce")`. Use `default` for the default export. */
21
+ export(name) {
22
+ return new PackageTarget([...this.segments, exportSegment(name)]);
23
+ }
24
+ }
25
+ /**
26
+ * A package as a target: `pkg("lodash")`, `pkg("@scope/name")`, `pkg("lodash/fp")` for a
27
+ * subpath, `pkg("node:fs")` or `pkg("fs")` for a module of Node. The target covers all
28
+ * subpaths and all exports of the package. `*` matches a part of the name: `pkg("@aws-sdk/*")`.
29
+ */
30
+ export function pkg(specifier) {
31
+ if (specifier.startsWith("node:") || isBuiltin(specifier)) {
32
+ const [name, ...rest] = specifier.replace(/^node:/, "").split("/");
33
+ return new PackageTarget([nodeSegment(name), ...rest]);
34
+ }
35
+ const parts = specifier.split("/");
36
+ const nameLength = specifier.startsWith("@") ? 2 : 1;
37
+ return new PackageTarget([packageSegment(parts.slice(0, nameLength).join("/")), ...parts.slice(nameLength)]);
38
+ }
39
+ /**
40
+ * A global of the host as a target: `globalApi("fetch")`, `globalApi("process")`, or one member
41
+ * of it, `globalApi("process.env")`. The target of a global covers its members. A use of a
42
+ * global counts as an import of it, of the kind `global`: see {@link KINDS}. `*` matches a
43
+ * part of the name: `globalApi("*Storage")`.
44
+ *
45
+ * A global is a name that no file of the project declares and that the ECMAScript library
46
+ * does not declare: the globals of Node, of the browser and of other hosts, such as
47
+ * `process`, `fetch`, `window`, `document` and `localStorage`. `Math`, `Date`, `JSON` and
48
+ * `Promise` are part of the language and are no globals here.
49
+ */
50
+ export function globalApi(name) {
51
+ const [global, ...members] = name.split(".");
52
+ return new PackageTarget([globalSegment(global), ...members.slice(0, 1).map(exportSegment)]);
53
+ }
54
+ /** A selection of files. */
55
+ export class Files {
56
+ /** @internal */
57
+ include;
58
+ /** @internal */
59
+ excluded;
60
+ /** @internal */
61
+ constructor(include, excluded) {
62
+ this.include = include;
63
+ this.excluded = excluded;
64
+ }
65
+ }
66
+ /** A set of forbidden targets. */
67
+ export class Targets {
68
+ /** @internal */
69
+ anyOf;
70
+ /** @internal */
71
+ unverified;
72
+ /** @internal */
73
+ constructor(anyOf, unverified) {
74
+ this.anyOf = anyOf;
75
+ this.unverified = unverified;
76
+ }
77
+ }
78
+ /** A list of patterns that form a union. */
79
+ export class AnyOf {
80
+ /** @internal */
81
+ patterns;
82
+ /** @internal */
83
+ constructor(patterns) {
84
+ this.patterns = patterns;
85
+ }
86
+ /** Removes the files that match one of `patterns` from the selection. */
87
+ except(...patterns) {
88
+ return new Files(this.patterns, patterns);
89
+ }
90
+ /** Adds targets that are not checked for existence. See {@link unverified}. */
91
+ andUnverified(...patterns) {
92
+ return new Targets(this.patterns, patterns);
93
+ }
94
+ }
95
+ export function anyOf(...patterns) {
96
+ return new AnyOf(patterns);
97
+ }
98
+ /** All files of the project. */
99
+ export function all() {
100
+ return new Files(undefined, undefined);
101
+ }
102
+ /** All files of the project except those that match one of the patterns. */
103
+ export function allExcept(...patterns) {
104
+ return new Files(undefined, patterns);
105
+ }
106
+ /**
107
+ * Targets that are not checked for existence. Use this only for files of the project that
108
+ * the compiler does not load, such as generated files that are not there yet.
109
+ */
110
+ export function unverified(...patterns) {
111
+ return new Targets([], patterns);
112
+ }
113
+ /** Starts a rule. The name is printed with every violation. */
114
+ export function rule(name) {
115
+ return new NamedRule(name);
116
+ }
117
+ /** A rule with a name. Say which files it applies to. */
118
+ export class NamedRule {
119
+ name;
120
+ /** @internal */
121
+ constructor(name) {
122
+ this.name = name;
123
+ }
124
+ /** The files that the rule constrains: `anyOf(..)`, `all()`, `allExcept(..)` or `anyOf(..).except(..)`. */
125
+ files(files) {
126
+ if (files instanceof AnyOf) {
127
+ for (const pattern of files.patterns) {
128
+ if (typeof pattern !== "string") {
129
+ throw new TypeError(`refpact: rule \`${this.name}\`: files(..) takes file patterns, not packages`);
130
+ }
131
+ }
132
+ return new ScopedRule(this.name, new Files(files.patterns, undefined));
133
+ }
134
+ return new ScopedRule(this.name, files);
135
+ }
136
+ /**
137
+ * The items that these files declare at their top level: functions, classes, interfaces,
138
+ * types, enums, variables and namespaces. Narrow them with the filters of
139
+ * {@link ItemSelection}, then say what they must meet, for example with
140
+ * {@link ItemSelection.mustResideIn}.
141
+ */
142
+ items(files) {
143
+ const selection = files instanceof AnyOf ? new Files(files.patterns, undefined) : files;
144
+ return new ItemSelection(this.name, selection, {
145
+ rule: (made, finder) => new ImportRule(made, finder),
146
+ custom: customCheck,
147
+ subjects,
148
+ file: filePattern,
149
+ target: targetPattern,
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * A rule with a name and files. Say what the files must or must not do, for example with
155
+ * `mustNotImport`, `mustOnlyImport` or `mustBeAcyclic`.
156
+ */
157
+ export class ScopedRule {
158
+ name;
159
+ selection;
160
+ /** @internal */
161
+ constructor(name, selection) {
162
+ this.name = name;
163
+ this.selection = selection;
164
+ }
165
+ /**
166
+ * The files must not import any of the targets, in any form: `import`, `import type`,
167
+ * `import()`, `require`, or `export .. from`. An import through a barrel file counts as
168
+ * an import of the file that declares the name.
169
+ */
170
+ mustNotImport(targets) {
171
+ return this.rule({ type: "must-not-use", targets: this.targets(targets) });
172
+ }
173
+ /**
174
+ * Every import of the files must lead to one of the targets. An import is allowed when the
175
+ * module in the code is allowed, or when the file that declares the name is.
176
+ *
177
+ * The list must name everything the files import, also packages and modules of Node:
178
+ * `pkg("*")` is every package, and `pkg("node:*")` is every module of Node.
179
+ */
180
+ mustOnlyImport(targets) {
181
+ return this.rule({ type: "must-only-use", targets: this.targets(targets) });
182
+ }
183
+ /**
184
+ * The files must not form a cycle: no file may reach itself through the imports between
185
+ * them. Each tangle of files is reported once, with one of its cycles.
186
+ */
187
+ mustBeAcyclic() {
188
+ return this.rule({ type: "must-be-acyclic" });
189
+ }
190
+ /**
191
+ * Like {@link ScopedRule.mustBeAcyclic}, between slices. The slice of a file is the
192
+ * shortest start of its path that matches one of the patterns: with `src/features/*`, the
193
+ * file `src/features/cart/view/list.ts` is in the slice `src/features/cart`. Imports inside
194
+ * a slice do not count, and a file without a slice is left out.
195
+ */
196
+ mustBeAcyclicBetween(slices) {
197
+ return this.rule({ type: "must-be-acyclic", slices: slices.patterns.map(filePattern) });
198
+ }
199
+ /**
200
+ * No file of one slice may import a file of another slice. Slices are as in
201
+ * {@link ScopedRule.mustBeAcyclicBetween}: with `src/features/*`, no feature may import
202
+ * another feature.
203
+ *
204
+ * A file that the rule does not select is in no slice. Leave a shared part out with
205
+ * `allExcept("src/features/shared/**")`.
206
+ */
207
+ mustBeIndependentBetween(slices) {
208
+ return this.rule({ type: "must-be-independent", slices: slices.patterns.map(filePattern) });
209
+ }
210
+ /**
211
+ * The files must not contain any of the markers: `console`, a use of the global
212
+ * `console`, `debugger`, a `debugger` statement, and `unresolved-import`, an import of a
213
+ * relative path, a `paths` alias or a `#name` of `imports` that leads to no file. A local
214
+ * variable named `console` is no marker. An import of a package is never an `unresolved-import`:
215
+ * refpact does not check that a package is installed.
216
+ *
217
+ * ```ts
218
+ * rule("every import resolves").files(all()).mustNotContain("unresolved-import").check(app);
219
+ * ```
220
+ */
221
+ mustNotContain(...markers) {
222
+ return this.rule({ type: "must-not-contain", markers });
223
+ }
224
+ /**
225
+ * Each file must have at most this number of lines. The blank lines and the comments
226
+ * count.
227
+ *
228
+ * ```ts
229
+ * rule("no file is longer than 400 lines").files(anyOf("src/**")).mustNotExceedLines(400).check(app);
230
+ * ```
231
+ */
232
+ mustNotExceedLines(lines) {
233
+ return this.rule({ type: "must-not-exceed-lines", lines });
234
+ }
235
+ /**
236
+ * Each file must be imported by another file. Leave the entry points out of the files:
237
+ * `allExcept("src/main.ts")`.
238
+ */
239
+ mustBeImported() {
240
+ return this.rule({ type: "must-be-used" });
241
+ }
242
+ /**
243
+ * No chain of imports may lead from the files to one of the targets: not the files
244
+ * themselves, and not a file that they import, at any distance. The report shows the
245
+ * shortest chain.
246
+ */
247
+ mustNotReach(targets) {
248
+ return this.rule({ type: "must-not-reach", targets: this.targets(targets) });
249
+ }
250
+ /** A chain of imports must lead from one of the root files to each file. Give the entry points as roots. */
251
+ mustBeReachableFrom(roots) {
252
+ return this.rule({ type: "must-be-reachable-from", roots: roots.patterns.map(filePattern) });
253
+ }
254
+ /**
255
+ * Only the importers, and the files of the rule themselves, may import the files of the
256
+ * rule. Every other file that imports one of them breaks the rule, also through a barrel
257
+ * file. Importers match files as the files of a rule do.
258
+ */
259
+ mustOnlyBeImportedBy(importers) {
260
+ return this.rule({ type: "must-only-be-used-by", users: importers.patterns.map(filePattern) });
261
+ }
262
+ /**
263
+ * Each file must be one that a place matches. The layers use it for `everyFileInALayer`.
264
+ *
265
+ * @internal
266
+ */
267
+ mustBeIn(places) {
268
+ return this.rule({ type: "must-be-in", places: places.map(filePattern) });
269
+ }
270
+ /**
271
+ * Every package that the files import must be in the `package.json` that the file belongs
272
+ * to: the nearest one above it. Any of `dependencies`, `devDependencies`,
273
+ * `peerDependencies` and `optionalDependencies` counts. A type import of `name` is also
274
+ * declared by `@types/name`. A package may import itself by its own name.
275
+ *
276
+ * The project must be loaded with `readDependencies`: the rule reads the references that
277
+ * the `package.json` files give, as a check of your own can.
278
+ */
279
+ mustOnlyImportDeclaredPackages() {
280
+ return this.custom(ANY_PACKAGE, PACKAGE_NAMES, (scope) => undeclared(scope, "declared"));
281
+ }
282
+ /**
283
+ * The files must not import a package that their `package.json` has only in
284
+ * `devDependencies`. Select the files that are shipped: `allExcept("**\/*.test.ts")`.
285
+ * The project must be loaded with `readDependencies`.
286
+ */
287
+ mustNotImportDevDependencies() {
288
+ return this.custom(ANY_PACKAGE, PACKAGE_NAMES, (scope) => undeclared(scope, "not-development"));
289
+ }
290
+ /**
291
+ * The files must pass a check of your own: a function that gets the {@link Scope} of the
292
+ * rule and returns the violations. The rule selects the files, applies its filters and its
293
+ * exemptions, and reports and baselines the violations, as for the rules above.
294
+ *
295
+ * A function cannot match a pattern, so the check names the lists of patterns that it
296
+ * needs, and the rule matches them: see {@link CheckPatterns}. A pattern that matches
297
+ * nothing is an error, as it is for `mustNotImport`.
298
+ *
299
+ * ```ts
300
+ * rule("a file imports at most ten files")
301
+ * .files(anyOf("src/**"))
302
+ * .mustSatisfy((scope) =>
303
+ * scope.subjects
304
+ * .filter((file) => scope.imports.filter((seen) => seen.file === file).length > 10)
305
+ * .map((file) => scope.violationOfFile(file, "imports more than ten files")),
306
+ * )
307
+ * .check(app);
308
+ * ```
309
+ */
310
+ mustSatisfy(check, patterns) {
311
+ const { constraint, names } = customCheck(patterns);
312
+ return this.custom(constraint, names, (scope) => check(scope));
313
+ }
314
+ targets(targets) {
315
+ return coreTargets(targets);
316
+ }
317
+ rule(constraint) {
318
+ return new ImportRule({ name: this.name, subjects: subjects(this.selection), constraint });
319
+ }
320
+ custom(constraint, names, find) {
321
+ return new ImportRule({ name: this.name, subjects: subjects(this.selection), constraint }, { names, find });
322
+ }
323
+ }
324
+ function coreTargets(targets) {
325
+ const set = targets instanceof AnyOf ? new Targets(targets.patterns, []) : targets;
326
+ return { anyOf: set.anyOf.map(targetPattern), unverified: set.unverified.map(targetPattern) };
327
+ }
328
+ /** The custom check that the core gets for these lists, and the names of the lists in their order. */
329
+ function customCheck(patterns) {
330
+ const targets = Object.entries(patterns?.targets ?? {});
331
+ const files = Object.entries(patterns?.files ?? {});
332
+ return {
333
+ constraint: {
334
+ type: "custom",
335
+ targets: targets.map(([, list]) => coreTargets(list)),
336
+ units: files.map(([, list]) => list.patterns.map(filePattern)),
337
+ },
338
+ names: { targets: targets.map(([name]) => name), files: files.map(([name]) => name) },
339
+ };
340
+ }
341
+ /** The subjects of a rule about these files. */
342
+ function subjects(files) {
343
+ return {
344
+ include: files.include === undefined ? { type: "all" } : { type: "any-of", patterns: files.include.map(filePattern) },
345
+ ...(files.excluded !== undefined ? { except: files.excluded.map(filePattern) } : {}),
346
+ };
347
+ }
348
+ /** The rules about `package.json` look at the imports of packages: the hit of an import is its package. */
349
+ const ANY_PACKAGE = { type: "custom", targets: [{ anyOf: [], unverified: [[packageSegment("*")]] }] };
350
+ const PACKAGE_NAMES = { targets: ["package"], files: [] };
351
+ /** The imports of packages by the selected files, as violations of a rule about uses. */
352
+ function packageImports(scope) {
353
+ const found = [];
354
+ for (const seen of scope.imports) {
355
+ const hit = seen.hits["package"];
356
+ const { reference } = seen;
357
+ // What a package.json names is no import.
358
+ if (!seen.subject || hit === undefined || DEPENDENCY_KINDS.includes(reference.kind)) {
359
+ continue;
360
+ }
361
+ const written = hit.length !== reference.target.length || hit.some((segment, index) => segment !== reference.target[index]);
362
+ found.push(scope.violationAt(seen.fact, {
363
+ type: "uses",
364
+ target: hit,
365
+ ...(written ? { written: reference.target } : {}),
366
+ kind: reference.kind,
367
+ certainty: reference.certainty,
368
+ }));
369
+ }
370
+ return found;
371
+ }
372
+ /**
373
+ * From the imports of packages by the selected files, those that break a rule about
374
+ * `package.json`. The rule reads what a check of the user can read: the references and the
375
+ * items that the `package.json` files give to a project that is loaded with
376
+ * `readDependencies`. Throws an {@link ArchitectureError} when no `package.json` can make
377
+ * the rule fail.
378
+ */
379
+ function undeclared(scope, check) {
380
+ const rule = scope.ruleName;
381
+ const malformed = (problem) => new ArchitectureError({ type: "malformed", rule, message: `rule \`${rule}\` cannot be checked:\n ${problem}` });
382
+ const { facts } = scope;
383
+ if (!facts.kinds.includes("dependency")) {
384
+ throw malformed("the project is loaded without `readDependencies`, so it has no package.json");
385
+ }
386
+ const manifests = new Map();
387
+ const manifestAt = (unit) => {
388
+ const file = unit.join("/");
389
+ const known = manifests.get(file);
390
+ if (known !== undefined) {
391
+ return known;
392
+ }
393
+ const made = { directory: unit.slice(0, -1).join("/"), name: undefined, production: new Set(), development: new Set() };
394
+ manifests.set(file, made);
395
+ return made;
396
+ };
397
+ for (const declaration of facts.declarations ?? []) {
398
+ if (declaration.kind === PACKAGE_ITEM_KIND) {
399
+ manifestAt(declaration.unit).name = declaration.name;
400
+ }
401
+ }
402
+ for (const fact of facts.facts) {
403
+ if (fact.what.type === "reference" && NAMED_DEPENDENCY_KINDS.includes(fact.what.kind)) {
404
+ const name = fact.what.target[0].slice(packageSegment("").length);
405
+ const manifest = manifestAt(fact.unit);
406
+ (fact.what.kind === "dev-dependency" ? manifest.development : manifest.production).add(name);
407
+ }
408
+ }
409
+ // The package.json of a file is the nearest one above it.
410
+ const nearest = new Map();
411
+ const manifestOf = (file) => {
412
+ if (!nearest.has(file)) {
413
+ let found;
414
+ for (const manifest of manifests.values()) {
415
+ const inside = !path.posix.relative(manifest.directory, file).startsWith("..");
416
+ if (inside && (found === undefined || path.posix.resolve(manifest.directory).length > path.posix.resolve(found.directory).length)) {
417
+ found = manifest;
418
+ }
419
+ }
420
+ nearest.set(file, found);
421
+ }
422
+ const manifest = nearest.get(file);
423
+ if (manifest === undefined) {
424
+ throw malformed(`no package.json is above ${file}`);
425
+ }
426
+ return manifest;
427
+ };
428
+ if (check === "not-development" && ![...manifests.values()].some((manifest) => manifest.development.size > 0)) {
429
+ throw malformed("no package.json of the project has devDependencies");
430
+ }
431
+ return packageImports(scope).filter((violation) => {
432
+ const detail = violation.detail;
433
+ if (detail.type !== "uses") {
434
+ return false;
435
+ }
436
+ const manifest = manifestOf(violation.file);
437
+ const name = detail.target[0].slice(packageSegment("").length);
438
+ const declares = (entry) => manifest.production.has(entry) || manifest.development.has(entry);
439
+ const types = typesPackage(name);
440
+ // A type import of `name` can come from `@types/name`.
441
+ const entry = declares(name) ? name : TYPE_KINDS.includes(detail.kind) && declares(types) ? types : undefined;
442
+ if (check === "declared") {
443
+ return entry === undefined && name !== manifest.name;
444
+ }
445
+ return entry !== undefined && !manifest.production.has(entry);
446
+ });
447
+ }
448
+ const TYPE_KINDS = ["import-type", "export-type-from"];
449
+ /** `@types/name` for `name`, and `@types/scope__name` for `@scope/name`. */
450
+ function typesPackage(name) {
451
+ return `@types/${name.startsWith("@") ? name.slice(1).replace("/", "__") : name}`;
452
+ }
453
+ /** An answer of the core without errors. Throws the errors of the rule. */
454
+ function answered(rule, answer) {
455
+ if (!answer.ok) {
456
+ const { errors } = answer;
457
+ throw new ArchitectureError({ type: "invalid", rule, errors, message: renderErrors(rule, errors) });
458
+ }
459
+ return answer;
460
+ }
461
+ /** The error that `check` throws. Its message is the report. */
462
+ export class ArchitectureError extends Error {
463
+ /** Why the rule did not pass. */
464
+ failure;
465
+ constructor(failure) {
466
+ super(failure.message);
467
+ this.name = "ArchitectureError";
468
+ this.failure = failure;
469
+ }
470
+ }
471
+ export class ImportRule {
472
+ rule;
473
+ /** The check of a rule with a custom check. */
474
+ finder;
475
+ /** @internal */
476
+ constructor(rule, finder) {
477
+ this.rule = rule;
478
+ this.finder = finder;
479
+ }
480
+ /** The name of the rule. */
481
+ get name() {
482
+ return this.rule.name;
483
+ }
484
+ /** @internal The rule with one more filter. */
485
+ filtered(filter) {
486
+ return new ImportRule(filter.apply(this.rule), this.finder);
487
+ }
488
+ /**
489
+ * Names the properties of imports that a check of your own reads, such as `license`. A
490
+ * name that the load does not produce is then an error, as a kind that it does not produce
491
+ * is: a check that reads `licence` for `license`, or the properties of a load without
492
+ * `readDependencies`, cannot pass because it finds nothing.
493
+ */
494
+ readingProperties(...names) {
495
+ return new ImportRule({ ...this.rule, properties: [...(this.rule.properties ?? []), ...names] }, this.finder);
496
+ }
497
+ /**
498
+ * Evaluates the rule and returns the violations, which may be none, or the errors of the
499
+ * rule. A rule about `package.json` throws an {@link ArchitectureError} when no
500
+ * `package.json` can make it fail.
501
+ */
502
+ evaluate(project) {
503
+ if (this.finder === undefined) {
504
+ return evaluate(this.rule, project.facts);
505
+ }
506
+ // The core applies the subjects, the filters and the exemptions; the check does the rest.
507
+ const { rule, finder } = this;
508
+ const selection = select(rule, project.facts);
509
+ if (!selection.ok) {
510
+ return selection;
511
+ }
512
+ const scope = new (rule.constraint.type === "items" ? ItemScope : Scope)(rule.name, project.facts, selection, finder.names, {
513
+ graph: (subjects) => answered(rule.name, graph(rule, project.facts, subjects)),
514
+ tangles: (subjects) => answered(rule.name, tangles(rule, project.facts, subjects)).tangles,
515
+ });
516
+ return { ok: true, violations: finder.find(scope, project) };
517
+ }
518
+ /**
519
+ * Evaluates the rule and returns the failure, or `undefined` when the rule passes. The
520
+ * violations of a warning are reported with `console.warn` and are no failure: see
521
+ * {@link ImportRule.asWarning}.
522
+ */
523
+ verify(project) {
524
+ const found = this.found(project);
525
+ if (!Array.isArray(found)) {
526
+ return found;
527
+ }
528
+ return this.violated(found, project);
529
+ }
530
+ /** The violations, or the reason why the rule cannot be evaluated. */
531
+ found(project) {
532
+ const rule = this.rule.name;
533
+ let result;
534
+ try {
535
+ result = this.evaluate(project);
536
+ }
537
+ catch (error) {
538
+ if (error instanceof ArchitectureError) {
539
+ return error.failure;
540
+ }
541
+ throw error;
542
+ }
543
+ if (!result.ok) {
544
+ return { type: "invalid", rule, errors: result.errors, message: renderErrors(rule, result.errors) };
545
+ }
546
+ return result.violations;
547
+ }
548
+ /**
549
+ * A diagram of what the rule sees, in Mermaid (the default) or DOT. The nodes are the files
550
+ * of the rule, or its slices when the rule is about slices, such as
551
+ * {@link ScopedRule.mustBeAcyclicBetween}. An edge is an import between two nodes that the
552
+ * filters and the exemptions of the rule let pass. The files and imports of a cycle are
553
+ * red. The constraint of the rule is not evaluated, so a rule of any kind draws its files.
554
+ * Throws an {@link ArchitectureError} when the rule cannot be evaluated.
555
+ *
556
+ * A Markdown viewer that knows Mermaid, such as GitHub's, draws the text in a block
557
+ * ` ```mermaid `.
558
+ *
559
+ * ```ts
560
+ * const text = rule("the features").files(all()).mustBeAcyclicBetween(anyOf("src/features/*")).diagram(app);
561
+ * writeFileSync("docs/features.md", `\`\`\`mermaid\n${text}\`\`\`\n`);
562
+ * ```
563
+ */
564
+ diagram(project, format = "mermaid") {
565
+ const { constraint } = this.rule;
566
+ const slices = constraint.type === "must-be-acyclic" || constraint.type === "must-be-independent" ? constraint.slices : undefined;
567
+ return this.draw(project, slices === undefined ? undefined : { slices }, format);
568
+ }
569
+ /** @internal The diagram of the rule with these nodes. */
570
+ draw(project, nodes, format) {
571
+ const { name } = this.rule;
572
+ const result = diagram(this.rule, project.facts, nodes, format);
573
+ if (!result.ok) {
574
+ throw new ArchitectureError({ type: "invalid", rule: name, errors: result.errors, message: renderErrors(name, result.errors) });
575
+ }
576
+ return result.text;
577
+ }
578
+ /** Throws an {@link ArchitectureError} when the rule is violated or cannot be evaluated. */
579
+ check(project) {
580
+ const failure = this.verify(project);
581
+ if (failure !== undefined) {
582
+ throw new ArchitectureError(failure);
583
+ }
584
+ }
585
+ /**
586
+ * Like {@link ImportRule.verify}, but violations that the baseline file accepts do not fail.
587
+ *
588
+ * A baseline is used only through this method and {@link ImportRule.checkWithBaseline}. It
589
+ * can only shrink: an entry that accepts more violations than the code has is a failure.
590
+ * When the environment variable `REFPACT_BASELINE` is `update`, this writes the violations
591
+ * of the rule to the file and passes.
592
+ */
593
+ verifyWithBaseline(project, baseline) {
594
+ if (process.env["REFPACT_BASELINE"] === "update") {
595
+ return this.writeBaseline(project, baseline);
596
+ }
597
+ const found = this.violationsForBaseline(project);
598
+ if (!Array.isArray(found)) {
599
+ return found;
600
+ }
601
+ const entries = baseline.read();
602
+ if (!Array.isArray(entries)) {
603
+ return entries;
604
+ }
605
+ const rule = this.rule.name;
606
+ const applied = applyBaseline(rule, found, entries, project.facts);
607
+ const violated = this.violated(applied.remaining, project);
608
+ if (violated !== undefined) {
609
+ return violated;
610
+ }
611
+ if (applied.stale.length > 0) {
612
+ const message = renderStale(rule, baseline.file, applied.stale);
613
+ return { type: "stale-baseline", rule, file: baseline.file, entries: applied.stale, message };
614
+ }
615
+ return undefined;
616
+ }
617
+ /**
618
+ * Throws an {@link ArchitectureError} when the rule has violations that the baseline file
619
+ * does not accept, when the baseline has stale entries, or when the rule cannot be evaluated.
620
+ */
621
+ checkWithBaseline(project, baseline) {
622
+ const failure = this.verifyWithBaseline(project, baseline);
623
+ if (failure !== undefined) {
624
+ throw new ArchitectureError(failure);
625
+ }
626
+ }
627
+ /**
628
+ * Writes the current violations of this rule to the baseline file. Entries of other rules
629
+ * in the file are kept. Returns the failure, or `undefined` when the file is written.
630
+ */
631
+ writeBaseline(project, baseline) {
632
+ const found = this.violationsForBaseline(project);
633
+ if (!Array.isArray(found)) {
634
+ return found;
635
+ }
636
+ return baseline.write(this.rule.name, found, project.facts);
637
+ }
638
+ /** The failure of the violations. A warning reports them and fails not. */
639
+ violated(violations, project) {
640
+ if (violations.length === 0) {
641
+ return undefined;
642
+ }
643
+ const { name: rule, reason, severity } = this.rule;
644
+ if (severity === "warning") {
645
+ console.warn(renderViolations(rule, violations, project.facts, reason, severity));
646
+ return undefined;
647
+ }
648
+ const message = renderViolations(rule, violations, project.facts, reason);
649
+ return { type: "violated", rule, ...(reason !== undefined ? { reason } : {}), violations, message };
650
+ }
651
+ violationsForBaseline(project) {
652
+ const rule = this.rule.name;
653
+ if (/[\t\r\n]/.test(rule)) {
654
+ const message = `refpact: the name of rule ${JSON.stringify(rule)} has a tab or a line break, so it cannot be in a baseline`;
655
+ return { type: "baseline", message };
656
+ }
657
+ return this.found(project);
658
+ }
659
+ /** The rule as the core sees it. */
660
+ toCore() {
661
+ return this.rule;
662
+ }
663
+ }
664
+ addFilters(ImportRule);
665
+ export { KINDS, MARKERS };