@shivaedev/quality 0.2.0 → 0.3.1
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 +114 -0
- package/README.md +207 -35
- package/dist/baseline/compare.d.ts +3 -8
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +13 -23
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +7 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +31 -23
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/guard.d.ts +23 -0
- package/dist/baseline/guard.d.ts.map +1 -0
- package/dist/baseline/guard.js +31 -0
- package/dist/baseline/guard.js.map +1 -0
- package/dist/baseline/legacy.d.ts +11 -0
- package/dist/baseline/legacy.d.ts.map +1 -0
- package/dist/baseline/legacy.js +33 -0
- package/dist/baseline/legacy.js.map +1 -0
- package/dist/baseline/rewrite.d.ts +3 -0
- package/dist/baseline/rewrite.d.ts.map +1 -0
- package/dist/baseline/rewrite.js +19 -0
- package/dist/baseline/rewrite.js.map +1 -0
- package/dist/baseline/update.d.ts +7 -6
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +28 -28
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +14 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +54 -29
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +3 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +19 -10
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +39 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts +4 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +31 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/run-main.d.ts +2 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +5 -2
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +6 -0
- package/dist/cli/shrink.d.ts.map +1 -0
- package/dist/cli/shrink.js +8 -0
- package/dist/cli/shrink.js.map +1 -0
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +1 -0
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +1 -0
- package/dist/config/decode.js.map +1 -1
- package/dist/config/file.d.ts +2 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +2 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +3 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +6 -4
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +2 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +11 -0
- package/dist/engine/baseline-file.d.ts.map +1 -0
- package/dist/engine/baseline-file.js +18 -0
- package/dist/engine/baseline-file.js.map +1 -0
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +3 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/session.d.ts +3 -5
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +12 -10
- package/dist/engine/session.js.map +1 -1
- package/dist/exceptions/registry.d.ts +1 -1
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +8 -3
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/failure.d.ts +0 -2
- package/dist/failure.d.ts.map +1 -1
- package/dist/failure.js +0 -2
- package/dist/failure.js.map +1 -1
- package/dist/git/base.d.ts +9 -0
- package/dist/git/base.d.ts.map +1 -0
- package/dist/git/base.js +59 -0
- package/dist/git/base.js.map +1 -0
- package/dist/git/command.d.ts +12 -0
- package/dist/git/command.d.ts.map +1 -0
- package/dist/git/command.js +13 -0
- package/dist/git/command.js.map +1 -0
- package/dist/git/history.d.ts +10 -0
- package/dist/git/history.d.ts.map +1 -0
- package/dist/git/history.js +28 -0
- package/dist/git/history.js.map +1 -0
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +0 -1
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/inventory/walk.d.ts +0 -1
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +1 -1
- package/dist/inventory/walk.js.map +1 -1
- package/dist/report/guard.d.ts +9 -0
- package/dist/report/guard.d.ts.map +1 -0
- package/dist/report/guard.js +34 -0
- package/dist/report/guard.js.map +1 -0
- package/dist/report/render.js +1 -2
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +4 -3
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +4 -7
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/built-in.d.ts +17 -0
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +21 -1
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts +9 -0
- package/dist/rules/comments/kinds.d.ts.map +1 -0
- package/dist/rules/comments/kinds.js +34 -0
- package/dist/rules/comments/kinds.js.map +1 -0
- package/dist/rules/comments/max-per-file.d.ts +5 -0
- package/dist/rules/comments/max-per-file.d.ts.map +1 -0
- package/dist/rules/comments/max-per-file.js +39 -0
- package/dist/rules/comments/max-per-file.js.map +1 -0
- package/dist/rules/comments/no-banner.d.ts +2 -0
- package/dist/rules/comments/no-banner.d.ts.map +1 -0
- package/dist/rules/comments/no-banner.js +9 -0
- package/dist/rules/comments/no-banner.js.map +1 -0
- package/dist/rules/comments/no-jsdoc.d.ts +4 -0
- package/dist/rules/comments/no-jsdoc.d.ts.map +1 -0
- package/dist/rules/comments/no-jsdoc.js +14 -0
- package/dist/rules/comments/no-jsdoc.js.map +1 -0
- package/dist/rules/comments/no-line-reference.d.ts +2 -0
- package/dist/rules/comments/no-line-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-line-reference.js +9 -0
- package/dist/rules/comments/no-line-reference.js.map +1 -0
- package/dist/rules/comments/no-pr-reference.d.ts +2 -0
- package/dist/rules/comments/no-pr-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-pr-reference.js +13 -0
- package/dist/rules/comments/no-pr-reference.js.map +1 -0
- package/dist/rules/comments/no-todo.d.ts +2 -0
- package/dist/rules/comments/no-todo.d.ts.map +1 -0
- package/dist/rules/comments/no-todo.js +8 -0
- package/dist/rules/comments/no-todo.js.map +1 -0
- package/dist/rules/comments/pattern-rule.d.ts +10 -0
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -0
- package/dist/rules/comments/pattern-rule.js +16 -0
- package/dist/rules/comments/pattern-rule.js.map +1 -0
- package/dist/rules/comments/scan.d.ts +13 -0
- package/dist/rules/comments/scan.d.ts.map +1 -0
- package/dist/rules/comments/scan.js +62 -0
- package/dist/rules/comments/scan.js.map +1 -0
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +8 -1
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts +14 -0
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/configs.js +39 -0
- package/dist/rules/suppressions/biome/configs.js.map +1 -0
- package/dist/rules/suppressions/biome/json.d.ts +27 -0
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/json.js +37 -0
- package/dist/rules/suppressions/biome/json.js.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts +9 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.js +68 -0
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -0
- package/dist/rules/suppressions/biome-overrides.d.ts +8 -0
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -0
- package/dist/rules/suppressions/biome-overrides.js +47 -0
- package/dist/rules/suppressions/biome-overrides.js.map +1 -0
- package/dist/rules/suppressions/directives.d.ts +4 -0
- package/dist/rules/suppressions/directives.d.ts.map +1 -0
- package/dist/rules/suppressions/directives.js +19 -0
- package/dist/rules/suppressions/directives.js.map +1 -0
- package/dist/rules/suppressions/no-double-cast.d.ts +2 -0
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -0
- package/dist/rules/suppressions/no-double-cast.js +38 -0
- package/dist/rules/suppressions/no-double-cast.js.map +1 -0
- package/dist/rules/suppressions/no-inline.d.ts +8 -0
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -0
- package/dist/rules/suppressions/no-inline.js +48 -0
- package/dist/rules/suppressions/no-inline.js.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts +4 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.js +20 -0
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -0
- package/dist/rules/syntax.d.ts +4 -0
- package/dist/rules/syntax.d.ts.map +1 -0
- package/dist/rules/syntax.js +12 -0
- package/dist/rules/syntax.js.map +1 -0
- package/package.json +4 -5
- package/src/baseline/compare.ts +15 -39
- package/src/baseline/format.ts +35 -32
- package/src/baseline/guard.ts +47 -0
- package/src/baseline/legacy.ts +46 -0
- package/src/baseline/rewrite.ts +24 -0
- package/src/baseline/update.ts +47 -30
- package/src/cli/args.ts +64 -30
- package/src/cli/baseline.ts +27 -13
- package/src/cli/check.ts +51 -0
- package/src/cli/lint.ts +1 -1
- package/src/cli/migrate.ts +36 -0
- package/src/cli/run-main.ts +8 -3
- package/src/cli/shrink.ts +31 -0
- package/src/cli.ts +11 -2
- package/src/config/decode.ts +1 -0
- package/src/config/file.ts +1 -0
- package/src/config/load.ts +4 -3
- package/src/config/resolve.ts +7 -4
- package/src/config.ts +3 -7
- package/src/engine/baseline-file.ts +27 -0
- package/src/engine/evaluate.ts +4 -2
- package/src/engine/session.ts +14 -12
- package/src/exceptions/registry.ts +9 -2
- package/src/failure.ts +0 -2
- package/src/git/base.ts +89 -0
- package/src/git/command.ts +31 -0
- package/src/git/history.ts +41 -0
- package/src/inventory/ignore-scope.ts +0 -1
- package/src/inventory/walk.ts +1 -1
- package/src/report/guard.ts +46 -0
- package/src/report/render.ts +2 -2
- package/src/report/stale.ts +11 -3
- package/src/rule.ts +5 -8
- package/src/rules/built-in.ts +21 -1
- package/src/rules/comments/kinds.ts +45 -0
- package/src/rules/comments/max-per-file.ts +46 -0
- package/src/rules/comments/no-banner.ts +10 -0
- package/src/rules/comments/no-jsdoc.ts +18 -0
- package/src/rules/comments/no-line-reference.ts +11 -0
- package/src/rules/comments/no-pr-reference.ts +13 -0
- package/src/rules/comments/no-todo.ts +8 -0
- package/src/rules/comments/pattern-rule.ts +28 -0
- package/src/rules/comments/scan.ts +80 -0
- package/src/rules/max-lines.ts +8 -1
- package/src/rules/suppressions/biome/configs.ts +50 -0
- package/src/rules/suppressions/biome/json.ts +55 -0
- package/src/rules/suppressions/biome/weakenings.ts +102 -0
- package/src/rules/suppressions/biome-overrides.ts +61 -0
- package/src/rules/suppressions/directives.ts +25 -0
- package/src/rules/suppressions/no-double-cast.ts +47 -0
- package/src/rules/suppressions/no-inline.ts +63 -0
- package/src/rules/suppressions/stylesheet-comments.ts +27 -0
- package/src/rules/syntax.ts +14 -0
package/src/cli/check.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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<ReadonlyArray<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
|
+
});
|
package/src/cli/lint.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const lint = (
|
|
|
11
11
|
): Effect.Effect<void, SetupFailure | GateFailed, FileSystem.FileSystem> =>
|
|
12
12
|
Effect.gen(function* () {
|
|
13
13
|
const session = yield* openSession(cwd, config);
|
|
14
|
-
const outcome = evaluate(session.violations, session.registry, session.baseline.entries, session.config.levels);
|
|
14
|
+
const outcome = evaluate(session.violations, session.registry, session.baseline.entries, session.config.levels, session.config.unregistrable);
|
|
15
15
|
yield* Console.log(
|
|
16
16
|
render(outcome, {
|
|
17
17
|
baseline: session.config.baseline,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Console, Effect, FileSystem } from "effect";
|
|
3
|
+
import { encodeBaseline } from "../baseline/format.ts";
|
|
4
|
+
import { convertLegacy, decodeLegacyBaseline, LEGACY_BASELINE } from "../baseline/legacy.ts";
|
|
5
|
+
import { loadConfig } from "../config/load.ts";
|
|
6
|
+
import { validOrFail } from "../decoded.ts";
|
|
7
|
+
import { readInput } from "../engine/baseline-file.ts";
|
|
8
|
+
import { scan } from "../engine/session.ts";
|
|
9
|
+
import { SetupFailure } from "../failure.ts";
|
|
10
|
+
import { writeText } from "../inventory/filesystem.ts";
|
|
11
|
+
import { plural } from "../report/plural.ts";
|
|
12
|
+
|
|
13
|
+
export const migrateBaseline = (
|
|
14
|
+
cwd: string,
|
|
15
|
+
configPath: string | undefined,
|
|
16
|
+
from: string | undefined,
|
|
17
|
+
): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const config = yield* loadConfig(cwd, configPath);
|
|
20
|
+
const source = from ?? LEGACY_BASELINE;
|
|
21
|
+
if (source === config.baseline || (yield* readInput(config.root, config.baseline)) !== undefined) {
|
|
22
|
+
return yield* new SetupFailure({ message: `${config.baseline} already exists. Migrate into a baseline file that does not exist yet.` });
|
|
23
|
+
}
|
|
24
|
+
const raw = yield* readInput(config.root, source);
|
|
25
|
+
if (raw === undefined) {
|
|
26
|
+
return yield* new SetupFailure({ message: `no baseline at ${source} to migrate. Name it with --from <file>.` });
|
|
27
|
+
}
|
|
28
|
+
const legacy = yield* validOrFail(source, yield* Effect.promise(() => decodeLegacyBaseline(raw)));
|
|
29
|
+
const entries = convertLegacy(legacy, (yield* scan(config)).violations);
|
|
30
|
+
const fs = yield* FileSystem.FileSystem;
|
|
31
|
+
const written = Effect.andThen(writeText(join(config.root, config.baseline), encodeBaseline(entries)), fs.remove(join(config.root, source)));
|
|
32
|
+
yield* Effect.mapError(written, (failure) => new SetupFailure({ message: failure.message }));
|
|
33
|
+
const fixed = legacy.length - entries.length;
|
|
34
|
+
const dropped = fixed > 0 ? `, dropping ${plural(fixed, "fixed entry", "fixed entries")}` : "";
|
|
35
|
+
yield* Console.log(`quality: moved ${plural(entries.length, "entry", "entries")} from ${source} to ${config.baseline}${dropped}.`);
|
|
36
|
+
});
|
package/src/cli/run-main.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import * as NodeChildProcessSpawner from "@effect/platform-node/NodeChildProcessSpawner";
|
|
1
2
|
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
3
|
+
import * as NodePath from "@effect/platform-node/NodePath";
|
|
2
4
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
3
|
-
import { Cause, Console, Effect, Exit, type FileSystem, Runtime } from "effect";
|
|
5
|
+
import { Cause, Console, Effect, Exit, type FileSystem, Layer, Runtime } from "effect";
|
|
4
6
|
import { SetupFailure } from "../failure.ts";
|
|
7
|
+
import type { Git } from "../git/command.ts";
|
|
5
8
|
|
|
6
9
|
const INTERRUPTED = 130;
|
|
7
10
|
const COULD_NOT_RUN = 2;
|
|
@@ -24,7 +27,9 @@ const teardown: Runtime.Teardown = (exit, onExit) => {
|
|
|
24
27
|
return onExit(Cause.hasInterruptsOnly(exit.cause) ? INTERRUPTED : exitCodeOf(Cause.squash(exit.cause)));
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
const services = NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
|
|
31
|
+
|
|
32
|
+
export const runMain = <Value, Error>(program: Effect.Effect<Value, Error, FileSystem.FileSystem | Git>): void => {
|
|
28
33
|
const explained = Effect.tapCause(program, (cause) => (Cause.hasInterruptsOnly(cause) ? Effect.void : explain(cause)));
|
|
29
|
-
NodeRuntime.runMain(Effect.provide(explained,
|
|
34
|
+
NodeRuntime.runMain(Effect.provide(explained, services), { disableErrorReporting: true, teardown });
|
|
30
35
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Console, Effect, type FileSystem } from "effect";
|
|
2
|
+
import type { Scope } from "../baseline/update.ts";
|
|
3
|
+
import type { SetupFailure } from "../failure.ts";
|
|
4
|
+
import { resolveBase } from "../git/base.ts";
|
|
5
|
+
import type { Git } from "../git/command.ts";
|
|
6
|
+
import { changesSince } from "../git/history.ts";
|
|
7
|
+
import { shrinkBaseline } from "./baseline.ts";
|
|
8
|
+
|
|
9
|
+
const followedMoves = (root: string, against: string | undefined): Effect.Effect<Scope, SetupFailure, Git> =>
|
|
10
|
+
resolveBase(root, against).pipe(
|
|
11
|
+
Effect.flatMap((base) => changesSince(root, [base.commit])),
|
|
12
|
+
Effect.map((changes): Scope => ({ moves: changes.moves })),
|
|
13
|
+
Effect.catchIf(
|
|
14
|
+
() => against === undefined,
|
|
15
|
+
(failure) => Effect.as(Console.error(`quality: moved files are not followed: ${failure.message}`), { moves: new Map() }),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const pruneBaseline = (
|
|
20
|
+
cwd: string,
|
|
21
|
+
config: string | undefined,
|
|
22
|
+
against: string | undefined,
|
|
23
|
+
): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | Git> =>
|
|
24
|
+
shrinkBaseline(cwd, config, (session) => followedMoves(session.config.root, against));
|
|
25
|
+
|
|
26
|
+
export const tightenBaseline = (
|
|
27
|
+
cwd: string,
|
|
28
|
+
config: string | undefined,
|
|
29
|
+
staged: boolean,
|
|
30
|
+
): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | Git> =>
|
|
31
|
+
shrinkBaseline(cwd, config, (session) => changesSince(session.config.root, staged ? ["--cached"] : ["HEAD"]));
|
package/src/cli.ts
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { Console, Effect } from "effect";
|
|
4
4
|
import { parseCommand, USAGE } from "./cli/args.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { writeBaseline } from "./cli/baseline.ts";
|
|
6
|
+
import { checkBaseline } from "./cli/check.ts";
|
|
6
7
|
import { lint } from "./cli/lint.ts";
|
|
8
|
+
import { migrateBaseline } from "./cli/migrate.ts";
|
|
7
9
|
import { runMain } from "./cli/run-main.ts";
|
|
10
|
+
import { pruneBaseline, tightenBaseline } from "./cli/shrink.ts";
|
|
8
11
|
import { SetupFailure } from "./failure.ts";
|
|
9
12
|
|
|
10
13
|
const parsed = parseCommand(process.argv.slice(2));
|
|
@@ -22,7 +25,13 @@ const program = Effect.gen(function* () {
|
|
|
22
25
|
case "BaselineWrite":
|
|
23
26
|
return yield* writeBaseline(process.cwd(), command.config, command.rules);
|
|
24
27
|
case "BaselinePrune":
|
|
25
|
-
return yield* pruneBaseline(process.cwd(), command.config);
|
|
28
|
+
return yield* pruneBaseline(process.cwd(), command.config, command.against);
|
|
29
|
+
case "BaselineTighten":
|
|
30
|
+
return yield* tightenBaseline(process.cwd(), command.config, command.staged);
|
|
31
|
+
case "BaselineCheck":
|
|
32
|
+
return yield* checkBaseline(process.cwd(), command.config, command.against);
|
|
33
|
+
case "BaselineMigrate":
|
|
34
|
+
return yield* migrateBaseline(process.cwd(), command.config, command.from);
|
|
26
35
|
}
|
|
27
36
|
});
|
|
28
37
|
|
package/src/config/decode.ts
CHANGED
|
@@ -24,6 +24,7 @@ const ConfigInput = Schema.Struct({
|
|
|
24
24
|
extensions: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
25
25
|
registry: Schema.optionalKey(Schema.String),
|
|
26
26
|
baseline: Schema.optionalKey(Schema.String),
|
|
27
|
+
adopt: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
27
28
|
local: Schema.optionalKey(Schema.Array(LocalRule)),
|
|
28
29
|
rules: Schema.optionalKey(Schema.Record(Schema.String, Setting)),
|
|
29
30
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONFIG_FILE = "quality.config.ts";
|
package/src/config/load.ts
CHANGED
|
@@ -5,10 +5,9 @@ import { validOrFail } from "../decoded.ts";
|
|
|
5
5
|
import { SetupFailure } from "../failure.ts";
|
|
6
6
|
import type { InventoryScope } from "../inventory/collect.ts";
|
|
7
7
|
import { type ConfigInput, decodeConfig } from "./decode.ts";
|
|
8
|
+
import { CONFIG_FILE } from "./file.ts";
|
|
8
9
|
import { type ResolvedRules, resolveRules } from "./resolve.ts";
|
|
9
10
|
|
|
10
|
-
export const CONFIG_FILE = "quality.config.ts";
|
|
11
|
-
|
|
12
11
|
const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
13
12
|
|
|
14
13
|
export interface ResolvedConfig extends ResolvedRules, InventoryScope {
|
|
@@ -16,6 +15,7 @@ export interface ResolvedConfig extends ResolvedRules, InventoryScope {
|
|
|
16
15
|
readonly file: string;
|
|
17
16
|
readonly registry: string;
|
|
18
17
|
readonly baseline: string;
|
|
18
|
+
readonly adopt: ReadonlyArray<string>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const messageOf = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
|
|
@@ -34,7 +34,8 @@ const importDefault = (file: string): Effect.Effect<unknown, SetupFailure> =>
|
|
|
34
34
|
|
|
35
35
|
const resolved = (root: string, file: string, config: ConfigInput, rules: ResolvedRules): ResolvedConfig => ({
|
|
36
36
|
...rules,
|
|
37
|
-
|
|
37
|
+
adopt: config.adopt ?? [],
|
|
38
|
+
baseline: config.baseline ?? "quality/baseline.jsonl",
|
|
38
39
|
exclude: config.exclude ?? [],
|
|
39
40
|
extensions: config.extensions ?? DEFAULT_EXTENSIONS,
|
|
40
41
|
file,
|
package/src/config/resolve.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ConfigInput } from "./decode.ts";
|
|
|
9
9
|
export interface ResolvedRules {
|
|
10
10
|
readonly active: ReadonlyArray<ActiveRule>;
|
|
11
11
|
readonly levels: ReadonlyMap<string, Level>;
|
|
12
|
+
readonly unregistrable: ReadonlySet<string>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
type Setting = NonNullable<ConfigInput["rules"]>[string];
|
|
@@ -47,14 +48,16 @@ export const resolveRules = (config: ConfigInput): Effect.Effect<Decoded<Resolve
|
|
|
47
48
|
const rules: ReadonlyArray<Rule> = [...builtInRules, ...(config.local ?? [])];
|
|
48
49
|
const settings = config.rules ?? {};
|
|
49
50
|
const known = new Set(rules.map((rule) => rule.id));
|
|
50
|
-
const unknown = Object.keys(settings)
|
|
51
|
-
.filter((
|
|
52
|
-
.map((
|
|
51
|
+
const unknown = [...Object.keys(settings).map((id) => `rules.${id}`), ...(config.adopt ?? []).map((id) => `adopt.${id}`)]
|
|
52
|
+
.filter((path) => !known.has(path.slice(path.indexOf(".") + 1)))
|
|
53
|
+
.map((path) => `${path}: no built-in or local rule has this id`);
|
|
53
54
|
const activated = yield* Effect.forEach(rules, (rule) => activate(rule, settings[rule.id]));
|
|
54
55
|
const issues = [...duplicateIssues(rules), ...unknown, ...activated.flatMap((result) => (result._tag === "Invalid" ? result.issues : []))];
|
|
55
56
|
if (issues.length > 0) {
|
|
56
57
|
return { _tag: "Invalid", issues };
|
|
57
58
|
}
|
|
58
59
|
const active = activated.flatMap((result) => (result._tag === "Valid" && result.value !== undefined ? [result.value] : []));
|
|
59
|
-
|
|
60
|
+
const levels = new Map(rules.map((rule) => [rule.id, levelOf(settings[rule.id])]));
|
|
61
|
+
const unregistrable = new Set(rules.filter((rule) => rule.registrable === false).map((rule) => rule.id));
|
|
62
|
+
return { _tag: "Valid", value: { active, levels, unregistrable } };
|
|
60
63
|
});
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Rule } from "./rule.ts";
|
|
2
2
|
import type { builtInRules } from "./rules/built-in.ts";
|
|
3
3
|
|
|
4
|
-
/** Every rule defaults to `error`. `warn` is for the transition to a rule and is meant to be temporary. */
|
|
5
4
|
export type Level = "error" | "warn" | "off";
|
|
6
5
|
|
|
7
6
|
export type RuleSetting<Input> = Level | { readonly level?: Level; readonly options?: Input };
|
|
@@ -14,18 +13,15 @@ export type RuleSettings<Rules extends ReadonlyArray<Rule>> = {
|
|
|
14
13
|
|
|
15
14
|
export type BuiltInRules = typeof builtInRules;
|
|
16
15
|
|
|
16
|
+
type IdOf<Rules extends ReadonlyArray<Rule>> = Rules[number]["id"];
|
|
17
|
+
|
|
17
18
|
export interface QualityConfig<Local extends ReadonlyArray<Rule> = ReadonlyArray<Rule>> {
|
|
18
|
-
/** Directories or files to check, relative to the config file. Defaults to the whole repository. */
|
|
19
19
|
readonly sources?: ReadonlyArray<string>;
|
|
20
|
-
/** Paths never checked, in .gitignore syntax. Ignored files are always skipped. */
|
|
21
20
|
readonly exclude?: ReadonlyArray<string>;
|
|
22
|
-
/** Extensions of the source files rules read. Defaults to TypeScript and JavaScript modules. */
|
|
23
21
|
readonly extensions?: ReadonlyArray<string>;
|
|
24
|
-
/** Permanent exceptions, each with a reason. Defaults to `quality/registry.json`. */
|
|
25
22
|
readonly registry?: string;
|
|
26
|
-
/** Existing violations that may only shrink. Defaults to `quality/baseline.json`. */
|
|
27
23
|
readonly baseline?: string;
|
|
28
|
-
|
|
24
|
+
readonly adopt?: ReadonlyArray<IdOf<readonly [...BuiltInRules, ...Local]>>;
|
|
29
25
|
readonly local?: Local;
|
|
30
26
|
readonly rules?: RuleSettings<readonly [...BuiltInRules, ...Local]>;
|
|
31
27
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Effect, type FileSystem } from "effect";
|
|
3
|
+
import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
|
|
4
|
+
import { LEGACY_BASELINE } from "../baseline/legacy.ts";
|
|
5
|
+
import type { ResolvedConfig } from "../config/load.ts";
|
|
6
|
+
import { validOrFail } from "../decoded.ts";
|
|
7
|
+
import { SetupFailure } from "../failure.ts";
|
|
8
|
+
import { readOptionalText } from "../inventory/filesystem.ts";
|
|
9
|
+
|
|
10
|
+
export interface BaselineFile {
|
|
11
|
+
readonly raw: string | undefined;
|
|
12
|
+
readonly entries: ReadonlyArray<BaselineEntry>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const readInput = (root: string, path: string): Effect.Effect<string | undefined, SetupFailure, FileSystem.FileSystem> =>
|
|
16
|
+
Effect.mapError(readOptionalText(join(root, path)), (failure) => new SetupFailure({ message: failure.message }));
|
|
17
|
+
|
|
18
|
+
export const readBaseline = (config: ResolvedConfig): Effect.Effect<BaselineFile, SetupFailure, FileSystem.FileSystem> =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const raw = yield* readInput(config.root, config.baseline);
|
|
21
|
+
if (raw === undefined && config.baseline !== LEGACY_BASELINE && (yield* readInput(config.root, LEGACY_BASELINE)) !== undefined) {
|
|
22
|
+
return yield* new SetupFailure({
|
|
23
|
+
message: `${LEGACY_BASELINE} holds a baseline in the earlier JSON format. Run \`quality baseline migrate\` to move it to ${config.baseline}.`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return { entries: yield* validOrFail(config.baseline, yield* Effect.promise(() => decodeBaseline(raw))), raw };
|
|
27
|
+
});
|
package/src/engine/evaluate.ts
CHANGED
|
@@ -9,24 +9,26 @@ export interface Outcome {
|
|
|
9
9
|
readonly warnings: ReadonlyArray<Violation>;
|
|
10
10
|
readonly regressions: ReadonlyArray<Regression>;
|
|
11
11
|
readonly staleBaseline: ReadonlyArray<StaleBaselineEntry>;
|
|
12
|
+
readonly looseBaseline: ReadonlyArray<StaleBaselineEntry>;
|
|
12
13
|
readonly staleRegistry: ReadonlyArray<StaleRegistryEntry>;
|
|
13
14
|
readonly baselined: number;
|
|
14
15
|
readonly registered: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/** Registered exceptions apply first; the baseline then covers what is left, per rule and file. */
|
|
18
18
|
export const evaluate = (
|
|
19
19
|
violations: ReadonlyArray<Violation>,
|
|
20
20
|
registry: ReadonlyArray<RegistryEntry>,
|
|
21
21
|
baseline: ReadonlyArray<BaselineEntry>,
|
|
22
22
|
levels: ReadonlyMap<string, Level>,
|
|
23
|
+
unregistrable: ReadonlySet<string>,
|
|
23
24
|
): Outcome => {
|
|
24
|
-
const registered = applyRegistry(violations, registry, levels);
|
|
25
|
+
const registered = applyRegistry(violations, registry, levels, unregistrable);
|
|
25
26
|
const baselined = applyBaseline(registered.kept, baseline, levels);
|
|
26
27
|
const kept = [...baselined.kept].sort(byLocation);
|
|
27
28
|
return {
|
|
28
29
|
baselined: baselined.baselined,
|
|
29
30
|
errors: kept.filter((violation) => violation.level === "error"),
|
|
31
|
+
looseBaseline: baselined.loose,
|
|
30
32
|
regressions: baselined.regressions,
|
|
31
33
|
registered: registered.registered,
|
|
32
34
|
staleBaseline: baselined.stale,
|
package/src/engine/session.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { Effect, type FileSystem } from "effect";
|
|
3
|
-
import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
|
|
4
3
|
import { loadConfig, type ResolvedConfig } from "../config/load.ts";
|
|
5
4
|
import { validOrFail } from "../decoded.ts";
|
|
6
5
|
import { decodeRegistry, type RegistryEntry } from "../exceptions/registry.ts";
|
|
7
6
|
import { SetupFailure } from "../failure.ts";
|
|
8
7
|
import { collectInventory, type Inventory } from "../inventory/collect.ts";
|
|
9
8
|
import { type FilesystemFailure, readOptionalText } from "../inventory/filesystem.ts";
|
|
9
|
+
import { type BaselineFile, readBaseline, readInput } from "./baseline-file.ts";
|
|
10
10
|
import { runRules } from "./run-rules.ts";
|
|
11
11
|
import type { Violation } from "./violation.ts";
|
|
12
12
|
|
|
@@ -15,26 +15,28 @@ export interface Session {
|
|
|
15
15
|
readonly inventory: Inventory;
|
|
16
16
|
readonly violations: ReadonlyArray<Violation>;
|
|
17
17
|
readonly registry: ReadonlyArray<RegistryEntry>;
|
|
18
|
-
readonly baseline:
|
|
18
|
+
readonly baseline: BaselineFile;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const setup = <Value, Requirements>(
|
|
22
22
|
effect: Effect.Effect<Value, FilesystemFailure, Requirements>,
|
|
23
23
|
): Effect.Effect<Value, SetupFailure, Requirements> => Effect.mapError(effect, (failure) => new SetupFailure({ message: failure.message }));
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
export const scan = (config: ResolvedConfig): Effect.Effect<Pick<Session, "inventory" | "violations">, SetupFailure, FileSystem.FileSystem> =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const inventory = yield* setup(collectInventory(config.root, config));
|
|
28
|
+
const services = yield* Effect.context<FileSystem.FileSystem>();
|
|
29
|
+
const readText = (path: string): Promise<string | undefined> => Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
|
|
30
|
+
const violations = yield* runRules(config.active, { files: inventory.files, readText, root: config.root, sources: inventory.sources });
|
|
31
|
+
return { inventory, violations };
|
|
32
|
+
});
|
|
27
33
|
|
|
28
34
|
export const openSession = (cwd: string, configPath: string | undefined): Effect.Effect<Session, SetupFailure, FileSystem.FileSystem> =>
|
|
29
35
|
Effect.gen(function* () {
|
|
30
36
|
const config = yield* loadConfig(cwd, configPath);
|
|
31
|
-
const inventory = yield* setup(collectInventory(config.root, config));
|
|
32
37
|
const registryRaw = yield* readInput(config.root, config.registry);
|
|
33
38
|
const registry = yield* validOrFail(config.registry, yield* Effect.promise(() => decodeRegistry(registryRaw)));
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const readText = (path: string): Promise<string | undefined> => Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
|
|
38
|
-
const violations = yield* runRules(config.active, { files: inventory.files, readText, root: config.root, sources: inventory.sources });
|
|
39
|
-
return { baseline: { entries: baseline, raw: baselineRaw }, config, inventory, registry, violations };
|
|
39
|
+
const baseline = yield* readBaseline(config);
|
|
40
|
+
const { inventory, violations } = yield* scan(config);
|
|
41
|
+
return { baseline, config, inventory, registry, violations };
|
|
40
42
|
});
|
|
@@ -33,6 +33,8 @@ export interface RegistryCheck {
|
|
|
33
33
|
const covers = (entry: RegistryEntry, violation: Violation): boolean =>
|
|
34
34
|
entry.rule === violation.rule && entry.file === violation.file && (entry.subject === undefined || entry.subject === violation.subject);
|
|
35
35
|
|
|
36
|
+
const UNREGISTRABLE = "names a rule that takes no exceptions. Fix the code, or baseline the violations while the repository adopts the rule";
|
|
37
|
+
|
|
36
38
|
const coveringEntry = (entries: ReadonlyArray<RegistryEntry>, violation: Violation): RegistryEntry | undefined => {
|
|
37
39
|
const matching = entries.filter((entry) => covers(entry, violation));
|
|
38
40
|
return matching.find((entry) => entry.subject !== undefined) ?? matching[0];
|
|
@@ -42,10 +44,12 @@ export const applyRegistry = (
|
|
|
42
44
|
violations: ReadonlyArray<Violation>,
|
|
43
45
|
entries: ReadonlyArray<RegistryEntry>,
|
|
44
46
|
levels: ReadonlyMap<string, Level>,
|
|
47
|
+
unregistrable: ReadonlySet<string>,
|
|
45
48
|
): RegistryCheck => {
|
|
49
|
+
const usable = entries.filter((entry) => !unregistrable.has(entry.rule));
|
|
46
50
|
const used = new Set<RegistryEntry>();
|
|
47
51
|
const kept = violations.filter((violation) => {
|
|
48
|
-
const entry = coveringEntry(
|
|
52
|
+
const entry = coveringEntry(usable, violation);
|
|
49
53
|
if (entry !== undefined) {
|
|
50
54
|
used.add(entry);
|
|
51
55
|
}
|
|
@@ -53,6 +57,9 @@ export const applyRegistry = (
|
|
|
53
57
|
});
|
|
54
58
|
const stale = entries
|
|
55
59
|
.filter((entry) => !used.has(entry))
|
|
56
|
-
.map((entry) => ({
|
|
60
|
+
.map((entry) => ({
|
|
61
|
+
entry,
|
|
62
|
+
problem: unregistrable.has(entry.rule) ? UNREGISTRABLE : unusedEntryProblem(levels, entry.rule, "matches no violation"),
|
|
63
|
+
}));
|
|
57
64
|
return { kept, registered: violations.length - kept.length, stale };
|
|
58
65
|
};
|
package/src/failure.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Data, Runtime } from "effect";
|
|
2
2
|
|
|
3
|
-
/** The check could not run: invalid configuration, unreadable input or a crashed rule. Exits 2. */
|
|
4
3
|
export class SetupFailure extends Data.TaggedError("SetupFailure")<{ readonly message: string }> {
|
|
5
4
|
override readonly [Runtime.errorReported] = false;
|
|
6
5
|
override readonly [Runtime.errorExitCode] = 2;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
|
-
/** The check ran and the repository fails the gate. The report is already printed. Exits 1. */
|
|
10
8
|
export class GateFailed extends Data.TaggedError("GateFailed") {
|
|
11
9
|
override readonly [Runtime.errorReported] = false;
|
|
12
10
|
override readonly [Runtime.errorExitCode] = 1;
|
package/src/git/base.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Console, Effect } from "effect";
|
|
2
|
+
import { SetupFailure } from "../failure.ts";
|
|
3
|
+
import { type Git, git } from "./command.ts";
|
|
4
|
+
|
|
5
|
+
export interface Base {
|
|
6
|
+
readonly ref: string;
|
|
7
|
+
readonly commit: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const FALLBACK_BRANCHES = ["origin/main", "origin/master"];
|
|
11
|
+
|
|
12
|
+
const fail = (message: string) => Effect.fail(new SetupFailure({ message }));
|
|
13
|
+
|
|
14
|
+
const isCommit = (root: string, ref: string): Effect.Effect<boolean, SetupFailure, Git> =>
|
|
15
|
+
Effect.map(git(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]), (result) => result.code === 0);
|
|
16
|
+
|
|
17
|
+
const defaultBranch = (root: string): Effect.Effect<string, SetupFailure, Git> =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const head = yield* git(root, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
20
|
+
if (head.code === 0) {
|
|
21
|
+
return head.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
for (const candidate of FALLBACK_BRANCHES) {
|
|
24
|
+
if (yield* isCommit(root, candidate)) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return yield* fail(
|
|
29
|
+
"cannot find the default branch: origin/HEAD, origin/main and origin/master are missing. Pass --against <ref>, or run `git remote set-head origin --auto`.",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const isShallow = (root: string): Effect.Effect<boolean, SetupFailure, Git> =>
|
|
34
|
+
Effect.map(git(root, ["rev-parse", "--is-shallow-repository"]), (result) => result.stdout.trim() === "true");
|
|
35
|
+
|
|
36
|
+
const mergeBase = (root: string, ref: string): Effect.Effect<string | undefined, SetupFailure, Git> =>
|
|
37
|
+
Effect.map(git(root, ["merge-base", "HEAD", ref]), (result) => (result.code === 0 ? result.stdout.trim() : undefined));
|
|
38
|
+
|
|
39
|
+
const remoteOf = (root: string, ref: string): Effect.Effect<ReadonlyArray<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
|
+
const missingMergeBase = (root: string, ref: string): Effect.Effect<never, SetupFailure, Git> =>
|
|
54
|
+
Effect.flatMap(isShallow(root), (shallow) =>
|
|
55
|
+
fail(
|
|
56
|
+
shallow
|
|
57
|
+
? `the clone is shallow and does not reach the merge base of HEAD and ${ref}. Fetch the history first: \`git fetch --unshallow\`, or fetch-depth: 0 in actions/checkout.`
|
|
58
|
+
: `HEAD and ${ref} share no history.`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const reachMergeBase = (root: string, ref: string): Effect.Effect<string, SetupFailure, Git> =>
|
|
63
|
+
Effect.gen(function* () {
|
|
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
|
+
});
|
|
77
|
+
|
|
78
|
+
export const resolveBase = (root: string, against: string | undefined): Effect.Effect<Base, SetupFailure, Git> =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const inside = yield* git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
81
|
+
if (inside.code !== 0 || inside.stdout.trim() !== "true") {
|
|
82
|
+
return yield* fail(`${root} is not in a git work tree; the baseline is compared with its version in git history.`);
|
|
83
|
+
}
|
|
84
|
+
const ref = against ?? (yield* defaultBranch(root));
|
|
85
|
+
if (!(yield* isCommit(root, ref))) {
|
|
86
|
+
return yield* fail(`${ref} names no commit in this clone. Fetch it first, or pass another --against <ref>.`);
|
|
87
|
+
}
|
|
88
|
+
return { commit: yield* reachMergeBase(root, ref), ref };
|
|
89
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
3
|
+
import { SetupFailure } from "../failure.ts";
|
|
4
|
+
|
|
5
|
+
export interface GitResult {
|
|
6
|
+
readonly code: number;
|
|
7
|
+
readonly stdout: string;
|
|
8
|
+
readonly stderr: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Git = ChildProcessSpawner.ChildProcessSpawner;
|
|
12
|
+
|
|
13
|
+
export const git = (cwd: string, args: ReadonlyArray<string>, env?: Readonly<Record<string, string>>): Effect.Effect<GitResult, SetupFailure, Git> =>
|
|
14
|
+
Effect.scoped(
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
17
|
+
const child = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd, env, extendEnv: true, stdin: "ignore" }));
|
|
18
|
+
const [stdout, stderr, code] = yield* Effect.all(
|
|
19
|
+
[Stream.mkString(Stream.decodeText(child.stdout)), Stream.mkString(Stream.decodeText(child.stderr)), child.exitCode],
|
|
20
|
+
{ concurrency: "unbounded" },
|
|
21
|
+
);
|
|
22
|
+
return { code: Number(code), stderr, stdout };
|
|
23
|
+
}),
|
|
24
|
+
).pipe(Effect.mapError((error) => new SetupFailure({ message: `cannot run git: ${error.message}` })));
|
|
25
|
+
|
|
26
|
+
export const gitOrFail = (cwd: string, args: ReadonlyArray<string>): Effect.Effect<string, SetupFailure, Git> =>
|
|
27
|
+
Effect.flatMap(git(cwd, args), (result) =>
|
|
28
|
+
result.code === 0
|
|
29
|
+
? Effect.succeed(result.stdout)
|
|
30
|
+
: Effect.fail(new SetupFailure({ message: `git ${args[0]} failed: ${result.stderr.trim() || `exit code ${result.code}`}` })),
|
|
31
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { SetupFailure } from "../failure.ts";
|
|
3
|
+
import { type Git, git, gitOrFail } from "./command.ts";
|
|
4
|
+
|
|
5
|
+
export interface Changes {
|
|
6
|
+
readonly files: ReadonlySet<string>;
|
|
7
|
+
readonly moves: ReadonlyMap<string, string>;
|
|
8
|
+
}
|
|
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
|
+
const parse = (output: string): Changes => {
|
|
18
|
+
const fields = output.split("\0");
|
|
19
|
+
const files = new Set<string>();
|
|
20
|
+
const moves = new Map<string, string>();
|
|
21
|
+
for (let index = 0; index < fields.length - 1; ) {
|
|
22
|
+
const status = fields[index] ?? "";
|
|
23
|
+
const paths = fields.slice(index + 1, index + (/^[RC]/.test(status) ? 3 : 2));
|
|
24
|
+
for (const path of paths) {
|
|
25
|
+
files.add(path);
|
|
26
|
+
}
|
|
27
|
+
const [from, to] = paths;
|
|
28
|
+
if (status.startsWith("R") && from !== undefined && to !== undefined) {
|
|
29
|
+
moves.set(from, to);
|
|
30
|
+
}
|
|
31
|
+
index += paths.length + 1;
|
|
32
|
+
}
|
|
33
|
+
return { files, moves };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Paths are relative to the root, and only tracked files count: a move is seen once both of its sides are staged.
|
|
37
|
+
export const changesSince = (root: string, revision: ReadonlyArray<string>): Effect.Effect<Changes, SetupFailure, Git> =>
|
|
38
|
+
Effect.map(
|
|
39
|
+
gitOrFail(root, ["diff", "--no-color", "--no-ext-diff", "--find-renames", "--name-status", "-z", "--relative", ...revision, "--"]),
|
|
40
|
+
parse,
|
|
41
|
+
);
|
|
@@ -39,6 +39,5 @@ const matcherVerdict = (matcher: IgnoreMatcher, boundary: string | undefined, pa
|
|
|
39
39
|
return result.unignored ? "kept" : "unknown";
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// The deepest ignore file with an opinion wins.
|
|
43
42
|
export const verdictFor = (scope: IgnoreScope, path: string, directory: boolean): Verdict =>
|
|
44
43
|
scope.matchers.map((matcher) => matcherVerdict(matcher, scope.boundary, path, directory)).findLast((verdict) => verdict !== "unknown") ?? "unknown";
|
package/src/inventory/walk.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const ignoreScopeAt = (
|
|
|
13
13
|
contents === undefined || contents === "" ? inherited : withIgnoreFile(inherited, dir, contents),
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// The walk itself never visits the directories between the root and a source, so their ignore files are read here.
|
|
17
17
|
export const scopeAbove = (root: string, dir: string): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> => {
|
|
18
18
|
const parts = relative(root, dir)
|
|
19
19
|
.split(sep)
|