@shivaedev/quality 0.5.1 → 0.7.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/CHANGELOG.md +26 -0
- package/README.md +36 -39
- package/biome/declarations.json +55 -0
- package/biome/preset.json +29 -13
- package/dist/baseline/legacy.d.ts +0 -1
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/legacy.js +0 -1
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/cli/args.d.ts +1 -6
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +7 -14
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/fix.d.ts +8 -2
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +55 -9
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli.js +1 -4
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +0 -1
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +0 -1
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +0 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +0 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -3
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js +4 -27
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +1 -1
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js +2 -2
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +0 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/git/history.js +1 -6
- package/dist/git/history.js.map +1 -1
- package/dist/imports/departures.d.ts +9 -0
- package/dist/imports/departures.d.ts.map +1 -0
- package/dist/imports/departures.js +49 -0
- package/dist/imports/departures.js.map +1 -0
- package/dist/imports/specifiers.d.ts +9 -0
- package/dist/imports/specifiers.d.ts.map +1 -0
- package/dist/imports/specifiers.js +71 -0
- package/dist/imports/specifiers.js.map +1 -0
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +11 -0
- package/dist/report/render.js.map +1 -1
- package/dist/rule.d.ts +2 -1
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/biome.js +1 -1
- package/dist/rules/biome.js.map +1 -1
- package/dist/rules/built-in.d.ts +1 -1
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +2 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/imports/aliased.d.ts +2 -0
- package/dist/rules/imports/aliased.d.ts.map +1 -0
- package/dist/rules/imports/aliased.js +23 -0
- package/dist/rules/imports/aliased.js.map +1 -0
- package/package.json +3 -2
- package/src/baseline/legacy.ts +0 -2
- package/src/cli/args.ts +8 -16
- package/src/cli/fix.ts +72 -10
- package/src/cli.ts +1 -4
- package/src/config/decode.ts +0 -1
- package/src/config/load.ts +0 -2
- package/src/config/resolve.ts +1 -3
- package/src/config.ts +0 -3
- package/src/git/base.ts +4 -31
- package/src/git/command.ts +2 -2
- package/src/git/history.ts +1 -8
- package/src/imports/departures.ts +72 -0
- package/src/imports/specifiers.ts +94 -0
- package/src/report/render.ts +12 -0
- package/src/rule.ts +3 -2
- package/src/rules/biome.ts +1 -1
- package/src/rules/built-in.ts +2 -0
- package/src/rules/imports/aliased.ts +26 -0
- package/dist/baseline/guard.d.ts +0 -23
- package/dist/baseline/guard.d.ts.map +0 -1
- package/dist/baseline/guard.js +0 -32
- package/dist/baseline/guard.js.map +0 -1
- package/dist/cli/check.d.ts +0 -5
- package/dist/cli/check.d.ts.map +0 -1
- package/dist/cli/check.js +0 -39
- package/dist/cli/check.js.map +0 -1
- package/dist/report/guard.d.ts +0 -9
- package/dist/report/guard.d.ts.map +0 -1
- package/dist/report/guard.js +0 -34
- package/dist/report/guard.js.map +0 -1
- package/src/baseline/guard.ts +0 -48
- package/src/cli/check.ts +0 -51
- package/src/report/guard.ts +0 -46
package/src/git/base.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect } from "effect";
|
|
2
2
|
import { SetupFailure } from "../failure.ts";
|
|
3
3
|
import { type Git, git } from "./command.ts";
|
|
4
4
|
|
|
@@ -36,50 +36,23 @@ const isShallow = (root: string): Effect.Effect<boolean, SetupFailure, Git> =>
|
|
|
36
36
|
const mergeBase = (root: string, ref: string): Effect.Effect<string | undefined, SetupFailure, Git> =>
|
|
37
37
|
Effect.map(git(root, ["merge-base", "HEAD", ref]), (result) => (result.code === 0 ? result.stdout.trim() : undefined));
|
|
38
38
|
|
|
39
|
-
const remoteOf = (root: string, ref: string): Effect.Effect<readonly string[], SetupFailure, Git> =>
|
|
40
|
-
Effect.gen(function* () {
|
|
41
|
-
const name = (yield* git(root, ["rev-parse", "--symbolic-full-name", ref])).stdout.trim();
|
|
42
|
-
const remotes = (yield* git(root, ["remote"])).stdout.split("\n");
|
|
43
|
-
return remotes.filter((remote) => remote !== "" && name.startsWith(`refs/remotes/${remote}/`)).slice(0, 1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const unshallow = (root: string, ref: string): Effect.Effect<void, SetupFailure, Git> =>
|
|
47
|
-
Effect.gen(function* () {
|
|
48
|
-
const remote = yield* remoteOf(root, ref);
|
|
49
|
-
yield* Console.error(`quality: the clone is shallow; fetching its full history to find the merge base of HEAD and ${ref}.`);
|
|
50
|
-
yield* git(root, ["fetch", "--quiet", "--unshallow", ...remote], { GIT_TERMINAL_PROMPT: "0" });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
39
|
const missingMergeBase = (root: string, ref: string): Effect.Effect<never, SetupFailure, Git> =>
|
|
54
40
|
Effect.flatMap(isShallow(root), (shallow) =>
|
|
55
41
|
fail(
|
|
56
42
|
shallow
|
|
57
|
-
? `the clone is shallow and does not reach the merge base of HEAD and ${ref}. Fetch the history first: \`git fetch --unshallow
|
|
43
|
+
? `the clone is shallow and does not reach the merge base of HEAD and ${ref}. Fetch the history first: \`git fetch --unshallow\`.`
|
|
58
44
|
: `HEAD and ${ref} share no history.`,
|
|
59
45
|
),
|
|
60
46
|
);
|
|
61
47
|
|
|
62
48
|
const reachMergeBase = (root: string, ref: string): Effect.Effect<string, SetupFailure, Git> =>
|
|
63
|
-
Effect.
|
|
64
|
-
const found = yield* mergeBase(root, ref);
|
|
65
|
-
if (found !== undefined) {
|
|
66
|
-
return found;
|
|
67
|
-
}
|
|
68
|
-
if (yield* isShallow(root)) {
|
|
69
|
-
yield* unshallow(root, ref);
|
|
70
|
-
const deepened = yield* mergeBase(root, ref);
|
|
71
|
-
if (deepened !== undefined) {
|
|
72
|
-
return deepened;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return yield* missingMergeBase(root, ref);
|
|
76
|
-
});
|
|
49
|
+
Effect.flatMap(mergeBase(root, ref), (found) => (found === undefined ? missingMergeBase(root, ref) : Effect.succeed(found)));
|
|
77
50
|
|
|
78
51
|
export const resolveBase = (root: string, against: string | undefined): Effect.Effect<Base, SetupFailure, Git> =>
|
|
79
52
|
Effect.gen(function* () {
|
|
80
53
|
const inside = yield* git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
81
54
|
if (inside.code !== 0 || inside.stdout.trim() !== "true") {
|
|
82
|
-
return yield* fail(`${root} is not in a git work tree;
|
|
55
|
+
return yield* fail(`${root} is not in a git work tree; moves are followed through git history.`);
|
|
83
56
|
}
|
|
84
57
|
const ref = against ?? (yield* defaultBranch(root));
|
|
85
58
|
if (!(yield* isCommit(root, ref))) {
|
package/src/git/command.ts
CHANGED
|
@@ -10,11 +10,11 @@ export interface GitResult {
|
|
|
10
10
|
|
|
11
11
|
export type Git = ChildProcessSpawner.ChildProcessSpawner;
|
|
12
12
|
|
|
13
|
-
export const git = (cwd: string, args: readonly string[]
|
|
13
|
+
export const git = (cwd: string, args: readonly string[]): Effect.Effect<GitResult, SetupFailure, Git> =>
|
|
14
14
|
Effect.scoped(
|
|
15
15
|
Effect.gen(function* () {
|
|
16
16
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
17
|
-
const child = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd,
|
|
17
|
+
const child = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd, stdin: "ignore" }));
|
|
18
18
|
const [stdout, stderr, code] = yield* Effect.all(
|
|
19
19
|
[Stream.mkString(Stream.decodeText(child.stdout)), Stream.mkString(Stream.decodeText(child.stderr)), child.exitCode],
|
|
20
20
|
{ concurrency: "unbounded" },
|
package/src/git/history.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import type { SetupFailure } from "../failure.ts";
|
|
3
|
-
import { type Git,
|
|
3
|
+
import { type Git, gitOrFail } from "./command.ts";
|
|
4
4
|
|
|
5
5
|
export interface Changes {
|
|
6
6
|
readonly files: ReadonlySet<string>;
|
|
7
7
|
readonly moves: ReadonlyMap<string, string>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const readAt = (root: string, commit: string, path: string): Effect.Effect<string | undefined, SetupFailure, Git> =>
|
|
11
|
-
Effect.gen(function* () {
|
|
12
|
-
const object = `${commit}:./${path}`;
|
|
13
|
-
const present = yield* git(root, ["cat-file", "-e", object]);
|
|
14
|
-
return present.code === 0 ? yield* gitOrFail(root, ["cat-file", "blob", object]) : undefined;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
10
|
const parse = (output: string): Changes => {
|
|
18
11
|
const fields = output.split("\0");
|
|
19
12
|
const files = new Set<string>();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import type { RuleInputs } from "../rule.ts";
|
|
4
|
+
import { parse } from "../rules/syntax.ts";
|
|
5
|
+
import type { ImportRequest } from "./extract.ts";
|
|
6
|
+
import { type Project, projectsFor } from "./projects.ts";
|
|
7
|
+
import { resolveImport } from "./resolve.ts";
|
|
8
|
+
import { type SpecifierSite, specifierSites } from "./specifiers.ts";
|
|
9
|
+
import { packageOf, type WorkspacePackage, workspacePackages } from "./workspace.ts";
|
|
10
|
+
|
|
11
|
+
export interface Departure extends SpecifierSite {
|
|
12
|
+
readonly file: string;
|
|
13
|
+
readonly installed: string | undefined;
|
|
14
|
+
readonly workspace: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const QUERY = /\?.*$/u;
|
|
18
|
+
|
|
19
|
+
const RELATIVE = /^\.\.?(?:\/|$)/u;
|
|
20
|
+
|
|
21
|
+
const MAY_BE_RELATIVE = /["'`]\.\.?[/"'`]|\\/u;
|
|
22
|
+
|
|
23
|
+
const TRAILING_SLASHES = /\/+$/u;
|
|
24
|
+
|
|
25
|
+
function noAmbient(): boolean {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function leavesLexically(path: string): boolean {
|
|
30
|
+
const normal = posix.normalize(path).replace(TRAILING_SLASHES, "");
|
|
31
|
+
return normal === ".." || normal.startsWith("../") || normal.includes("/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FileContext {
|
|
35
|
+
readonly from: string;
|
|
36
|
+
readonly packages: readonly WorkspacePackage[];
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly project: Project;
|
|
39
|
+
readonly root: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A relative import leaves its folder when its path climbs or descends, or when it names a folder whose index file lies below.
|
|
43
|
+
function departureAt(context: FileContext, site: SpecifierSite): Departure | undefined {
|
|
44
|
+
const path = site.specifier.replace(QUERY, "");
|
|
45
|
+
if (!RELATIVE.test(path)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const request: ImportRequest = { kind: "import", line: site.line, specifier: site.specifier, type: site.type };
|
|
49
|
+
const target = resolveImport(context.root, noAmbient, request, context.from, context.project);
|
|
50
|
+
const local = target?.kind === "file" ? target.path : undefined;
|
|
51
|
+
if (!(leavesLexically(path) || (local !== undefined && posix.dirname(local) !== posix.dirname(context.path)))) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const owner = local === undefined ? undefined : packageOf(context.packages, local);
|
|
55
|
+
const workspace = owner !== undefined && owner !== packageOf(context.packages, context.path) ? owner.name : undefined;
|
|
56
|
+
return { ...site, file: context.path, installed: target?.kind === "external" ? target.package : undefined, workspace };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function departures(inputs: RuleInputs): Promise<readonly Departure[]> {
|
|
60
|
+
const root = realpathSync(inputs.root);
|
|
61
|
+
const projectOf = projectsFor(root);
|
|
62
|
+
const packages = await workspacePackages(inputs);
|
|
63
|
+
return inputs.sources.flatMap((file) => {
|
|
64
|
+
const syntax = MAY_BE_RELATIVE.test(file.text) ? parse(file) : undefined;
|
|
65
|
+
if (syntax === undefined) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const from = posix.join(root, file.path);
|
|
69
|
+
const context = { from, packages, path: file.path, project: projectOf(from), root };
|
|
70
|
+
return specifierSites(syntax).flatMap((site) => departureAt(context, site) ?? []);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
export interface SpecifierSite {
|
|
4
|
+
readonly line: number;
|
|
5
|
+
readonly specifier: string;
|
|
6
|
+
readonly start: number;
|
|
7
|
+
readonly type: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Found {
|
|
11
|
+
readonly node: ts.Node | undefined;
|
|
12
|
+
readonly type: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MOCKS = new Set(["doMock", "doUnmock", "importActual", "importMock", "mock", "unmock"]);
|
|
16
|
+
|
|
17
|
+
const MAY_CALL = /\b(?:import|require)\s*\(|\bvi\s*\.\s*(?:doMock|doUnmock|importActual|importMock|mock|unmock)\b/u;
|
|
18
|
+
|
|
19
|
+
function declared(node: ts.Node): Found | undefined {
|
|
20
|
+
if (ts.isImportDeclaration(node)) {
|
|
21
|
+
return { node: node.moduleSpecifier, type: node.importClause?.isTypeOnly === true };
|
|
22
|
+
}
|
|
23
|
+
if (ts.isExportDeclaration(node)) {
|
|
24
|
+
return { node: node.moduleSpecifier, type: node.isTypeOnly };
|
|
25
|
+
}
|
|
26
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
27
|
+
return { node: node.moduleReference.expression, type: node.isTypeOnly };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isMock(callee: ts.Expression): boolean {
|
|
33
|
+
return (
|
|
34
|
+
ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === "vi" && MOCKS.has(callee.name.text)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function called(node: ts.Node): Found | undefined {
|
|
39
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
40
|
+
return { node: node.argument.literal, type: true };
|
|
41
|
+
}
|
|
42
|
+
if (!ts.isCallExpression(node)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const callee = node.expression;
|
|
46
|
+
const loads = callee.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(callee) && callee.text === "require") || isMock(callee);
|
|
47
|
+
return loads ? { node: node.arguments[0], type: false } : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function siteOf(source: ts.SourceFile, found: Found | undefined): readonly SpecifierSite[] {
|
|
51
|
+
const literal = found?.node;
|
|
52
|
+
if (found === undefined || literal === undefined || !ts.isStringLiteralLike(literal)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const start = literal.getStart(source);
|
|
56
|
+
return [{ line: source.getLineAndCharacterOfPosition(start).line + 1, specifier: literal.text, start, type: found.type }];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const JSDOC_IMPORT = /^[ \t]*(?:\/\*\*|\*)?[ \t]*@import\b[^"'`]*?\bfrom\s*(?<quote>["'])(?<path>[^"'\r\n]*)\k<quote>/gmu;
|
|
60
|
+
|
|
61
|
+
function siteAt(source: ts.SourceFile, start: number, specifier: string): SpecifierSite {
|
|
62
|
+
return { line: source.getLineAndCharacterOfPosition(start).line + 1, specifier, start, type: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A node keeps only its last JSDoc block, so @import tags are read from every comment before it.
|
|
66
|
+
function jsDocImports(source: ts.SourceFile, node: ts.Node, seen: Set<number>): readonly SpecifierSite[] {
|
|
67
|
+
return (ts.getLeadingCommentRanges(source.text, node.pos) ?? []).flatMap((range) => {
|
|
68
|
+
const comment = source.text.slice(range.pos, range.end);
|
|
69
|
+
if (seen.has(range.pos) || !comment.startsWith("/**")) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
seen.add(range.pos);
|
|
73
|
+
return [...comment.matchAll(JSDOC_IMPORT)].map((match) => {
|
|
74
|
+
const path = match.groups?.path ?? "";
|
|
75
|
+
return siteAt(source, range.pos + match.index + match[0].length - path.length - 1, path);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function specifierSites(source: ts.SourceFile): readonly SpecifierSite[] {
|
|
81
|
+
const sites: SpecifierSite[] = [];
|
|
82
|
+
const nested = MAY_CALL.test(source.text);
|
|
83
|
+
const documented = source.text.includes("@import");
|
|
84
|
+
const comments = new Set<number>();
|
|
85
|
+
function visit(node: ts.Node): void {
|
|
86
|
+
sites.push(...siteOf(source, declared(node) ?? (nested ? called(node) : undefined)));
|
|
87
|
+
if (documented) {
|
|
88
|
+
sites.push(...jsDocImports(source, node, comments));
|
|
89
|
+
}
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
}
|
|
92
|
+
visit(source);
|
|
93
|
+
return [...new Map(sites.map((site) => [site.start, site])).values()].sort((left, right) => left.start - right.start);
|
|
94
|
+
}
|
package/src/report/render.ts
CHANGED
|
@@ -73,6 +73,17 @@ const summary = (outcome: Outcome, context: ReportContext): string => {
|
|
|
73
73
|
return `${verdict}${warnings}. ${plural(context.checked, "source file")} checked${suppressed}.`;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
function rerecord(outcome: Outcome): readonly string[] {
|
|
77
|
+
const rules = [...new Set(outcome.errors.map((violation) => violation.rule))].sort((left, right) => left.localeCompare(right));
|
|
78
|
+
if (rules.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const command = ["quality baseline write", ...rules.map((rule) => `--rule ${rule}`)].join(" ");
|
|
82
|
+
return [
|
|
83
|
+
`If the baseline should keep these findings, such as the debt of a moved or renamed file, record them again with \`${command}\` and call out the baseline growth in the pull request description.`,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
export const render = (outcome: Outcome, context: ReportContext): string => {
|
|
77
88
|
const regressions = new Map(outcome.regressions.map((regression) => [keyOf(regression.entry.rule, regression.entry.file), regression]));
|
|
78
89
|
const warningBody = context.warnings === "all" ? (inRule: readonly Violation[]) => detailed(inRule, regressions) : summarized;
|
|
@@ -81,5 +92,6 @@ export const render = (outcome: Outcome, context: ReportContext): string => {
|
|
|
81
92
|
...ruleSections("warn", outcome.warnings, context, warningBody),
|
|
82
93
|
...staleSections(outcome, context),
|
|
83
94
|
summary(outcome, context),
|
|
95
|
+
...rerecord(outcome),
|
|
84
96
|
].join("\n\n");
|
|
85
97
|
};
|
package/src/rule.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Bivariant } from "@shivaedev/types";
|
|
1
2
|
import { describeIssue, type StandardSchemaV1 } from "./standard-schema.ts";
|
|
2
3
|
|
|
3
4
|
export interface Finding {
|
|
@@ -33,8 +34,8 @@ export type Configured =
|
|
|
33
34
|
| { readonly _tag: "Ready"; readonly check: (inputs: RuleInputs) => Promise<Findings> };
|
|
34
35
|
|
|
35
36
|
export interface Rule<Id extends string = string, Input = unknown> {
|
|
36
|
-
//
|
|
37
|
-
configure(options: Input | undefined)
|
|
37
|
+
// Bivariance keeps rules with different options in one list; configure validates its input at runtime.
|
|
38
|
+
configure: Bivariant<(options: Input | undefined) => Promise<Configured>>;
|
|
38
39
|
readonly description: string;
|
|
39
40
|
readonly family?: boolean;
|
|
40
41
|
readonly id: Id;
|
package/src/rules/biome.ts
CHANGED
|
@@ -40,7 +40,7 @@ const bridge = defineRule({
|
|
|
40
40
|
return [...preset, ...report.diagnostics.filter((diagnostic) => FAILING.has(diagnostic.severity)).map(findingOf)];
|
|
41
41
|
},
|
|
42
42
|
description:
|
|
43
|
-
"Biome's lint, format and assist findings under the shared preset. `quality fix` applies
|
|
43
|
+
"Biome's lint, format and assist findings under the shared preset. `quality fix` applies Biome's lint fixes, unsafe ones included, its assist actions and formatting.",
|
|
44
44
|
id: "biome",
|
|
45
45
|
registrable: false,
|
|
46
46
|
});
|
package/src/rules/built-in.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { noJsdoc } from "./comments/no-jsdoc.ts";
|
|
|
6
6
|
import { noLineReference } from "./comments/no-line-reference.ts";
|
|
7
7
|
import { noPrReference } from "./comments/no-pr-reference.ts";
|
|
8
8
|
import { noTodo } from "./comments/no-todo.ts";
|
|
9
|
+
import { importsAliased } from "./imports/aliased.ts";
|
|
9
10
|
import { importCycles } from "./imports/cycles.ts";
|
|
10
11
|
import { importFences } from "./imports/fences.ts";
|
|
11
12
|
import { importsResolvable } from "./imports/resolvable.ts";
|
|
@@ -31,5 +32,6 @@ export const builtInRules = [
|
|
|
31
32
|
importCycles,
|
|
32
33
|
importFences,
|
|
33
34
|
importsResolvable,
|
|
35
|
+
importsAliased,
|
|
34
36
|
manifestsSorted,
|
|
35
37
|
] as const;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Departure, departures } from "../../imports/departures.ts";
|
|
2
|
+
import { defineRule } from "../../rule.ts";
|
|
3
|
+
|
|
4
|
+
function messageOf({ installed, specifier, workspace }: Departure): string {
|
|
5
|
+
const leaves = `"${specifier}" leaves its folder.`;
|
|
6
|
+
if (installed !== undefined) {
|
|
7
|
+
return `${leaves} It reaches into the installed package ${installed}; import that package by its name.`;
|
|
8
|
+
}
|
|
9
|
+
if (workspace !== undefined) {
|
|
10
|
+
return `${leaves} It reaches into the workspace package ${workspace}; import it by that name, through a path its package.json "exports" lists.`;
|
|
11
|
+
}
|
|
12
|
+
return `${leaves} Import it through a "#…" alias from the "imports" of its package.json, and declare one there if none fits.`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const importsAliased = defineRule({
|
|
16
|
+
check: async (inputs) =>
|
|
17
|
+
(await departures(inputs)).map((departure) => ({
|
|
18
|
+
file: departure.file,
|
|
19
|
+
line: departure.line,
|
|
20
|
+
message: messageOf(departure),
|
|
21
|
+
subject: departure.specifier,
|
|
22
|
+
})),
|
|
23
|
+
description:
|
|
24
|
+
"A relative import names only a file in its own folder. Every other import goes through an alias: the package.json `imports` of its package or another workspace package's name.",
|
|
25
|
+
id: "imports/aliased",
|
|
26
|
+
});
|
package/dist/baseline/guard.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { BaselineEntry } from "./format.ts";
|
|
2
|
-
export type GuardProblem = {
|
|
3
|
-
readonly _tag: "Added";
|
|
4
|
-
readonly entry: BaselineEntry;
|
|
5
|
-
} | {
|
|
6
|
-
readonly _tag: "Raised";
|
|
7
|
-
readonly entry: BaselineEntry;
|
|
8
|
-
readonly base: BaselineEntry;
|
|
9
|
-
} | {
|
|
10
|
-
readonly _tag: "Unadopted";
|
|
11
|
-
readonly rule: string;
|
|
12
|
-
readonly entries: number;
|
|
13
|
-
} | {
|
|
14
|
-
readonly _tag: "NothingAdopted";
|
|
15
|
-
readonly rule: string;
|
|
16
|
-
};
|
|
17
|
-
export interface GuardResult {
|
|
18
|
-
readonly adopted: readonly string[];
|
|
19
|
-
readonly moved: number;
|
|
20
|
-
readonly problems: readonly GuardProblem[];
|
|
21
|
-
}
|
|
22
|
-
export declare const guardBaseline: (base: readonly BaselineEntry[], working: readonly BaselineEntry[], renames: ReadonlyMap<string, string>, adopt: readonly string[]) => GuardResult;
|
|
23
|
-
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/baseline/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,YAAY,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GACxF;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC3C;AAED,eAAO,MAAM,aAAa,GACzB,MAAM,SAAS,aAAa,EAAE,EAC9B,SAAS,SAAS,aAAa,EAAE,EACjC,SAAS,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,SAAS,MAAM,EAAE,KACtB,WA2BF,CAAC"}
|
package/dist/baseline/guard.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { covers, groupBy, keyOf } from "../engine/violation.js";
|
|
2
|
-
export const guardBaseline = (base, working, renames, adopt) => {
|
|
3
|
-
const before = new Map(base.map((entry) => [keyOf(entry.rule, entry.file), entry]));
|
|
4
|
-
const covered = new Set(base.map((entry) => entry.rule));
|
|
5
|
-
const byRule = groupBy(working, (entry) => entry.rule);
|
|
6
|
-
const problems = [];
|
|
7
|
-
let moved = 0;
|
|
8
|
-
for (const entry of working.filter((candidate) => covered.has(candidate.rule))) {
|
|
9
|
-
const exact = before.get(keyOf(entry.rule, entry.file));
|
|
10
|
-
const origin = renames.get(entry.file);
|
|
11
|
-
const matched = exact ?? (origin === undefined ? undefined : before.get(keyOf(entry.rule, origin)));
|
|
12
|
-
if (matched === undefined) {
|
|
13
|
-
problems.push({ _tag: "Added", entry });
|
|
14
|
-
}
|
|
15
|
-
else if (entry.count > matched.count) {
|
|
16
|
-
problems.push({ _tag: "Raised", base: matched, entry });
|
|
17
|
-
}
|
|
18
|
-
else if (exact === undefined) {
|
|
19
|
-
moved += 1;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
const adopted = (rule) => adopt.some((name) => covers(name, rule));
|
|
23
|
-
const fresh = [...byRule.entries()].filter(([rule]) => !covered.has(rule));
|
|
24
|
-
for (const [rule, entries] of fresh.filter(([rule]) => !adopted(rule))) {
|
|
25
|
-
problems.push({ _tag: "Unadopted", entries: entries.length, rule });
|
|
26
|
-
}
|
|
27
|
-
for (const name of adopt.filter((candidate) => ![...byRule.keys()].some((rule) => covers(candidate, rule)))) {
|
|
28
|
-
problems.push({ _tag: "NothingAdopted", rule: name });
|
|
29
|
-
}
|
|
30
|
-
return { adopted: fresh.map(([rule]) => rule).filter(adopted), moved, problems };
|
|
31
|
-
};
|
|
32
|
-
//# sourceMappingURL=guard.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/baseline/guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAehE,MAAM,CAAC,MAAM,aAAa,GAAG,CAC5B,IAA8B,EAC9B,OAAiC,EACjC,OAAoC,EACpC,KAAwB,EACV,EAAE;IAChB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAChF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,KAAK,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QACpG,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACzC,CAAC;aAAM,IAAI,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;YACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;aAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAChC,KAAK,IAAI,CAAC,CAAC;QACZ,CAAC;IACF,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3E,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7G,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAClF,CAAC,CAAC"}
|
package/dist/cli/check.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Effect, type FileSystem } from "effect";
|
|
2
|
-
import { GateFailed, type SetupFailure } from "../failure.ts";
|
|
3
|
-
import type { Git } from "../git/command.ts";
|
|
4
|
-
export declare const checkBaseline: (cwd: string, configPath: string | undefined, against: string | undefined) => Effect.Effect<void, SetupFailure | GateFailed, FileSystem.FileSystem | Git>;
|
|
5
|
-
//# sourceMappingURL=check.d.ts.map
|
package/dist/cli/check.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAQ1D,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAsB7C,eAAO,MAAM,aAAa,GACzB,KAAK,MAAM,EACX,YAAY,MAAM,GAAG,SAAS,EAC9B,SAAS,MAAM,GAAG,SAAS,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,EAAE,UAAU,CAAC,UAAU,GAAG,GAAG,CAa1E,CAAC"}
|
package/dist/cli/check.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
import { Console, Effect } from "effect";
|
|
3
|
-
import { decodeBaseline } from "../baseline/format.js";
|
|
4
|
-
import { guardBaseline } from "../baseline/guard.js";
|
|
5
|
-
import { convertLegacy, decodeLegacyBaseline, LEGACY_BASELINE, measured } from "../baseline/legacy.js";
|
|
6
|
-
import { loadConfig } from "../config/load.js";
|
|
7
|
-
import { validOrFail } from "../decoded.js";
|
|
8
|
-
import { readBaseline } from "../engine/baseline-file.js";
|
|
9
|
-
import { scan } from "../engine/session.js";
|
|
10
|
-
import { GateFailed } from "../failure.js";
|
|
11
|
-
import { resolveBase } from "../git/base.js";
|
|
12
|
-
import { changesSince, readAt } from "../git/history.js";
|
|
13
|
-
import { renderGuard } from "../report/guard.js";
|
|
14
|
-
const baseEntries = (config, base, path) => Effect.gen(function* () {
|
|
15
|
-
const raw = yield* readAt(config.root, base.commit, path);
|
|
16
|
-
if (raw !== undefined) {
|
|
17
|
-
return yield* validOrFail(`${path} at ${base.ref}`, yield* Effect.promise(() => decodeBaseline(raw)));
|
|
18
|
-
}
|
|
19
|
-
const legacyRaw = yield* readAt(config.root, base.commit, LEGACY_BASELINE);
|
|
20
|
-
if (legacyRaw === undefined) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
const legacy = yield* validOrFail(`${LEGACY_BASELINE} at ${base.ref}`, yield* Effect.promise(() => decodeLegacyBaseline(legacyRaw)));
|
|
24
|
-
return measured(legacy) ? convertLegacy(legacy, (yield* scan(config)).violations) : legacy;
|
|
25
|
-
});
|
|
26
|
-
export const checkBaseline = (cwd, configPath, against) => Effect.gen(function* () {
|
|
27
|
-
const config = yield* loadConfig(cwd, configPath);
|
|
28
|
-
const working = yield* readBaseline(config);
|
|
29
|
-
const base = yield* resolveBase(config.root, against);
|
|
30
|
-
const { moves } = yield* changesSince(config.root, [base.commit]);
|
|
31
|
-
const renames = new Map([...moves].map(([from, to]) => [to, from]));
|
|
32
|
-
const before = yield* baseEntries(config, base, renames.get(config.baseline) ?? config.baseline);
|
|
33
|
-
const result = guardBaseline(before, working.entries, renames, config.adopt);
|
|
34
|
-
yield* Console.log(renderGuard(result, { base, baseline: config.baseline, config: relative(config.root, config.file) }));
|
|
35
|
-
if (result.problems.length > 0) {
|
|
36
|
-
return yield* new GateFailed();
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
//# sourceMappingURL=check.js.map
|
package/dist/cli/check.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"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"}
|
package/dist/report/guard.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { GuardResult } from "../baseline/guard.ts";
|
|
2
|
-
import type { Base } from "../git/base.ts";
|
|
3
|
-
export interface GuardContext {
|
|
4
|
-
readonly base: Base;
|
|
5
|
-
readonly baseline: string;
|
|
6
|
-
readonly config: string;
|
|
7
|
-
}
|
|
8
|
-
export declare const renderGuard: (result: GuardResult, context: GuardContext) => string;
|
|
9
|
-
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/report/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAG3C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAyBD,eAAO,MAAM,WAAW,GAAI,QAAQ,WAAW,EAAE,SAAS,YAAY,KAAG,MAYxE,CAAC"}
|
package/dist/report/guard.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { plural } from "./plural.js";
|
|
2
|
-
const problemLine = (problem, context) => {
|
|
3
|
-
switch (problem._tag) {
|
|
4
|
-
case "Added":
|
|
5
|
-
return `${problem.entry.rule} ${problem.entry.file} is new, at ${problem.entry.count}.`;
|
|
6
|
-
case "Raised":
|
|
7
|
-
return `${problem.entry.rule} ${problem.entry.file} rose to ${problem.entry.count} from ${problem.base.count}.`;
|
|
8
|
-
case "Unadopted":
|
|
9
|
-
return `${problem.rule} is newly baselined in ${plural(problem.entries, "file")} without being named under \`adopt\` in ${context.config}.`;
|
|
10
|
-
case "NothingAdopted":
|
|
11
|
-
return `\`adopt\` in ${context.config} names ${problem.rule}, which has nothing baselined. Remove it.`;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const against = (base) => `${base.ref} (merge base ${base.commit.slice(0, 12)})`;
|
|
15
|
-
const held = (result) => [
|
|
16
|
-
...(result.moved > 0 ? [`${plural(result.moved, "entry", "entries")} followed moved files`] : []),
|
|
17
|
-
...(result.adopted.length > 0 ? [`adopted ${result.adopted.join(", ")}`] : []),
|
|
18
|
-
]
|
|
19
|
-
.map((part) => `; ${part}`)
|
|
20
|
-
.join("");
|
|
21
|
-
export const renderGuard = (result, context) => {
|
|
22
|
-
if (result.problems.length === 0) {
|
|
23
|
-
return `quality: ${context.baseline} holds against ${against(context.base)}${held(result)}.`;
|
|
24
|
-
}
|
|
25
|
-
return [
|
|
26
|
-
[
|
|
27
|
-
`error ${context.baseline} grew against ${against(context.base)}`,
|
|
28
|
-
" The baseline only shrinks. Fix new violations instead of baselining them; a rule enters the baseline for the first time only while `adopt` names it.",
|
|
29
|
-
...result.problems.map((problem) => ` ${problemLine(problem, context)}`),
|
|
30
|
-
].join("\n"),
|
|
31
|
-
`quality: baseline check failed with ${plural(result.problems.length, "problem")}.`,
|
|
32
|
-
].join("\n\n");
|
|
33
|
-
};
|
|
34
|
-
//# sourceMappingURL=guard.js.map
|
package/dist/report/guard.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/report/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQrC,MAAM,WAAW,GAAG,CAAC,OAAqB,EAAE,OAAqB,EAAU,EAAE;IAC5E,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,OAAO;YACX,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,eAAe,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC;QACzF,KAAK,QAAQ;YACZ,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,YAAY,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC;QACjH,KAAK,WAAW;YACf,OAAO,GAAG,OAAO,CAAC,IAAI,0BAA0B,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,2CAA2C,OAAO,CAAC,MAAM,GAAG,CAAC;QAC7I,KAAK,gBAAgB;YACpB,OAAO,gBAAgB,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,IAAI,2CAA2C,CAAC;IACzG,CAAC;AACF,CAAC,CAAC;AAEF,MAAM,OAAO,GAAG,CAAC,IAAU,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,gBAAgB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;AAE/F,MAAM,IAAI,GAAG,CAAC,MAAmB,EAAU,EAAE,CAC5C;IACC,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;CAC9E;KACC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;KAC1B,IAAI,CAAC,EAAE,CAAC,CAAC;AAEZ,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAmB,EAAE,OAAqB,EAAU,EAAE;IACjF,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,YAAY,OAAO,CAAC,QAAQ,kBAAkB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;IAC9F,CAAC;IACD,OAAO;QACN;YACC,SAAS,OAAO,CAAC,QAAQ,iBAAiB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACjE,wJAAwJ;YACxJ,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;SACzE,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,uCAAuC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG;KACnF,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAChB,CAAC,CAAC"}
|
package/src/baseline/guard.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { covers, groupBy, keyOf } from "../engine/violation.ts";
|
|
2
|
-
import type { BaselineEntry } from "./format.ts";
|
|
3
|
-
|
|
4
|
-
export type GuardProblem =
|
|
5
|
-
| { readonly _tag: "Added"; readonly entry: BaselineEntry }
|
|
6
|
-
| { readonly _tag: "Raised"; readonly entry: BaselineEntry; readonly base: BaselineEntry }
|
|
7
|
-
| { readonly _tag: "Unadopted"; readonly rule: string; readonly entries: number }
|
|
8
|
-
| { readonly _tag: "NothingAdopted"; readonly rule: string };
|
|
9
|
-
|
|
10
|
-
export interface GuardResult {
|
|
11
|
-
readonly adopted: readonly string[];
|
|
12
|
-
readonly moved: number;
|
|
13
|
-
readonly problems: readonly GuardProblem[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const guardBaseline = (
|
|
17
|
-
base: readonly BaselineEntry[],
|
|
18
|
-
working: readonly BaselineEntry[],
|
|
19
|
-
renames: ReadonlyMap<string, string>,
|
|
20
|
-
adopt: readonly string[],
|
|
21
|
-
): GuardResult => {
|
|
22
|
-
const before = new Map(base.map((entry) => [keyOf(entry.rule, entry.file), entry]));
|
|
23
|
-
const covered = new Set(base.map((entry) => entry.rule));
|
|
24
|
-
const byRule = groupBy(working, (entry) => entry.rule);
|
|
25
|
-
const problems: GuardProblem[] = [];
|
|
26
|
-
let moved = 0;
|
|
27
|
-
for (const entry of working.filter((candidate) => covered.has(candidate.rule))) {
|
|
28
|
-
const exact = before.get(keyOf(entry.rule, entry.file));
|
|
29
|
-
const origin = renames.get(entry.file);
|
|
30
|
-
const matched = exact ?? (origin === undefined ? undefined : before.get(keyOf(entry.rule, origin)));
|
|
31
|
-
if (matched === undefined) {
|
|
32
|
-
problems.push({ _tag: "Added", entry });
|
|
33
|
-
} else if (entry.count > matched.count) {
|
|
34
|
-
problems.push({ _tag: "Raised", base: matched, entry });
|
|
35
|
-
} else if (exact === undefined) {
|
|
36
|
-
moved += 1;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const adopted = (rule: string): boolean => adopt.some((name) => covers(name, rule));
|
|
40
|
-
const fresh = [...byRule.entries()].filter(([rule]) => !covered.has(rule));
|
|
41
|
-
for (const [rule, entries] of fresh.filter(([rule]) => !adopted(rule))) {
|
|
42
|
-
problems.push({ _tag: "Unadopted", entries: entries.length, rule });
|
|
43
|
-
}
|
|
44
|
-
for (const name of adopt.filter((candidate) => ![...byRule.keys()].some((rule) => covers(candidate, rule)))) {
|
|
45
|
-
problems.push({ _tag: "NothingAdopted", rule: name });
|
|
46
|
-
}
|
|
47
|
-
return { adopted: fresh.map(([rule]) => rule).filter(adopted), moved, problems };
|
|
48
|
-
};
|
package/src/cli/check.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
import { Console, Effect, type FileSystem } from "effect";
|
|
3
|
-
import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
|
|
4
|
-
import { guardBaseline } from "../baseline/guard.ts";
|
|
5
|
-
import { convertLegacy, decodeLegacyBaseline, LEGACY_BASELINE, measured } from "../baseline/legacy.ts";
|
|
6
|
-
import { loadConfig, type ResolvedConfig } from "../config/load.ts";
|
|
7
|
-
import { validOrFail } from "../decoded.ts";
|
|
8
|
-
import { readBaseline } from "../engine/baseline-file.ts";
|
|
9
|
-
import { scan } from "../engine/session.ts";
|
|
10
|
-
import { GateFailed, type SetupFailure } from "../failure.ts";
|
|
11
|
-
import { type Base, resolveBase } from "../git/base.ts";
|
|
12
|
-
import type { Git } from "../git/command.ts";
|
|
13
|
-
import { changesSince, readAt } from "../git/history.ts";
|
|
14
|
-
import { renderGuard } from "../report/guard.ts";
|
|
15
|
-
|
|
16
|
-
const baseEntries = (
|
|
17
|
-
config: ResolvedConfig,
|
|
18
|
-
base: Base,
|
|
19
|
-
path: string,
|
|
20
|
-
): Effect.Effect<readonly BaselineEntry[], SetupFailure, FileSystem.FileSystem | Git> =>
|
|
21
|
-
Effect.gen(function* () {
|
|
22
|
-
const raw = yield* readAt(config.root, base.commit, path);
|
|
23
|
-
if (raw !== undefined) {
|
|
24
|
-
return yield* validOrFail(`${path} at ${base.ref}`, yield* Effect.promise(() => decodeBaseline(raw)));
|
|
25
|
-
}
|
|
26
|
-
const legacyRaw = yield* readAt(config.root, base.commit, LEGACY_BASELINE);
|
|
27
|
-
if (legacyRaw === undefined) {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
const legacy = yield* validOrFail(`${LEGACY_BASELINE} at ${base.ref}`, yield* Effect.promise(() => decodeLegacyBaseline(legacyRaw)));
|
|
31
|
-
return measured(legacy) ? convertLegacy(legacy, (yield* scan(config)).violations) : legacy;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const checkBaseline = (
|
|
35
|
-
cwd: string,
|
|
36
|
-
configPath: string | undefined,
|
|
37
|
-
against: string | undefined,
|
|
38
|
-
): Effect.Effect<void, SetupFailure | GateFailed, FileSystem.FileSystem | Git> =>
|
|
39
|
-
Effect.gen(function* () {
|
|
40
|
-
const config = yield* loadConfig(cwd, configPath);
|
|
41
|
-
const working = yield* readBaseline(config);
|
|
42
|
-
const base = yield* resolveBase(config.root, against);
|
|
43
|
-
const { moves } = yield* changesSince(config.root, [base.commit]);
|
|
44
|
-
const renames = new Map([...moves].map(([from, to]) => [to, from]));
|
|
45
|
-
const before = yield* baseEntries(config, base, renames.get(config.baseline) ?? config.baseline);
|
|
46
|
-
const result = guardBaseline(before, working.entries, renames, config.adopt);
|
|
47
|
-
yield* Console.log(renderGuard(result, { base, baseline: config.baseline, config: relative(config.root, config.file) }));
|
|
48
|
-
if (result.problems.length > 0) {
|
|
49
|
-
return yield* new GateFailed();
|
|
50
|
-
}
|
|
51
|
-
});
|