@shivaedev/quality 0.31.0 → 0.32.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 +17 -0
- package/README.md +19 -4
- package/dist/baseline/installDriver.d.ts +5 -0
- package/dist/baseline/installDriver.d.ts.map +1 -0
- package/dist/baseline/installDriver.js +11 -0
- package/dist/baseline/installDriver.js.map +1 -0
- package/dist/baseline/merge.d.ts +4 -0
- package/dist/baseline/merge.d.ts.map +1 -0
- package/dist/baseline/merge.js +28 -0
- package/dist/baseline/merge.js.map +1 -0
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/rewrite.js +0 -1
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/shards.d.ts +13 -0
- package/dist/baseline/shards.d.ts.map +1 -0
- package/dist/baseline/shards.js +48 -0
- package/dist/baseline/shards.js.map +1 -0
- package/dist/cli/args.d.ts +8 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +6 -7
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/messages.d.ts +1 -1
- package/dist/cli/messages.d.ts.map +1 -1
- package/dist/cli/messages.js +6 -2
- package/dist/cli/messages.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +10 -6
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/program.js +6 -0
- package/dist/cli/program.js.map +1 -1
- package/dist/config/load.js +1 -1
- package/dist/engine/baselineFile.d.ts +2 -1
- package/dist/engine/baselineFile.d.ts.map +1 -1
- package/dist/engine/baselineFile.js +23 -6
- package/dist/engine/baselineFile.js.map +1 -1
- package/dist/imports/fences/reach.d.ts +5 -4
- package/dist/imports/fences/reach.d.ts.map +1 -1
- package/dist/imports/fences/reach.js +3 -2
- package/dist/imports/fences/reach.js.map +1 -1
- package/dist/rules/stories/unitTests.js +2 -2
- package/dist/rules/stories/unitTests.js.map +1 -1
- package/dist/rules/test-names/usesDom.d.ts.map +1 -1
- package/dist/rules/test-names/usesDom.js +11 -4
- package/dist/rules/test-names/usesDom.js.map +1 -1
- package/docs/debt-and-hooks.md +6 -3
- package/package.json +1 -1
- package/src/baseline/installDriver.ts +12 -0
- package/src/baseline/merge.ts +34 -0
- package/src/baseline/rewrite.ts +0 -1
- package/src/baseline/shards.ts +60 -0
- package/src/cli/args.ts +11 -0
- package/src/cli/baseline.ts +6 -10
- package/src/cli/messages.ts +6 -2
- package/src/cli/migrate.ts +10 -6
- package/src/cli/program.ts +6 -0
- package/src/config/load.ts +1 -1
- package/src/engine/baselineFile.ts +24 -7
- package/src/imports/fences/reach.ts +17 -11
- package/src/rules/stories/unitTests.ts +3 -3
- package/src/rules/test-names/usesDom.ts +11 -4
|
@@ -2,6 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { Effect, type FileSystem } from "effect";
|
|
3
3
|
import { type BaselineEntry, decodeBaseline } from "#baseline/format.ts";
|
|
4
4
|
import { LEGACY_BASELINE } from "#baseline/legacy.ts";
|
|
5
|
+
import { type Shard, shardOf, shardPaths } from "#baseline/shards.ts";
|
|
5
6
|
import type { ResolvedConfig } from "#config/load.ts";
|
|
6
7
|
import { validOrFail } from "#decoded.ts";
|
|
7
8
|
import { SetupFailure } from "#failure.ts";
|
|
@@ -9,7 +10,7 @@ import { readOptionalText } from "#inventory/filesystem.ts";
|
|
|
9
10
|
|
|
10
11
|
export interface BaselineFile {
|
|
11
12
|
readonly entries: readonly BaselineEntry[];
|
|
12
|
-
readonly
|
|
13
|
+
readonly files: ReadonlyMap<string, Shard>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function readInput(root: string, path: string): Effect.Effect<string | undefined, SetupFailure, FileSystem.FileSystem> {
|
|
@@ -19,11 +20,27 @@ export function readInput(root: string, path: string): Effect.Effect<string | un
|
|
|
19
20
|
export const readBaseline = Effect.fn("Engine.readBaseline")(function* (
|
|
20
21
|
config: ResolvedConfig,
|
|
21
22
|
): Effect.fn.Return<BaselineFile, SetupFailure, FileSystem.FileSystem> {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const files = new Map<string, Shard>();
|
|
24
|
+
for (const path of yield* shardPaths(config).pipe(Effect.mapError((error) => new SetupFailure({ message: error.message })))) {
|
|
25
|
+
const raw = yield* readInput(config.root, path);
|
|
26
|
+
if (raw === undefined) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const entries = yield* validOrFail(path, yield* Effect.promise(() => decodeBaseline(raw)));
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const expected = shardOf(entry.file, config.baseline);
|
|
32
|
+
if (expected !== path) {
|
|
33
|
+
return yield* new SetupFailure({ message: `${entry.file} belongs in ${expected}, not ${path}.` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
files.set(path, { entries, raw });
|
|
27
37
|
}
|
|
28
|
-
|
|
38
|
+
for (const source of ["quality/baseline.jsonl", LEGACY_BASELINE]) {
|
|
39
|
+
if (source !== config.baseline && (yield* readInput(config.root, source)) !== undefined) {
|
|
40
|
+
return yield* new SetupFailure({
|
|
41
|
+
message: `${source} holds an earlier baseline. Run \`quality baseline migrate --from ${source}\` to move it to ${config.baseline} and package shards.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { entries: [...files.values()].flatMap((file) => file.entries), files };
|
|
29
46
|
});
|
|
@@ -2,8 +2,9 @@ import type { ImportEdge } from "#imports/graph.ts";
|
|
|
2
2
|
import type { Endpoint } from "#imports/resolve.ts";
|
|
3
3
|
import type { Matcher } from "./match.ts";
|
|
4
4
|
|
|
5
|
-
export interface Reached {
|
|
5
|
+
export interface Reached<TMatch = unknown> {
|
|
6
6
|
readonly line: number;
|
|
7
|
+
readonly match: TMatch;
|
|
7
8
|
readonly path: readonly string[];
|
|
8
9
|
readonly target: Endpoint;
|
|
9
10
|
}
|
|
@@ -31,32 +32,37 @@ export function outgoing(edges: readonly ImportEdge[]): ReadonlyMap<string, read
|
|
|
31
32
|
return from;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
interface Walk {
|
|
35
|
-
readonly forbidden:
|
|
35
|
+
interface Walk<TMatch> {
|
|
36
|
+
readonly forbidden: (endpoint: Endpoint) => TMatch | undefined;
|
|
36
37
|
readonly queue: string[];
|
|
37
|
-
readonly reached: Map<string, Reached
|
|
38
|
+
readonly reached: Map<string, Reached<TMatch>>;
|
|
38
39
|
readonly trails: Map<string, Trail>;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function follow(walk: Walk
|
|
42
|
+
function follow<TMatch>(walk: Walk<TMatch>, before: Trail, edge: ImportEdge): void {
|
|
42
43
|
const label = labelOf(edge.to);
|
|
43
44
|
const trail = { line: before.path.length === 1 ? edge.line : before.line, path: [...before.path, label] };
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const match = walk.forbidden(edge.to);
|
|
46
|
+
if (match) {
|
|
47
|
+
walk.reached.set(label, walk.reached.get(label) ?? { ...trail, match, target: edge.to });
|
|
46
48
|
} else if (edge.to.kind === "file" && !walk.trails.has(edge.to.path)) {
|
|
47
49
|
walk.trails.set(edge.to.path, trail);
|
|
48
50
|
walk.queue.push(edge.to.path);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
export interface Walked {
|
|
53
|
-
readonly reached: readonly Reached[];
|
|
54
|
+
export interface Walked<TMatch = unknown> {
|
|
55
|
+
readonly reached: readonly Reached<TMatch>[];
|
|
54
56
|
readonly visited: ReadonlyMap<string, Trail>;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
// A breadth-first walk that stops at each forbidden endpoint, so a finding names the shortest path to the first forbidden step.
|
|
58
|
-
export function walkFrom(
|
|
59
|
-
|
|
60
|
+
export function walkFrom<TMatch>(
|
|
61
|
+
start: string,
|
|
62
|
+
edgesFrom: ReadonlyMap<string, readonly ImportEdge[]>,
|
|
63
|
+
forbidden: (endpoint: Endpoint) => TMatch | undefined,
|
|
64
|
+
): Walked<TMatch> {
|
|
65
|
+
const walk: Walk<TMatch> = { forbidden, queue: [start], reached: new Map(), trails: new Map([[start, { line: 0, path: [start] }]]) };
|
|
60
66
|
for (const node of walk.queue) {
|
|
61
67
|
const before = walk.trails.get(node) ?? { line: 0, path: [node] };
|
|
62
68
|
for (const edge of edgesFrom.get(node) ?? []) {
|
|
@@ -29,11 +29,11 @@ function repair(path: readonly string[]): string {
|
|
|
29
29
|
: `split ${first}: move the pure code under test into its own module that reaches no engine, and test that module in the .unit file.`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function reachFinding(file: string, reached: Reached): Finding {
|
|
32
|
+
function reachFinding(file: string, reached: Reached<string>): Finding {
|
|
33
33
|
return {
|
|
34
34
|
file,
|
|
35
35
|
line: reached.line,
|
|
36
|
-
message: `Is a unit test but reaches ${
|
|
36
|
+
message: `Is a unit test but reaches ${reached.match} (${labelOf(reached.target)}) through ${reached.path.join(" -> ")}. A unit test reaches no engine, directly or through what it imports. ${KIT} ${repair(reached.path)}`,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -76,7 +76,7 @@ export function unitTests(graph: ImportGraph, sources: readonly SourceFile[]): (
|
|
|
76
76
|
const edgesFrom = runtimeEdges(graph);
|
|
77
77
|
const byPath = new Map(sources.map((source) => [source.path, source]));
|
|
78
78
|
return (file) => {
|
|
79
|
-
const walked = walkFrom(file, edgesFrom,
|
|
79
|
+
const walked = walkFrom(file, edgesFrom, engineOf);
|
|
80
80
|
const checked = [...walked.visited].filter(([module]) => module === file || isTestSupport(module));
|
|
81
81
|
return [
|
|
82
82
|
...[...walked.reached].sort((a, b) => a.line - b.line).map((reached) => reachFinding(file, reached)),
|
|
@@ -6,17 +6,24 @@ const DOM_GLOBALS: ReadonlySet<string> = new Set(["document", "window"]);
|
|
|
6
6
|
|
|
7
7
|
const TESTING_LIBRARY = "@testing-library/";
|
|
8
8
|
|
|
9
|
-
function touchesDom(node: ts.Node): boolean {
|
|
9
|
+
function touchesDom(node: ts.Node, checker: ts.TypeChecker): boolean {
|
|
10
10
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
11
11
|
return node.moduleSpecifier.text.startsWith(TESTING_LIBRARY);
|
|
12
12
|
}
|
|
13
13
|
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && DOM_GLOBALS.has(node.expression.text)) {
|
|
14
|
-
return
|
|
14
|
+
return (checker.getSymbolAtLocation(node.expression)?.declarations ?? []).length === 0;
|
|
15
15
|
}
|
|
16
|
-
return ts.forEachChild(node, touchesDom) ?? false;
|
|
16
|
+
return ts.forEachChild(node, (child) => touchesDom(child, checker)) ?? false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function usesDom(file: SourceFile): boolean {
|
|
20
20
|
const source = parse(file);
|
|
21
|
-
|
|
21
|
+
if (source === undefined) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, noResolve: true };
|
|
25
|
+
const host = ts.createCompilerHost(options);
|
|
26
|
+
host.getSourceFile = (path) => (path === source.fileName ? source : undefined);
|
|
27
|
+
const checker = ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
28
|
+
return touchesDom(source, checker);
|
|
22
29
|
}
|