@shivaedev/quality 0.18.0 → 0.20.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 +57 -0
- package/README.md +136 -544
- package/biome/declarations.json +0 -45
- package/biome/plugins/constant-names.grit +1 -1
- package/biome/plugins/effect-fn-functions.grit +1 -1
- package/biome/preset.json +16 -9
- package/dist/biome/nativeEffectCall.d.ts +4 -0
- package/dist/biome/nativeEffectCall.d.ts.map +1 -0
- package/dist/biome/nativeEffectCall.js +34 -0
- package/dist/biome/nativeEffectCall.js.map +1 -0
- package/dist/biome/recognizeDiagnostics.d.ts +3 -0
- package/dist/biome/recognizeDiagnostics.d.ts.map +1 -0
- package/dist/biome/recognizeDiagnostics.js +58 -0
- package/dist/biome/recognizeDiagnostics.js.map +1 -0
- package/dist/biome/report.d.ts +10 -0
- package/dist/biome/report.d.ts.map +1 -1
- package/dist/biome/report.js +23 -5
- package/dist/biome/report.js.map +1 -1
- package/dist/biome/syntax.d.ts +12 -0
- package/dist/biome/syntax.d.ts.map +1 -0
- package/dist/biome/syntax.js +29 -0
- package/dist/biome/syntax.js.map +1 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +6 -6
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +1 -1
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/run-main.d.ts +1 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js.map +1 -1
- package/dist/config/decode.d.ts +3 -7
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +5 -3
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +11 -7
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +8 -12
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +10 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +4 -4
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js.map +1 -1
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +1 -1
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.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 +1 -1
- package/dist/git/command.js.map +1 -1
- package/dist/hooks/install.d.ts +1 -1
- package/dist/hooks/install.d.ts.map +1 -1
- package/dist/hooks/location.d.ts +1 -1
- package/dist/hooks/location.d.ts.map +1 -1
- package/dist/hooks/preCommit.d.ts +3 -3
- package/dist/hooks/preCommit.d.ts.map +1 -1
- package/dist/hooks/preCommit.js +25 -35
- package/dist/hooks/preCommit.js.map +1 -1
- package/dist/hooks/uninstall.d.ts +1 -1
- package/dist/hooks/uninstall.d.ts.map +1 -1
- package/dist/imports/browserGlobals.d.ts +3 -0
- package/dist/imports/browserGlobals.d.ts.map +1 -0
- package/dist/imports/browserGlobals.js +125 -0
- package/dist/imports/browserGlobals.js.map +1 -0
- package/dist/imports/browserNames.d.ts +2 -0
- package/dist/imports/browserNames.d.ts.map +1 -0
- package/dist/imports/browserNames.js +78 -0
- package/dist/imports/browserNames.js.map +1 -0
- package/dist/imports/fences/check.d.ts +4 -0
- package/dist/imports/fences/check.d.ts.map +1 -0
- package/dist/imports/fences/check.js +10 -0
- package/dist/imports/fences/check.js.map +1 -0
- package/dist/imports/fences/declarations.d.ts +5 -0
- package/dist/imports/fences/declarations.d.ts.map +1 -0
- package/dist/imports/fences/declarations.js +29 -0
- package/dist/imports/fences/declarations.js.map +1 -0
- package/dist/imports/fences/discover.d.ts +5 -0
- package/dist/imports/fences/discover.d.ts.map +1 -0
- package/dist/imports/fences/discover.js +41 -0
- package/dist/imports/fences/discover.js.map +1 -0
- package/dist/imports/graph.js +2 -2
- package/dist/imports/graph.js.map +1 -1
- package/dist/imports/isTooling.d.ts +2 -0
- package/dist/imports/isTooling.d.ts.map +1 -0
- package/dist/imports/isTooling.js +9 -0
- package/dist/imports/isTooling.js.map +1 -0
- package/dist/imports/projects.js.map +1 -1
- package/dist/imports/runtime.d.ts +5 -0
- package/dist/imports/runtime.d.ts.map +1 -0
- package/dist/imports/runtime.js +57 -0
- package/dist/imports/runtime.js.map +1 -0
- package/dist/imports/workspace.d.ts +1 -1
- package/dist/imports/workspace.d.ts.map +1 -1
- package/dist/imports/workspace.js +5 -1
- package/dist/imports/workspace.js.map +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +8 -1
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/trackedPaths.d.ts +3 -0
- package/dist/inventory/trackedPaths.d.ts.map +1 -0
- package/dist/inventory/trackedPaths.js +26 -0
- package/dist/inventory/trackedPaths.js.map +1 -0
- package/dist/inventory/walk.d.ts +4 -4
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +32 -12
- package/dist/inventory/walk.js.map +1 -1
- package/dist/rules/built-in.d.ts +2 -3
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +6 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/max-per-file.js +1 -1
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/no-environment-pragma.js +1 -1
- package/dist/rules/comments/no-environment-pragma.js.map +1 -1
- package/dist/rules/files/folderNames.js +1 -1
- package/dist/rules/files/folderNames.js.map +1 -1
- package/dist/rules/files/otherNames.d.ts.map +1 -1
- package/dist/rules/files/otherNames.js +1 -10
- package/dist/rules/files/otherNames.js.map +1 -1
- package/dist/rules/imports/bundleInputs.d.ts +7 -0
- package/dist/rules/imports/bundleInputs.d.ts.map +1 -0
- package/dist/rules/imports/bundleInputs.js +77 -0
- package/dist/rules/imports/bundleInputs.js.map +1 -0
- package/dist/rules/imports/fences.d.ts +1 -2
- package/dist/rules/imports/fences.d.ts.map +1 -1
- package/dist/rules/imports/fences.js +4 -14
- package/dist/rules/imports/fences.js.map +1 -1
- package/dist/rules/imports/manifest.d.ts +2 -0
- package/dist/rules/imports/manifest.d.ts.map +1 -0
- package/dist/rules/imports/manifest.js +92 -0
- package/dist/rules/imports/manifest.js.map +1 -0
- package/dist/rules/imports/noTestCode.d.ts +2 -0
- package/dist/rules/imports/noTestCode.d.ts.map +1 -0
- package/dist/rules/imports/noTestCode.js +17 -0
- package/dist/rules/imports/noTestCode.js.map +1 -0
- package/dist/rules/imports/runtime.d.ts +2 -0
- package/dist/rules/imports/runtime.d.ts.map +1 -0
- package/dist/rules/imports/runtime.js +45 -0
- package/dist/rules/imports/runtime.js.map +1 -0
- package/dist/rules/max-lines.d.ts +0 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +3 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +21 -4
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/biomeRecommended.js +2 -2
- package/dist/rules/suppressions/biomeRecommended.js.map +1 -1
- package/dist/rules/suppressions/no-inline.js +1 -1
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/standard-schema.js +10 -2
- package/dist/standard-schema.js.map +1 -1
- package/dist/testProjects.d.ts +3 -0
- package/dist/testProjects.d.ts.map +1 -0
- package/dist/testProjects.js +25 -0
- package/dist/testProjects.js.map +1 -0
- package/docs/debt-and-hooks.md +90 -0
- package/docs/modules.md +253 -0
- package/docs/naming.md +126 -0
- package/docs/north-star.md +61 -0
- package/docs/roadmap.md +37 -0
- package/docs/rules.md +310 -0
- package/docs/tooling.md +105 -0
- package/package.json +17 -14
- package/src/biome/nativeEffectCall.ts +45 -0
- package/src/biome/recognizeDiagnostics.ts +67 -0
- package/src/biome/report.ts +24 -6
- package/src/biome/syntax.ts +36 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/baseline.ts +3 -3
- package/src/cli/fix.ts +13 -13
- package/src/cli/run-main.ts +1 -1
- package/src/config/decode.ts +7 -3
- package/src/config/load.ts +15 -9
- package/src/config/resolve.ts +10 -15
- package/src/config.ts +8 -15
- package/src/decoded.ts +3 -3
- package/src/engine/session.ts +3 -3
- package/src/engine/violation.ts +2 -2
- package/src/git/command.ts +1 -1
- package/src/hooks/preCommit.ts +31 -40
- package/src/imports/browserGlobals.ts +142 -0
- package/src/imports/browserNames.ts +80 -0
- package/src/imports/fences/check.ts +12 -0
- package/src/imports/fences/declarations.ts +38 -0
- package/src/imports/fences/discover.ts +37 -0
- package/src/imports/graph.ts +2 -2
- package/src/imports/isTooling.ts +11 -0
- package/src/imports/projects.ts +2 -2
- package/src/imports/runtime.ts +63 -0
- package/src/imports/workspace.ts +7 -2
- package/src/inventory/collect.ts +8 -1
- package/src/inventory/trackedPaths.ts +28 -0
- package/src/inventory/walk.ts +65 -36
- package/src/rules/built-in.ts +6 -0
- package/src/rules/comments/max-per-file.ts +1 -1
- package/src/rules/comments/no-environment-pragma.ts +1 -1
- package/src/rules/files/folderNames.ts +1 -1
- package/src/rules/files/otherNames.ts +1 -10
- package/src/rules/imports/bundleInputs.ts +94 -0
- package/src/rules/imports/fences.ts +4 -16
- package/src/rules/imports/manifest.ts +108 -0
- package/src/rules/imports/noTestCode.ts +18 -0
- package/src/rules/imports/runtime.ts +47 -0
- package/src/rules/max-lines.ts +4 -10
- package/src/rules/suppressions/biome-overrides.ts +31 -4
- package/src/rules/suppressions/biomeRecommended.ts +2 -2
- package/src/rules/suppressions/no-inline.ts +1 -1
- package/src/standard-schema.ts +11 -3
- package/src/testProjects.ts +30 -0
- package/dist/vitest.d.ts +0 -6
- package/dist/vitest.d.ts.map +0 -1
- package/dist/vitest.js +0 -55
- package/dist/vitest.js.map +0 -1
- package/src/vitest.ts +0 -60
package/src/biome/report.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { relative, sep } from "node:path";
|
|
1
2
|
import { Schema } from "effect";
|
|
3
|
+
import { ignoredTrackedPaths } from "#inventory/trackedPaths.ts";
|
|
4
|
+
import { recognizeDiagnostics } from "./recognizeDiagnostics.ts";
|
|
2
5
|
import { type BiomeRun, runBiome } from "./run.ts";
|
|
3
6
|
|
|
4
|
-
const Position = Schema.Struct({ line: Schema.Int });
|
|
7
|
+
const Position = Schema.Struct({ column: Schema.optionalKey(Schema.Int), line: Schema.Int });
|
|
5
8
|
|
|
6
9
|
const Diagnostic = Schema.Struct({
|
|
7
10
|
category: Schema.optionalKey(Schema.String),
|
|
8
|
-
location: Schema.Struct({ path: Schema.optionalKey(Schema.String), start: Schema.optionalKey(Position) }),
|
|
11
|
+
location: Schema.Struct({ end: Schema.optionalKey(Position), path: Schema.optionalKey(Schema.String), start: Schema.optionalKey(Position) }),
|
|
9
12
|
message: Schema.String,
|
|
10
13
|
severity: Schema.String,
|
|
11
14
|
});
|
|
@@ -21,11 +24,12 @@ export type Report = typeof Report.Type;
|
|
|
21
24
|
|
|
22
25
|
const decodeReport = Schema.decodeUnknownOption(Schema.fromJsonString(Report));
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
new Error(`biome ${args.join(" ")} exited ${run.code} without a report:\n${[run.stdout, run.stderr].join("\n").trim()}`);
|
|
27
|
+
function failure(args: readonly string[], run: BiomeRun): Error {
|
|
28
|
+
return new Error(`biome ${args.join(" ")} exited ${run.code} without a report:\n${[run.stdout, run.stderr].join("\n").trim()}`);
|
|
29
|
+
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
const command = [...args, "--reporter=json", "--max-diagnostics=none",
|
|
31
|
+
async function reportFor(root: string, args: readonly string[], paths: readonly string[]): Promise<Report> {
|
|
32
|
+
const command = [...args, "--reporter=json", "--max-diagnostics=none", ...paths];
|
|
29
33
|
const run = await runBiome(root, command);
|
|
30
34
|
const report = decodeReport(run.stdout.trim());
|
|
31
35
|
if (report._tag === "None") {
|
|
@@ -33,3 +37,17 @@ export async function biomeReport(root: string, args: readonly string[]): Promis
|
|
|
33
37
|
}
|
|
34
38
|
return report.value;
|
|
35
39
|
}
|
|
40
|
+
|
|
41
|
+
export async function biomeReport(root: string, args: readonly string[]): Promise<Report> {
|
|
42
|
+
const [report, tracked] = await Promise.all([reportFor(root, args, ["."]), ignoredTrackedPaths(root)]);
|
|
43
|
+
const paths = tracked
|
|
44
|
+
.map((path) => relative(root, path))
|
|
45
|
+
.filter((path) => !path.split(sep).some((part) => part === ".git" || part === "node_modules"));
|
|
46
|
+
const extra = paths.length === 0 ? undefined : await reportFor(root, [...args, "--vcs-enabled=false"], paths);
|
|
47
|
+
const diagnostics = [...report.diagnostics, ...(extra?.diagnostics ?? [])];
|
|
48
|
+
const unique = [...new Map(diagnostics.map((diagnostic) => [JSON.stringify(diagnostic), diagnostic])).values()];
|
|
49
|
+
return {
|
|
50
|
+
diagnostics: recognizeDiagnostics(root, unique),
|
|
51
|
+
summary: { changed: report.summary.changed + (extra?.summary.changed ?? 0) },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
export interface Syntax {
|
|
5
|
+
readonly checker: ts.TypeChecker;
|
|
6
|
+
readonly source: ts.SourceFile;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function syntaxOf(path: string): Syntax {
|
|
10
|
+
const source = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true);
|
|
11
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, noResolve: true };
|
|
12
|
+
const host = ts.createCompilerHost(options);
|
|
13
|
+
host.getSourceFile = (file) => (file === path ? source : undefined);
|
|
14
|
+
const program = ts.createProgram([path], options, host);
|
|
15
|
+
return { checker: program.getTypeChecker(), source };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function offsetOf(source: ts.SourceFile, position: { readonly line: number; readonly column?: number }): number | undefined {
|
|
19
|
+
const start = source.getLineStarts()[position.line - 1];
|
|
20
|
+
if (start === undefined || position.column === undefined || position.column < 1) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const line = source.text.slice(start, source.getLineStarts()[position.line] ?? source.text.length);
|
|
24
|
+
const characters = [...line];
|
|
25
|
+
return position.column <= characters.length + 1 ? start + characters.slice(0, position.column - 1).join("").length : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function nodeAt(source: ts.SourceFile, start: number, end: number): ts.Node | undefined {
|
|
29
|
+
function visit(node: ts.Node): ts.Node | undefined {
|
|
30
|
+
if (node.getStart(source) > start || node.end < end) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return ts.forEachChild(node, visit) ?? (node.getStart(source) === start && node.end === end ? node : undefined);
|
|
34
|
+
}
|
|
35
|
+
return visit(source);
|
|
36
|
+
}
|
package/src/cli/args.ts
CHANGED
|
@@ -37,9 +37,9 @@ baseline migrate Move a baseline from the earlier JSON format (--from, quality/
|
|
|
37
37
|
hooks install Install the git pre-commit hook, shared by every worktree, that runs hooks pre-commit. Keeps a hook that is not quality's
|
|
38
38
|
unless --force replaces it.
|
|
39
39
|
hooks uninstall Remove quality's pre-commit hook. Keeps a hook that is not quality's.
|
|
40
|
-
hooks pre-commit What the hook runs:
|
|
40
|
+
hooks pre-commit What the hook runs: lint, then package.json scripts typecheck and quality:commit when present.
|
|
41
41
|
|
|
42
|
-
--config <file> Config file; its directory is the repository root.
|
|
42
|
+
--config <file> Config file; its directory is the repository root. Without this option, uses ./quality.config.ts when present, otherwise defaults.
|
|
43
43
|
--against <ref> The branch the work merges into, whose merge base prune follows moves from. Defaults to origin/HEAD, then origin/main, then origin/master.
|
|
44
44
|
Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
|
|
45
45
|
|
package/src/cli/baseline.ts
CHANGED
|
@@ -46,11 +46,11 @@ const changes = (pruned: Pruned): string => {
|
|
|
46
46
|
return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
export const shrinkBaseline = <
|
|
49
|
+
export const shrinkBaseline = <TRequirements>(
|
|
50
50
|
cwd: string,
|
|
51
51
|
config: string | undefined,
|
|
52
|
-
scopeOf: (session: Session) => Effect.Effect<Scope, SetupFailure,
|
|
53
|
-
): Effect.Effect<void, SetupFailure, FileSystem.FileSystem |
|
|
52
|
+
scopeOf: (session: Session) => Effect.Effect<Scope, SetupFailure, TRequirements>,
|
|
53
|
+
): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | TRequirements> =>
|
|
54
54
|
Effect.gen(function* () {
|
|
55
55
|
const session = yield* openSession(cwd, config);
|
|
56
56
|
if (session.baseline.raw === undefined) {
|
package/src/cli/fix.ts
CHANGED
|
@@ -33,21 +33,21 @@ export function snapshotOf(root: string): Effect.Effect<ReadonlyMap<string, stri
|
|
|
33
33
|
}).pipe(Effect.mapError((failure) => new SetupFailure({ message: failure.message })));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface Pass<
|
|
36
|
+
export interface Pass<TFailure, TRequirements> {
|
|
37
37
|
readonly label: string;
|
|
38
|
-
readonly run: Effect.Effect<number,
|
|
38
|
+
readonly run: Effect.Effect<number, TFailure, TRequirements>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export interface Settling<
|
|
42
|
-
readonly passes:
|
|
43
|
-
readonly snapshot: Effect.Effect<ReadonlyMap<string, string>,
|
|
41
|
+
export interface Settling<TFailure, TRequirements> {
|
|
42
|
+
readonly passes: readonly Pass<TFailure, TRequirements>[];
|
|
43
|
+
readonly snapshot: Effect.Effect<ReadonlyMap<string, string>, TFailure, TRequirements>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function changedBy(root: string, args: readonly string[]): Effect.Effect<number, SetupFailure> {
|
|
47
47
|
return Effect.map(biome(root, args), (report) => report.summary.changed);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function biomePasses(root: string):
|
|
50
|
+
function biomePasses(root: string): readonly Pass<SetupFailure, never>[] {
|
|
51
51
|
return [
|
|
52
52
|
{
|
|
53
53
|
label: "with fixes",
|
|
@@ -57,11 +57,11 @@ function biomePasses(root: string): ReadonlyArray<Pass<SetupFailure, never>> {
|
|
|
57
57
|
];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function round<
|
|
61
|
-
passes:
|
|
60
|
+
function round<TFailure, TRequirements>(
|
|
61
|
+
passes: readonly Pass<TFailure, TRequirements>[],
|
|
62
62
|
index: number,
|
|
63
|
-
afterEachPass: Effect.Effect<void,
|
|
64
|
-
): Effect.Effect<number,
|
|
63
|
+
afterEachPass: Effect.Effect<void, TFailure, TRequirements>,
|
|
64
|
+
): Effect.Effect<number, TFailure, TRequirements> {
|
|
65
65
|
return Effect.gen(function* () {
|
|
66
66
|
let total = 0;
|
|
67
67
|
const parts: string[] = [];
|
|
@@ -91,14 +91,14 @@ function unsettled(changed: ReadonlySet<string>): SetupFailure {
|
|
|
91
91
|
"Run `biome check --write --unsafe --verbose` to name them.",
|
|
92
92
|
];
|
|
93
93
|
return new SetupFailure({
|
|
94
|
-
message: [...named, "Two fixes probably undo each other there.
|
|
94
|
+
message: [...named, "Two fixes probably undo each other there. Repair the conflicting code by hand so both checks pass."].join("\n"),
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export function settle<
|
|
98
|
+
export function settle<TFailure, TRequirements>({
|
|
99
99
|
passes,
|
|
100
100
|
snapshot,
|
|
101
|
-
}: Settling<
|
|
101
|
+
}: Settling<TFailure, TRequirements>): Effect.Effect<void, TFailure | SetupFailure, TRequirements> {
|
|
102
102
|
return Effect.gen(function* () {
|
|
103
103
|
for (let index = 1; index < MAX_FIX_ROUNDS; index += 1) {
|
|
104
104
|
if ((yield* round(passes, index, Effect.void)) === 0) {
|
package/src/cli/run-main.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function quietOnClosedPipe(stream: EventEmitter): void {
|
|
|
47
47
|
|
|
48
48
|
const services = NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
|
|
49
49
|
|
|
50
|
-
export const runMain = <
|
|
50
|
+
export const runMain = <TValue, TError>(program: Effect.Effect<TValue, TError, FileSystem.FileSystem | Git>): void => {
|
|
51
51
|
quietOnClosedPipe(process.stdout);
|
|
52
52
|
quietOnClosedPipe(process.stderr);
|
|
53
53
|
const explained = Effect.tapCause(program, (cause) => (Cause.hasInterruptsOnly(cause) ? Effect.void : explain(cause)));
|
package/src/config/decode.ts
CHANGED
|
@@ -12,7 +12,7 @@ const isRule = (value: unknown): value is Rule =>
|
|
|
12
12
|
&& "configure" in value
|
|
13
13
|
&& typeof value.configure === "function";
|
|
14
14
|
|
|
15
|
-
const Level = Schema.
|
|
15
|
+
const Level = Schema.Literal("error");
|
|
16
16
|
|
|
17
17
|
const Setting = Schema.Union([Level, Schema.Struct({ level: Schema.optionalKey(Level), options: Schema.optionalKey(Schema.Unknown) })]);
|
|
18
18
|
|
|
@@ -22,13 +22,17 @@ const AdoptRemoved = Schema.Never.annotate({
|
|
|
22
22
|
message: "removed in quality 0.7.0. Delete the key, and record a rule's existing findings with `quality baseline write --rule <id>`.",
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
const PreCommitRemoved = Schema.Never.annotate({
|
|
26
|
+
message:
|
|
27
|
+
"removed. Put repository checks in the package.json quality:commit script; the hook also runs typecheck when present. Use `quality baseline tighten --staged` explicitly to lower staged debt.",
|
|
28
|
+
});
|
|
29
|
+
|
|
25
30
|
const ConfigInput = Schema.Struct({
|
|
26
31
|
adopt: Schema.optionalKey(AdoptRemoved),
|
|
27
32
|
baseline: Schema.optionalKey(Schema.String),
|
|
28
|
-
exclude: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
29
33
|
extensions: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
30
34
|
local: Schema.optionalKey(Schema.Array(LocalRule)),
|
|
31
|
-
preCommit: Schema.optionalKey(
|
|
35
|
+
preCommit: Schema.optionalKey(PreCommitRemoved),
|
|
32
36
|
registry: Schema.optionalKey(Schema.String),
|
|
33
37
|
rules: Schema.optionalKey(Schema.Record(Schema.String, Setting)),
|
|
34
38
|
sources: Schema.optionalKey(Schema.Array(Schema.String)),
|
package/src/config/load.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { basename, dirname, resolve } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { Effect, FileSystem } from "effect";
|
|
4
|
-
import type { PreCommit } from "#config.ts";
|
|
5
4
|
import { validOrFail } from "#decoded.ts";
|
|
6
5
|
import { SetupFailure } from "#failure.ts";
|
|
7
6
|
import type { InventoryScope } from "#inventory/collect.ts";
|
|
@@ -14,7 +13,6 @@ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs"
|
|
|
14
13
|
export interface ResolvedConfig extends ResolvedRules, InventoryScope {
|
|
15
14
|
readonly baseline: string;
|
|
16
15
|
readonly file: string;
|
|
17
|
-
readonly preCommit: Required<PreCommit>;
|
|
18
16
|
readonly registry: string;
|
|
19
17
|
readonly root: string;
|
|
20
18
|
}
|
|
@@ -36,10 +34,9 @@ const importDefault = (file: string): Effect.Effect<unknown, SetupFailure> =>
|
|
|
36
34
|
const resolved = (root: string, file: string, config: ConfigInput, rules: ResolvedRules): ResolvedConfig => ({
|
|
37
35
|
...rules,
|
|
38
36
|
baseline: config.baseline ?? "quality/baseline.jsonl",
|
|
39
|
-
exclude:
|
|
37
|
+
exclude: [],
|
|
40
38
|
extensions: config.extensions ?? DEFAULT_EXTENSIONS,
|
|
41
39
|
file,
|
|
42
|
-
preCommit: { run: config.preCommit?.run ?? [], tighten: config.preCommit?.tighten ?? false },
|
|
43
40
|
registry: config.registry ?? "quality/registry.json",
|
|
44
41
|
root,
|
|
45
42
|
sources: config.sources ?? ["."],
|
|
@@ -49,11 +46,20 @@ export const loadConfig = (cwd: string, path: string | undefined): Effect.Effect
|
|
|
49
46
|
Effect.gen(function* () {
|
|
50
47
|
const fs = yield* FileSystem.FileSystem;
|
|
51
48
|
const file = resolve(cwd, path ?? CONFIG_FILE);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
const entries = yield* fs
|
|
50
|
+
.readDirectory(dirname(file))
|
|
51
|
+
.pipe(
|
|
52
|
+
Effect.catchTag("PlatformError", (error) =>
|
|
53
|
+
error.reason._tag === "NotFound"
|
|
54
|
+
? Effect.succeed<string[]>([])
|
|
55
|
+
: Effect.fail(new SetupFailure({ message: `cannot inspect ${file}: ${error.message}` })),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
const present = entries.includes(basename(file));
|
|
59
|
+
if (!present && path !== undefined) {
|
|
60
|
+
return yield* new SetupFailure({ message: `no config at ${file}. Check the --config path.` });
|
|
55
61
|
}
|
|
56
|
-
const loaded = yield* importDefault(file);
|
|
62
|
+
const loaded = present ? yield* importDefault(file) : {};
|
|
57
63
|
const config = yield* validOrFail(file, yield* Effect.promise(() => decodeConfig(loaded)));
|
|
58
64
|
const rules = yield* validOrFail(file, yield* resolveRules(config));
|
|
59
65
|
return resolved(dirname(file), file, config, rules);
|
package/src/config/resolve.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import type { Level } from "#config.ts";
|
|
3
2
|
import type { Decoded } from "#decoded.ts";
|
|
4
3
|
import type { ActiveRule } from "#engine/run-rules.ts";
|
|
5
4
|
import type { RuleIndex } from "#engine/violation.ts";
|
|
@@ -15,8 +14,6 @@ type Setting = NonNullable<ConfigInput["rules"]>[string];
|
|
|
15
14
|
|
|
16
15
|
const messageOf = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
|
|
17
16
|
|
|
18
|
-
const levelOf = (setting: Setting | undefined): Level => (typeof setting === "string" ? setting : (setting?.level ?? "error"));
|
|
19
|
-
|
|
20
17
|
const optionsOf = (setting: Setting | undefined): unknown => (typeof setting === "object" ? setting.options : undefined);
|
|
21
18
|
|
|
22
19
|
const duplicateIssues = (rules: readonly Rule[]): readonly string[] =>
|
|
@@ -24,23 +21,21 @@ const duplicateIssues = (rules: readonly Rule[]): readonly string[] =>
|
|
|
24
21
|
rules.findIndex((other) => other.id === rule.id) === index ? [] : [`local: rule id "${rule.id}" is already defined`],
|
|
25
22
|
);
|
|
26
23
|
|
|
27
|
-
const activate = (rule: Rule, setting: Setting | undefined): Effect.Effect<Decoded<ActiveRule
|
|
28
|
-
|
|
29
|
-
if (level === "off") {
|
|
30
|
-
return Effect.succeed({ _tag: "Valid", value: undefined });
|
|
31
|
-
}
|
|
32
|
-
return Effect.tryPromise({ catch: messageOf, try: () => rule.configure(optionsOf(setting)) }).pipe(
|
|
24
|
+
const activate = (rule: Rule, setting: Setting | undefined): Effect.Effect<Decoded<ActiveRule>> =>
|
|
25
|
+
Effect.tryPromise({ catch: messageOf, try: () => rule.configure(optionsOf(setting)) }).pipe(
|
|
33
26
|
Effect.map(
|
|
34
|
-
(configured): Decoded<ActiveRule
|
|
27
|
+
(configured): Decoded<ActiveRule> =>
|
|
35
28
|
configured._tag === "Invalid"
|
|
36
29
|
? { _tag: "Invalid", issues: configured.issues.map((issue) => `rules.${rule.id}.options: ${issue}`) }
|
|
37
|
-
: {
|
|
30
|
+
: {
|
|
31
|
+
_tag: "Valid",
|
|
32
|
+
value: { check: configured.check, description: rule.description, family: rule.family === true, id: rule.id, level: "error" },
|
|
33
|
+
},
|
|
38
34
|
),
|
|
39
35
|
Effect.catch((failure: string) =>
|
|
40
|
-
Effect.succeed<Decoded<ActiveRule
|
|
36
|
+
Effect.succeed<Decoded<ActiveRule>>({ _tag: "Invalid", issues: [`rules.${rule.id}.options: validation threw: ${failure}`] }),
|
|
41
37
|
),
|
|
42
38
|
);
|
|
43
|
-
};
|
|
44
39
|
|
|
45
40
|
export const resolveRules = (config: ConfigInput): Effect.Effect<Decoded<ResolvedRules>> =>
|
|
46
41
|
Effect.gen(function* () {
|
|
@@ -58,8 +53,8 @@ export const resolveRules = (config: ConfigInput): Effect.Effect<Decoded<Resolve
|
|
|
58
53
|
if (issues.length > 0) {
|
|
59
54
|
return { _tag: "Invalid", issues };
|
|
60
55
|
}
|
|
61
|
-
const active = activated.flatMap((result) => (result._tag === "Valid"
|
|
62
|
-
const levels = new Map(rules.map((rule) => [rule.id,
|
|
56
|
+
const active = activated.flatMap((result) => (result._tag === "Valid" ? [result.value] : []));
|
|
57
|
+
const levels = new Map(rules.map((rule) => [rule.id, "error" as const]));
|
|
63
58
|
const unregistrable = new Set(rules.filter((rule) => rule.registrable === false).map((rule) => rule.id));
|
|
64
59
|
return { _tag: "Valid", value: { active, families, levels, unregistrable } };
|
|
65
60
|
});
|
package/src/config.ts
CHANGED
|
@@ -3,30 +3,23 @@ import type { Rule } from "./rule.ts";
|
|
|
3
3
|
|
|
4
4
|
export type Level = "error" | "warn" | "off";
|
|
5
5
|
|
|
6
|
-
export type RuleSetting<
|
|
6
|
+
export type RuleSetting<TInput> = "error" | { readonly level?: "error"; readonly options?: TInput };
|
|
7
7
|
|
|
8
|
-
type InputOf<
|
|
8
|
+
type InputOf<TCandidate> = TCandidate extends Rule<string, infer TInput> ? TInput : never;
|
|
9
9
|
|
|
10
|
-
export type RuleSettings<
|
|
11
|
-
readonly [
|
|
10
|
+
export type RuleSettings<TRules extends readonly Rule[]> = {
|
|
11
|
+
readonly [TCandidate in TRules[number] as TCandidate["id"]]?: RuleSetting<InputOf<TCandidate>>;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type BuiltInRules = typeof builtInRules;
|
|
15
15
|
|
|
16
|
-
export interface
|
|
17
|
-
readonly run?: readonly string[];
|
|
18
|
-
readonly tighten?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface QualityConfig<Local extends readonly Rule[] = readonly Rule[]> {
|
|
16
|
+
export interface QualityConfig<TLocal extends readonly Rule[] = readonly Rule[]> {
|
|
22
17
|
readonly baseline?: string;
|
|
23
|
-
readonly exclude?: readonly string[];
|
|
24
18
|
readonly extensions?: readonly string[];
|
|
25
|
-
readonly local?:
|
|
26
|
-
readonly preCommit?: PreCommit;
|
|
19
|
+
readonly local?: TLocal;
|
|
27
20
|
readonly registry?: string;
|
|
28
|
-
readonly rules?: RuleSettings<readonly [...BuiltInRules, ...
|
|
21
|
+
readonly rules?: RuleSettings<readonly [...BuiltInRules, ...TLocal]>;
|
|
29
22
|
readonly sources?: readonly string[];
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
export const defineConfig = <const
|
|
25
|
+
export const defineConfig = <const TLocal extends readonly Rule[] = readonly []>(config: QualityConfig<TLocal>): QualityConfig<TLocal> => config;
|
package/src/decoded.ts
CHANGED
|
@@ -2,14 +2,14 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { SetupFailure } from "./failure.ts";
|
|
3
3
|
import { describeIssue, type StandardSchemaV1 } from "./standard-schema.ts";
|
|
4
4
|
|
|
5
|
-
export type Decoded<
|
|
5
|
+
export type Decoded<TValue> = { readonly _tag: "Valid"; readonly value: TValue } | { readonly _tag: "Invalid"; readonly issues: readonly string[] };
|
|
6
6
|
|
|
7
|
-
export const decodeWith = async <
|
|
7
|
+
export const decodeWith = async <TOutput>(schema: StandardSchemaV1<unknown, TOutput>, value: unknown): Promise<Decoded<TOutput>> => {
|
|
8
8
|
const result = await schema["~standard"].validate(value);
|
|
9
9
|
return result.issues === undefined ? { _tag: "Valid", value: result.value } : { _tag: "Invalid", issues: result.issues.map(describeIssue) };
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const validOrFail = <
|
|
12
|
+
export const validOrFail = <TValue>(file: string, decoded: Decoded<TValue>): Effect.Effect<TValue, SetupFailure> =>
|
|
13
13
|
decoded._tag === "Valid"
|
|
14
14
|
? Effect.succeed(decoded.value)
|
|
15
15
|
: Effect.fail(new SetupFailure({ message: `${file} is invalid:\n${decoded.issues.map((issue) => ` - ${issue}`).join("\n")}` }));
|
package/src/engine/session.ts
CHANGED
|
@@ -18,9 +18,9 @@ export interface Session {
|
|
|
18
18
|
readonly violations: readonly Violation[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const setup = <
|
|
22
|
-
effect: Effect.Effect<
|
|
23
|
-
): Effect.Effect<
|
|
21
|
+
const setup = <TValue, TRequirements>(
|
|
22
|
+
effect: Effect.Effect<TValue, FilesystemFailure, TRequirements>,
|
|
23
|
+
): Effect.Effect<TValue, SetupFailure, TRequirements> => Effect.mapError(effect, (failure) => new SetupFailure({ message: failure.message }));
|
|
24
24
|
|
|
25
25
|
export const scan = (config: ResolvedConfig): Effect.Effect<Pick<Session, "inventory" | "violations">, SetupFailure, FileSystem.FileSystem> =>
|
|
26
26
|
Effect.gen(function* () {
|
package/src/engine/violation.ts
CHANGED
|
@@ -13,8 +13,8 @@ export const keyOf = (rule: string, file: string): string => `${rule}\u0000${fil
|
|
|
13
13
|
export const byLocation = (left: Violation, right: Violation): number =>
|
|
14
14
|
left.rule.localeCompare(right.rule) || left.file.localeCompare(right.file) || (left.line ?? 0) - (right.line ?? 0);
|
|
15
15
|
|
|
16
|
-
export const groupBy = <
|
|
17
|
-
const groups = new Map<string,
|
|
16
|
+
export const groupBy = <TItem>(items: readonly TItem[], key: (item: TItem) => string): ReadonlyMap<string, readonly TItem[]> => {
|
|
17
|
+
const groups = new Map<string, TItem[]>();
|
|
18
18
|
for (const item of items) {
|
|
19
19
|
const group = groups.get(key(item));
|
|
20
20
|
if (group === undefined) {
|
package/src/git/command.ts
CHANGED
package/src/hooks/preCommit.ts
CHANGED
|
@@ -1,71 +1,62 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { Console, Effect,
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
4
|
-
import type { Scope } from "#baseline/update.ts";
|
|
5
|
-
import { shrinkBaseline } from "#cli/baseline.ts";
|
|
2
|
+
import { Console, Effect, Schema } from "effect";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
6
4
|
import { lint } from "#cli/lint.ts";
|
|
7
|
-
import { loadConfig
|
|
5
|
+
import { loadConfig } from "#config/load.ts";
|
|
6
|
+
import { decodeWith, validOrFail } from "#decoded.ts";
|
|
8
7
|
import { GateFailed, SetupFailure } from "#failure.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { changesSince } from "#git/history.ts";
|
|
8
|
+
import { readOptionalText } from "#inventory/filesystem.ts";
|
|
11
9
|
|
|
12
10
|
const COMMIT_AGAIN =
|
|
13
11
|
"help: fix what the output above reports, then commit again; `quality hooks pre-commit` repeats these checks without committing.";
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const scope: Scope = {
|
|
24
|
-
files: new Set([...staged.files].filter((file) => !held.has(file))),
|
|
25
|
-
moves: new Map([...staged.moves].filter(([from]) => !held.has(from))),
|
|
26
|
-
};
|
|
27
|
-
return scope;
|
|
28
|
-
});
|
|
13
|
+
const Manifest = Schema.toStandardSchemaV1(
|
|
14
|
+
Schema.fromJsonString(
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
packageManager: Schema.optionalKey(Schema.String),
|
|
17
|
+
scripts: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
29
21
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
22
|
+
const commitScripts = Effect.fnUntraced(function* (root: string) {
|
|
23
|
+
const file = join(root, "package.json");
|
|
24
|
+
const text = yield* readOptionalText(file).pipe(Effect.mapError((error) => new SetupFailure({ message: error.message })));
|
|
25
|
+
if (text === undefined) {
|
|
26
|
+
return { manager: "npm", scripts: [] };
|
|
36
27
|
}
|
|
37
|
-
yield*
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
yield*
|
|
28
|
+
const manifest = yield* validOrFail(file, yield* Effect.promise(() => decodeWith(Manifest, text)));
|
|
29
|
+
const manager = manifest.packageManager?.split("@")[0] ?? "npm";
|
|
30
|
+
if (!["npm", "pnpm", "yarn", "bun"].includes(manager)) {
|
|
31
|
+
return yield* new SetupFailure({ message: `${file}: unsupported packageManager "${manager}". Use npm, pnpm, yarn or bun.` });
|
|
41
32
|
}
|
|
33
|
+
return { manager, scripts: ["typecheck", "quality:commit"].filter((script) => manifest.scripts?.[script] !== undefined) };
|
|
42
34
|
});
|
|
43
35
|
|
|
44
|
-
const
|
|
45
|
-
function* (root: string,
|
|
36
|
+
const runScript = Effect.fnUntraced(
|
|
37
|
+
function* (root: string, manager: string, script: string) {
|
|
46
38
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
47
39
|
const child = yield* spawner.spawn(
|
|
48
|
-
ChildProcess.make(
|
|
40
|
+
ChildProcess.make(manager, ["run", script], { cwd: root, stderr: "inherit", stdin: "inherit", stdout: "inherit" }),
|
|
49
41
|
);
|
|
50
42
|
return Number(yield* child.exitCode);
|
|
51
43
|
},
|
|
52
44
|
Effect.scoped,
|
|
53
|
-
Effect.mapError((error) => new SetupFailure({ message: `cannot run a pre-commit
|
|
45
|
+
Effect.mapError((error) => new SetupFailure({ message: `cannot run a pre-commit script: ${error.message}` })),
|
|
54
46
|
);
|
|
55
47
|
|
|
56
48
|
export const preCommitHook = Effect.fn("Hooks.preCommitHook")(function* (cwd: string, path: string | undefined) {
|
|
57
49
|
const config = yield* loadConfig(cwd, path);
|
|
58
|
-
|
|
59
|
-
yield* tighten(cwd, path, config);
|
|
60
|
-
}
|
|
50
|
+
const { manager, scripts } = yield* commitScripts(config.root);
|
|
61
51
|
const linted = yield* lint(cwd, path, "summary").pipe(
|
|
62
52
|
Effect.as(true),
|
|
63
53
|
Effect.catchTag("GateFailed", () => Effect.succeed(false)),
|
|
64
54
|
);
|
|
65
55
|
const failed = linted ? [] : ["quality lint"];
|
|
66
|
-
for (const
|
|
56
|
+
for (const script of scripts) {
|
|
57
|
+
const command = `${manager} run ${script}`;
|
|
67
58
|
yield* Console.log(`quality: running \`${command}\``);
|
|
68
|
-
const code = yield*
|
|
59
|
+
const code = yield* runScript(config.root, manager, script);
|
|
69
60
|
if (code !== 0) {
|
|
70
61
|
failed.push(`\`${command}\` (exit code ${code})`);
|
|
71
62
|
}
|