@shivaedev/quality 0.23.0 → 0.24.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 +7 -0
- package/README.md +4 -0
- package/dist/baseline/compare.d.ts +2 -2
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +5 -3
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +5 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +23 -13
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/legacy.d.ts +2 -2
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/legacy.js +4 -4
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/baseline/rewrite.d.ts +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/rewrite.js +2 -2
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/update.d.ts +2 -2
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +24 -18
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +15 -9
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +10 -6
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +2 -2
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +34 -40
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/run-main.js +2 -2
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +2 -2
- package/dist/cli/shrink.d.ts.map +1 -1
- package/dist/cli/shrink.js +9 -3
- package/dist/cli/shrink.js.map +1 -1
- package/dist/config/decode.d.ts +1 -1
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +13 -9
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +26 -20
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +22 -2
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +18 -10
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +2 -2
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js +7 -5
- package/dist/decoded.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +1 -1
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/baseline-file.js +4 -2
- package/dist/engine/baseline-file.js.map +1 -1
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +5 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/run-rules.d.ts +1 -1
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +24 -14
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +8 -4
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +7 -7
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js +22 -10
- package/dist/engine/violation.js.map +1 -1
- package/dist/exceptions/registry.d.ts +2 -2
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +10 -6
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/git/base.d.ts +1 -2
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js +22 -10
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +2 -2
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js +13 -9
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/git/history.js +5 -3
- package/dist/git/history.js.map +1 -1
- package/dist/inventory/collect.d.ts +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +7 -5
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/filesystem.d.ts +1 -1
- package/dist/inventory/filesystem.d.ts.map +1 -1
- package/dist/inventory/filesystem.js +11 -7
- package/dist/inventory/filesystem.js.map +1 -1
- package/dist/inventory/ignore-scope.d.ts +4 -4
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +18 -9
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/manifests/sorted.d.ts +1 -1
- package/dist/manifests/sorted.d.ts.map +1 -1
- package/dist/manifests/sorted.js +14 -16
- package/dist/manifests/sorted.js.map +1 -1
- package/dist/report/plural.d.ts +1 -1
- package/dist/report/plural.d.ts.map +1 -1
- package/dist/report/plural.js +3 -1
- package/dist/report/plural.js.map +1 -1
- package/dist/report/render.d.ts +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +29 -21
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +18 -8
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +13 -13
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +10 -8
- package/dist/rule.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/comments/kinds.d.ts +4 -4
- package/dist/rules/comments/kinds.d.ts.map +1 -1
- package/dist/rules/comments/kinds.js +16 -8
- package/dist/rules/comments/kinds.js.map +1 -1
- package/dist/rules/comments/max-per-file.d.ts.map +1 -1
- package/dist/rules/comments/max-per-file.js +13 -9
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/pattern-rule.d.ts +1 -1
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/pattern-rule.js +12 -10
- package/dist/rules/comments/pattern-rule.js.map +1 -1
- package/dist/rules/comments/scan.d.ts +3 -3
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/comments/scan.js +23 -19
- package/dist/rules/comments/scan.js.map +1 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +12 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/stories/origins.d.ts +10 -0
- package/dist/rules/stories/origins.d.ts.map +1 -0
- package/dist/rules/stories/origins.js +129 -0
- package/dist/rules/stories/origins.js.map +1 -0
- package/dist/rules/stories/prerequisites.d.ts +4 -0
- package/dist/rules/stories/prerequisites.d.ts.map +1 -0
- package/dist/rules/stories/prerequisites.js +32 -0
- package/dist/rules/stories/prerequisites.js.map +1 -0
- package/dist/rules/stories/program.d.ts +5 -0
- package/dist/rules/stories/program.d.ts.map +1 -0
- package/dist/rules/stories/program.js +22 -0
- package/dist/rules/stories/program.js.map +1 -0
- package/dist/rules/stories/specs.d.ts +2 -0
- package/dist/rules/stories/specs.d.ts.map +1 -0
- package/dist/rules/stories/specs.js +80 -0
- package/dist/rules/stories/specs.js.map +1 -0
- package/dist/rules/suppressions/biome/configs.d.ts +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/configs.js +23 -17
- package/dist/rules/suppressions/biome/configs.js.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.js +15 -7
- package/dist/rules/suppressions/biome/declarations.js.map +1 -1
- package/dist/rules/suppressions/biome/json.d.ts +6 -6
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.js +27 -13
- package/dist/rules/suppressions/biome/json.js.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/resolve.js +23 -17
- package/dist/rules/suppressions/biome/resolve.js.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.js +78 -52
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +45 -33
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/directives.d.ts +1 -1
- package/dist/rules/suppressions/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/directives.js +5 -3
- package/dist/rules/suppressions/directives.js.map +1 -1
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -1
- package/dist/rules/suppressions/no-double-cast.js +14 -10
- package/dist/rules/suppressions/no-double-cast.js.map +1 -1
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +8 -6
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.js +11 -5
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
- package/dist/rules/syntax.d.ts +1 -1
- package/dist/rules/syntax.d.ts.map +1 -1
- package/dist/rules/syntax.js +2 -2
- package/dist/rules/syntax.js.map +1 -1
- package/dist/standard-schema.d.ts +6 -6
- package/dist/standard-schema.d.ts.map +1 -1
- package/docs/modules.md +2 -0
- package/package.json +1 -1
- package/src/baseline/compare.ts +5 -3
- package/src/baseline/format.ts +17 -10
- package/src/baseline/legacy.ts +4 -4
- package/src/baseline/rewrite.ts +2 -2
- package/src/baseline/update.ts +20 -16
- package/src/cli/args.ts +15 -10
- package/src/cli/baseline.ts +36 -35
- package/src/cli/fix.ts +39 -42
- package/src/cli/lint.ts +17 -19
- package/src/cli/migrate.ts +20 -21
- package/src/cli/run-main.ts +2 -2
- package/src/cli/shrink.ts +11 -8
- package/src/config/decode.ts +15 -10
- package/src/config/load.ts +44 -37
- package/src/config/resolve.ts +32 -27
- package/src/config.ts +3 -1
- package/src/decoded.ts +5 -4
- package/src/engine/baseline-file.ts +14 -12
- package/src/engine/evaluate.ts +6 -5
- package/src/engine/run-rules.ts +21 -13
- package/src/engine/session.ts +32 -25
- package/src/engine/violation.ts +22 -11
- package/src/exceptions/registry.ts +10 -8
- package/src/git/base.ts +45 -37
- package/src/git/command.ts +6 -4
- package/src/git/history.ts +5 -4
- package/src/inventory/collect.ts +39 -34
- package/src/inventory/filesystem.ts +38 -30
- package/src/inventory/ignore-scope.ts +20 -10
- package/src/manifests/sorted.ts +18 -18
- package/src/report/plural.ts +3 -1
- package/src/report/render.ts +23 -18
- package/src/report/stale.ts +33 -25
- package/src/rule.ts +25 -19
- package/src/rules/built-in.ts +2 -0
- package/src/rules/comments/kinds.ts +16 -9
- package/src/rules/comments/max-per-file.ts +15 -11
- package/src/rules/comments/pattern-rule.ts +5 -4
- package/src/rules/comments/scan.ts +18 -15
- package/src/rules/max-lines.ts +3 -2
- package/src/rules/stories/origins.ts +147 -0
- package/src/rules/stories/prerequisites.ts +37 -0
- package/src/rules/stories/program.ts +29 -0
- package/src/rules/stories/specs.ts +93 -0
- package/src/rules/suppressions/biome/configs.ts +23 -17
- package/src/rules/suppressions/biome/declarations.ts +15 -10
- package/src/rules/suppressions/biome/json.ts +25 -13
- package/src/rules/suppressions/biome/resolve.ts +23 -18
- package/src/rules/suppressions/biome/weakenings.ts +60 -41
- package/src/rules/suppressions/biome-overrides.ts +32 -22
- package/src/rules/suppressions/directives.ts +5 -4
- package/src/rules/suppressions/no-double-cast.ts +14 -10
- package/src/rules/suppressions/no-inline.ts +5 -4
- package/src/rules/suppressions/stylesheet-comments.ts +11 -6
- package/src/rules/syntax.ts +2 -2
- package/src/standard-schema.ts +4 -4
package/src/config/decode.ts
CHANGED
|
@@ -3,15 +3,18 @@ import { type Decoded, decodeWith } from "#decoded.ts";
|
|
|
3
3
|
import { ExceptionDeclaration } from "#exceptions/declaration.ts";
|
|
4
4
|
import type { Rule } from "#rule.ts";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
function isRule(value: unknown): value is Rule {
|
|
7
|
+
return (
|
|
8
|
+
typeof value === "object"
|
|
9
|
+
&& value !== null
|
|
10
|
+
&& "id" in value
|
|
11
|
+
&& typeof value.id === "string"
|
|
12
|
+
&& "description" in value
|
|
13
|
+
&& typeof value.description === "string"
|
|
14
|
+
&& "configure" in value
|
|
15
|
+
&& typeof value.configure === "function"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
const Level = Schema.Literal("error");
|
|
17
20
|
|
|
@@ -44,4 +47,6 @@ export type ConfigInput = typeof ConfigInput.Type;
|
|
|
44
47
|
|
|
45
48
|
const standard = Schema.toStandardSchemaV1(ConfigInput, { parseOptions: { errors: "all", onExcessProperty: "error" } });
|
|
46
49
|
|
|
47
|
-
export
|
|
50
|
+
export function decodeConfig(value: unknown): Promise<Decoded<ConfigInput>> {
|
|
51
|
+
return decodeWith(standard, value);
|
|
52
|
+
}
|
package/src/config/load.ts
CHANGED
|
@@ -22,10 +22,12 @@ export interface ResolvedConfig extends ResolvedRules, InventoryScope {
|
|
|
22
22
|
readonly root: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function messageOf(cause: unknown): string {
|
|
26
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
Effect.flatMap(
|
|
29
|
+
function importDefault(file: string): Effect.Effect<unknown, SetupFailure> {
|
|
30
|
+
return Effect.flatMap(
|
|
29
31
|
Effect.tryPromise({
|
|
30
32
|
catch: (cause) => new SetupFailure({ message: `cannot load ${file}: ${messageOf(cause)}` }),
|
|
31
33
|
try: async (): Promise<unknown> => import(pathToFileURL(file).href),
|
|
@@ -35,6 +37,7 @@ const importDefault = (file: string): Effect.Effect<unknown, SetupFailure> =>
|
|
|
35
37
|
? Effect.succeed(loaded.default)
|
|
36
38
|
: Effect.fail(new SetupFailure({ message: `${file} has no default export. Export defineConfig({ ... }) as the default.` })),
|
|
37
39
|
);
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
const LegacyOptions = Schema.Struct({ declared: Schema.Array(ConsumerDeclaration) });
|
|
40
43
|
function biomeDeclarations(config: ConfigInput): readonly Declaration[] {
|
|
@@ -43,38 +46,42 @@ function biomeDeclarations(config: ConfigInput): readonly Declaration[] {
|
|
|
43
46
|
return Schema.is(LegacyOptions)(options) ? options.declared : [];
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
function resolved(root: string, file: string, config: ConfigInput, rules: ResolvedRules): ResolvedConfig {
|
|
50
|
+
return {
|
|
51
|
+
...rules,
|
|
52
|
+
baseline: config.baseline ?? "quality/baseline.jsonl",
|
|
53
|
+
biomeDeclarations: biomeDeclarations(config),
|
|
54
|
+
exceptions: config.exceptions ?? [],
|
|
55
|
+
exclude: [],
|
|
56
|
+
extensions: config.extensions ?? DEFAULT_EXTENSIONS,
|
|
57
|
+
file,
|
|
58
|
+
registry: config.registry ?? "quality/registry.json",
|
|
59
|
+
root,
|
|
60
|
+
sources: config.sources ?? ["."],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
58
63
|
|
|
59
|
-
export const loadConfig =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
export const loadConfig = Effect.fn("Config.loadConfig")(function* (
|
|
65
|
+
cwd: string,
|
|
66
|
+
path: string | undefined,
|
|
67
|
+
): Effect.fn.Return<ResolvedConfig, SetupFailure, FileSystem.FileSystem> {
|
|
68
|
+
const fs = yield* FileSystem.FileSystem;
|
|
69
|
+
const file = resolve(cwd, path ?? CONFIG_FILE);
|
|
70
|
+
const entries = yield* fs
|
|
71
|
+
.readDirectory(dirname(file))
|
|
72
|
+
.pipe(
|
|
73
|
+
Effect.catchTag("PlatformError", (error) =>
|
|
74
|
+
error.reason._tag === "NotFound"
|
|
75
|
+
? Effect.succeed<string[]>([])
|
|
76
|
+
: Effect.fail(new SetupFailure({ message: `cannot inspect ${file}: ${error.message}` })),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
const present = entries.includes(basename(file));
|
|
80
|
+
if (!present && path !== undefined) {
|
|
81
|
+
return yield* new SetupFailure({ message: `no config at ${file}. Check the --config path.` });
|
|
82
|
+
}
|
|
83
|
+
const loaded = present ? yield* importDefault(file) : {};
|
|
84
|
+
const config = yield* validOrFail(file, yield* Effect.promise(() => decodeConfig(loaded)));
|
|
85
|
+
const rules = yield* validOrFail(file, yield* resolveRules(config));
|
|
86
|
+
return resolved(dirname(file), file, config, rules);
|
|
87
|
+
});
|
package/src/config/resolve.ts
CHANGED
|
@@ -12,14 +12,19 @@ export interface ResolvedRules extends RuleIndex {
|
|
|
12
12
|
|
|
13
13
|
type Setting = NonNullable<ConfigInput["rules"]>[string];
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function messageOf(cause: unknown): string {
|
|
16
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
17
|
+
}
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
function optionsOf(setting: Setting | undefined): unknown {
|
|
20
|
+
return typeof setting === "object" ? setting.options : undefined;
|
|
21
|
+
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
rules.flatMap((rule, index) =>
|
|
23
|
+
function duplicateIssues(rules: readonly Rule[]): readonly string[] {
|
|
24
|
+
return rules.flatMap((rule, index) =>
|
|
21
25
|
rules.findIndex((other) => other.id === rule.id) === index ? [] : [`local: rule id "${rule.id}" is already defined`],
|
|
22
26
|
);
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
function activeRule(rule: Rule, check: ActiveRule["check"]): ActiveRule {
|
|
25
30
|
return {
|
|
@@ -32,8 +37,8 @@ function activeRule(rule: Rule, check: ActiveRule["check"]): ActiveRule {
|
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
Effect.tryPromise({ catch: messageOf, try: () => rule.configure(optionsOf(setting)) }).pipe(
|
|
40
|
+
function activate(rule: Rule, setting: Setting | undefined): Effect.Effect<Decoded<ActiveRule>> {
|
|
41
|
+
return Effect.tryPromise({ catch: messageOf, try: () => rule.configure(optionsOf(setting)) }).pipe(
|
|
37
42
|
Effect.map(
|
|
38
43
|
(configured): Decoded<ActiveRule> =>
|
|
39
44
|
configured._tag === "Invalid"
|
|
@@ -47,25 +52,25 @@ const activate = (rule: Rule, setting: Setting | undefined): Effect.Effect<Decod
|
|
|
47
52
|
Effect.succeed<Decoded<ActiveRule>>({ _tag: "Invalid", issues: [`rules.${rule.id}.options: validation threw: ${failure}`] }),
|
|
48
53
|
),
|
|
49
54
|
);
|
|
55
|
+
}
|
|
50
56
|
|
|
51
|
-
export const resolveRules = (config: ConfigInput): Effect.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
57
|
+
export const resolveRules = Effect.fn("Config.resolveRules")(function* (config: ConfigInput): Effect.fn.Return<Decoded<ResolvedRules>> {
|
|
58
|
+
const rules: readonly Rule[] = [...builtInRules, ...(config.local ?? [])];
|
|
59
|
+
const settings = config.rules ?? {};
|
|
60
|
+
const known = new Set(rules.map((rule) => rule.id));
|
|
61
|
+
const families = new Set(rules.filter((rule) => rule.family === true).map((rule) => rule.id));
|
|
62
|
+
const unknown = [
|
|
63
|
+
...Object.keys(settings)
|
|
64
|
+
.filter((id) => !known.has(id))
|
|
65
|
+
.map((id) => `rules.${id}`),
|
|
66
|
+
].map((path) => `${path}: no built-in or local rule has this id`);
|
|
67
|
+
const activated = yield* Effect.forEach(rules, (rule) => activate(rule, settings[rule.id]));
|
|
68
|
+
const issues = [...duplicateIssues(rules), ...unknown, ...activated.flatMap((result) => (result._tag === "Invalid" ? result.issues : []))];
|
|
69
|
+
if (issues.length > 0) {
|
|
70
|
+
return { _tag: "Invalid", issues };
|
|
71
|
+
}
|
|
72
|
+
const active = activated.flatMap((result) => (result._tag === "Valid" ? [result.value] : []));
|
|
73
|
+
const levels = new Map(rules.map((rule) => [rule.id, "error" as const]));
|
|
74
|
+
const unregistrable = new Set(rules.filter((rule) => rule.registrable === false).map((rule) => rule.id));
|
|
75
|
+
return { _tag: "Valid", value: { active, families, levels, unregistrable } };
|
|
76
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -24,4 +24,6 @@ export interface QualityConfig<TLocal extends readonly Rule[] = readonly Rule[]>
|
|
|
24
24
|
readonly sources?: readonly string[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export
|
|
27
|
+
export function defineConfig<const TLocal extends readonly Rule[] = readonly []>(config: QualityConfig<TLocal>): QualityConfig<TLocal> {
|
|
28
|
+
return config;
|
|
29
|
+
}
|
package/src/decoded.ts
CHANGED
|
@@ -4,12 +4,13 @@ import { describeIssue, type StandardSchemaV1 } from "./standard-schema.ts";
|
|
|
4
4
|
|
|
5
5
|
export type Decoded<TValue> = { readonly _tag: "Valid"; readonly value: TValue } | { readonly _tag: "Invalid"; readonly issues: readonly string[] };
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export async function decodeWith<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
|
|
13
|
-
decoded._tag === "Valid"
|
|
12
|
+
export function validOrFail<TValue>(file: string, decoded: Decoded<TValue>): Effect.Effect<TValue, SetupFailure> {
|
|
13
|
+
return 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")}` }));
|
|
16
|
+
}
|
|
@@ -12,16 +12,18 @@ export interface BaselineFile {
|
|
|
12
12
|
readonly raw: string | undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
Effect.mapError(readOptionalText(join(root, path)), (failure) => new SetupFailure({ message: failure.message }));
|
|
15
|
+
export function readInput(root: string, path: string): Effect.Effect<string | undefined, SetupFailure, FileSystem.FileSystem> {
|
|
16
|
+
return Effect.mapError(readOptionalText(join(root, path)), (failure) => new SetupFailure({ message: failure.message }));
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
export const readBaseline =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
}
|
|
19
|
+
export const readBaseline = Effect.fn("Engine.readBaseline")(function* (
|
|
20
|
+
config: ResolvedConfig,
|
|
21
|
+
): Effect.fn.Return<BaselineFile, SetupFailure, FileSystem.FileSystem> {
|
|
22
|
+
const raw = yield* readInput(config.root, config.baseline);
|
|
23
|
+
if (raw === undefined && config.baseline !== LEGACY_BASELINE && (yield* readInput(config.root, LEGACY_BASELINE)) !== undefined) {
|
|
24
|
+
return yield* new SetupFailure({
|
|
25
|
+
message: `${LEGACY_BASELINE} holds a baseline in the earlier JSON format. Run \`quality baseline migrate\` to move it to ${config.baseline}.`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return { entries: yield* validOrFail(config.baseline, yield* Effect.promise(() => decodeBaseline(raw))), raw };
|
|
29
|
+
});
|
package/src/engine/evaluate.ts
CHANGED
|
@@ -14,12 +14,12 @@ export interface Outcome {
|
|
|
14
14
|
readonly warnings: readonly Violation[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export function evaluate(
|
|
18
18
|
violations: readonly Violation[],
|
|
19
19
|
registry: readonly RegistryEntry[],
|
|
20
20
|
baseline: readonly BaselineEntry[],
|
|
21
21
|
rules: RuleIndex,
|
|
22
|
-
): Outcome
|
|
22
|
+
): Outcome {
|
|
23
23
|
const registered = applyRegistry(violations, registry, rules);
|
|
24
24
|
const baselined = applyBaseline(registered.kept, baseline, rules);
|
|
25
25
|
const kept = [...baselined.kept].sort(byLocation);
|
|
@@ -33,7 +33,8 @@ export const evaluate = (
|
|
|
33
33
|
staleRegistry: registered.stale,
|
|
34
34
|
warnings: kept.filter((violation) => violation.level === "warn"),
|
|
35
35
|
};
|
|
36
|
-
}
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
export
|
|
39
|
-
outcome.errors.length === 0 && outcome.staleBaseline.length === 0 && outcome.staleRegistry.length === 0;
|
|
38
|
+
export function passes(outcome: Outcome): boolean {
|
|
39
|
+
return outcome.errors.length === 0 && outcome.staleBaseline.length === 0 && outcome.staleRegistry.length === 0;
|
|
40
|
+
}
|
package/src/engine/run-rules.ts
CHANGED
|
@@ -14,10 +14,14 @@ export interface ActiveRule {
|
|
|
14
14
|
readonly registrable?: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function messageOf(cause: unknown): string {
|
|
18
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
19
|
+
}
|
|
18
20
|
|
|
19
21
|
// Registry and baseline entries match on the path, so every finding names its file the same way.
|
|
20
|
-
|
|
22
|
+
function repositoryPath(root: string, file: string): string {
|
|
23
|
+
return posix(isAbsolute(file) ? relative(root, file) : file).replace(/^(?:\.\/)+/u, "");
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
function exceptionHint(rule: ActiveRule, finding: Finding, file: string, config: string): string {
|
|
23
27
|
const id = rule.family && finding.subject !== undefined ? `${rule.id}/${finding.subject}` : rule.id;
|
|
@@ -34,25 +38,29 @@ function exceptionHint(rule: ActiveRule, finding: Finding, file: string, config:
|
|
|
34
38
|
return ` For a permanent exception, add to exceptions in ${config}: ${shape}.`;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
41
|
+
function violationOf(rule: ActiveRule, root: string, finding: Finding, exceptionFile: string): Violation {
|
|
42
|
+
return {
|
|
43
|
+
...finding,
|
|
44
|
+
file: repositoryPath(root, finding.file),
|
|
45
|
+
level: rule.level,
|
|
46
|
+
message: finding.message + exceptionHint(rule, finding, repositoryPath(root, finding.file), exceptionFile),
|
|
47
|
+
rule: rule.family && finding.subject !== undefined ? `${rule.id}/${finding.subject}` : rule.id,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
Effect.map(
|
|
51
|
+
function runRule(rule: ActiveRule, inputs: RuleInputs, exceptionFile: string): Effect.Effect<readonly Violation[], SetupFailure> {
|
|
52
|
+
return Effect.map(
|
|
47
53
|
Effect.tryPromise({
|
|
48
54
|
catch: (cause) => new SetupFailure({ message: `rule ${rule.id} failed: ${messageOf(cause)}` }),
|
|
49
55
|
try: () => rule.check(inputs),
|
|
50
56
|
}),
|
|
51
57
|
(findings) => findings.map((finding) => violationOf(rule, inputs.root, finding, exceptionFile)),
|
|
52
58
|
);
|
|
59
|
+
}
|
|
53
60
|
|
|
54
|
-
export
|
|
55
|
-
Effect.map(
|
|
61
|
+
export function runRules(rules: readonly ActiveRule[], inputs: RuleInputs): Effect.Effect<readonly Violation[], SetupFailure> {
|
|
62
|
+
return Effect.map(
|
|
56
63
|
Effect.forEach(rules, (rule) => runRule(rule, inputs, inputs.configFile ?? "quality.config.ts"), { concurrency: "unbounded" }),
|
|
57
64
|
(results) => results.flat(),
|
|
58
65
|
);
|
|
66
|
+
}
|
package/src/engine/session.ts
CHANGED
|
@@ -19,32 +19,39 @@ export interface Session {
|
|
|
19
19
|
readonly violations: readonly Violation[];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function setup<TValue, TRequirements>(
|
|
23
23
|
effect: Effect.Effect<TValue, FilesystemFailure, TRequirements>,
|
|
24
|
-
): Effect.Effect<TValue, SetupFailure, TRequirements>
|
|
24
|
+
): Effect.Effect<TValue, SetupFailure, TRequirements> {
|
|
25
|
+
return Effect.mapError(effect, (failure) => new SetupFailure({ message: failure.message }));
|
|
26
|
+
}
|
|
25
27
|
|
|
26
|
-
export const scan =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
export const scan = Effect.fn("Engine.scan")(function* (
|
|
29
|
+
config: ResolvedConfig,
|
|
30
|
+
): Effect.fn.Return<Pick<Session, "inventory" | "violations">, SetupFailure, FileSystem.FileSystem> {
|
|
31
|
+
const inventory = yield* setup(collectInventory(config.root, config));
|
|
32
|
+
const services = yield* Effect.context<FileSystem.FileSystem>();
|
|
33
|
+
function readText(path: string): Promise<string | undefined> {
|
|
34
|
+
return Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
|
|
35
|
+
}
|
|
36
|
+
const violations = yield* runRules(config.active, {
|
|
37
|
+
configFile: relative(config.root, config.file),
|
|
38
|
+
exceptions: config.exceptions,
|
|
39
|
+
files: inventory.files,
|
|
40
|
+
readText,
|
|
41
|
+
root: config.root,
|
|
42
|
+
sources: inventory.sources,
|
|
40
43
|
});
|
|
44
|
+
return { inventory, violations };
|
|
45
|
+
});
|
|
41
46
|
|
|
42
|
-
export const openSession =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
47
|
+
export const openSession = Effect.fn("Engine.openSession")(function* (
|
|
48
|
+
cwd: string,
|
|
49
|
+
configPath: string | undefined,
|
|
50
|
+
): Effect.fn.Return<Session, SetupFailure, FileSystem.FileSystem> {
|
|
51
|
+
const config = yield* loadConfig(cwd, configPath);
|
|
52
|
+
const registryRaw = yield* readInput(config.root, config.registry);
|
|
53
|
+
const registry = yield* validOrFail(config.registry, yield* Effect.promise(() => decodeRegistry(registryRaw)));
|
|
54
|
+
const baseline = yield* readBaseline(config);
|
|
55
|
+
const { inventory, violations } = yield* scan(config);
|
|
56
|
+
return { baseline, config, inventory, registry: [...registry, ...registryEntries(config.exceptions)], violations };
|
|
57
|
+
});
|
package/src/engine/violation.ts
CHANGED
|
@@ -8,12 +8,15 @@ export interface Violation extends Finding {
|
|
|
8
8
|
readonly rule: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export function keyOf(rule: string, file: string): string {
|
|
12
|
+
return `${rule}\u0000${file}`;
|
|
13
|
+
}
|
|
12
14
|
|
|
13
|
-
export
|
|
14
|
-
left.rule.localeCompare(right.rule) || left.file.localeCompare(right.file) || (left.line ?? 0) - (right.line ?? 0);
|
|
15
|
+
export function byLocation(left: Violation, right: Violation): number {
|
|
16
|
+
return left.rule.localeCompare(right.rule) || left.file.localeCompare(right.file) || (left.line ?? 0) - (right.line ?? 0);
|
|
17
|
+
}
|
|
15
18
|
|
|
16
|
-
export
|
|
19
|
+
export function groupBy<TItem>(items: readonly TItem[], key: (item: TItem) => string): ReadonlyMap<string, readonly TItem[]> {
|
|
17
20
|
const groups = new Map<string, TItem[]>();
|
|
18
21
|
for (const item of items) {
|
|
19
22
|
const group = groups.get(key(item));
|
|
@@ -24,7 +27,7 @@ export const groupBy = <TItem>(items: readonly TItem[], key: (item: TItem) => st
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
return groups;
|
|
27
|
-
}
|
|
30
|
+
}
|
|
28
31
|
|
|
29
32
|
export interface RuleIndex {
|
|
30
33
|
readonly families: ReadonlySet<string>;
|
|
@@ -32,18 +35,26 @@ export interface RuleIndex {
|
|
|
32
35
|
readonly unregistrable: ReadonlySet<string>;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
export
|
|
38
|
+
export function covers(name: string, rule: string): boolean {
|
|
39
|
+
return rule === name || rule.startsWith(`${name}/`);
|
|
40
|
+
}
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
function ownerOf(index: RuleIndex, rule: string): string {
|
|
43
|
+
return [...index.families].find((family) => covers(family, rule)) ?? rule;
|
|
44
|
+
}
|
|
38
45
|
|
|
39
|
-
export
|
|
46
|
+
export function levelOf(index: RuleIndex, rule: string): Level | undefined {
|
|
47
|
+
return index.levels.get(ownerOf(index, rule));
|
|
48
|
+
}
|
|
40
49
|
|
|
41
|
-
export
|
|
50
|
+
export function registrable(index: RuleIndex, rule: string): boolean {
|
|
51
|
+
return !index.unregistrable.has(ownerOf(index, rule));
|
|
52
|
+
}
|
|
42
53
|
|
|
43
|
-
export
|
|
54
|
+
export function unusedEntryProblem(index: RuleIndex, rule: string, unused: string): string {
|
|
44
55
|
const level = levelOf(index, rule);
|
|
45
56
|
if (level === undefined) {
|
|
46
57
|
return "names no known rule";
|
|
47
58
|
}
|
|
48
59
|
return level === "off" ? "names a rule that is off" : unused;
|
|
49
|
-
}
|
|
60
|
+
}
|
|
@@ -15,8 +15,9 @@ const standard = Schema.toStandardSchemaV1(Schema.fromJsonString(Schema.Array(Re
|
|
|
15
15
|
parseOptions: { errors: "all", onExcessProperty: "error" },
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
raw === undefined ? Promise.resolve({ _tag: "Valid", value: [] }) : decodeWith(standard, raw);
|
|
18
|
+
export function decodeRegistry(raw: string | undefined): Promise<Decoded<readonly RegistryEntry[]>> {
|
|
19
|
+
return raw === undefined ? Promise.resolve({ _tag: "Valid", value: [] }) : decodeWith(standard, raw);
|
|
20
|
+
}
|
|
20
21
|
|
|
21
22
|
export interface StaleRegistryEntry {
|
|
22
23
|
readonly entry: RegistryEntry;
|
|
@@ -29,17 +30,18 @@ export interface RegistryCheck {
|
|
|
29
30
|
readonly stale: readonly StaleRegistryEntry[];
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
entry.rule === violation.rule && entry.file === violation.file && (entry.subject === undefined || entry.subject === violation.subject);
|
|
33
|
+
function covers(entry: RegistryEntry, violation: Violation): boolean {
|
|
34
|
+
return entry.rule === violation.rule && entry.file === violation.file && (entry.subject === undefined || entry.subject === violation.subject);
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
const UNREGISTRABLE = "names a rule that takes no exceptions. Fix the code, or baseline the violations while the repository adopts the rule";
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
function coveringEntry(entries: readonly RegistryEntry[], violation: Violation): RegistryEntry | undefined {
|
|
38
40
|
const matching = entries.filter((entry) => covers(entry, violation));
|
|
39
41
|
return matching.find((entry) => entry.subject !== undefined) ?? matching[0];
|
|
40
|
-
}
|
|
42
|
+
}
|
|
41
43
|
|
|
42
|
-
export
|
|
44
|
+
export function applyRegistry(violations: readonly Violation[], entries: readonly RegistryEntry[], rules: RuleIndex): RegistryCheck {
|
|
43
45
|
const usable = entries.filter((entry) => registrable(rules, entry.rule));
|
|
44
46
|
const used = new Set<RegistryEntry>();
|
|
45
47
|
const kept = violations.filter((violation) => {
|
|
@@ -56,4 +58,4 @@ export const applyRegistry = (violations: readonly Violation[], entries: readonl
|
|
|
56
58
|
problem: registrable(rules, entry.rule) ? unusedEntryProblem(rules, entry.rule, "matches no violation") : UNREGISTRABLE,
|
|
57
59
|
}));
|
|
58
60
|
return { kept, registered: violations.length - kept.length, stale };
|
|
59
|
-
}
|
|
61
|
+
}
|