@avi2dg/checks 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +9 -10
- package/dist/data-shape/index.js +428 -0
- package/{templates → dist/templates}/reference.md +1 -1
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/dependency-rules.md +7 -1
- package/docs/configs/effect-rules.md +12 -2
- package/docs/configs/native-settings.md +10 -31
- package/docs/configs/typescript-rules.md +43 -1
- package/docs/design.md +210 -104
- package/docs/gates/checks-changelog.md +8 -5
- package/docs/gates/checks-ci-wiring.md +2 -2
- package/docs/gates/checks-comment-gate.md +3 -4
- package/docs/gates/checks-commit-identity.md +3 -3
- package/docs/gates/checks-docs.md +67 -45
- package/docs/gates/checks-exports.md +104 -0
- package/docs/gates/checks-flake.md +3 -3
- package/docs/gates/checks-lint-coverage.md +17 -9
- package/docs/gates/checks-lint.md +8 -4
- package/docs/gates/checks-mutation-compare.md +3 -3
- package/docs/gates/checks-quarantine-clock.md +3 -5
- package/docs/gates/checks-release-notes.md +4 -4
- package/docs/gates/checks-release-report.md +3 -3
- package/docs/gates/checks-repetition.md +2 -1
- package/docs/gates/checks-subsumed-tests.md +3 -3
- package/docs/gates/checks-suppressions-ratchet.md +3 -4
- package/docs/gates/checks-test-layout.md +4 -4
- package/docs/gates/checks-test.md +3 -2
- package/docs/gates/checks-unused.md +4 -4
- package/docs/gates/checks-vendor.md +3 -3
- package/oxlintrc.json +22 -2
- package/package.json +17 -9
- package/src/complexity/exports.ts +188 -0
- package/src/complexity/knip.ts +70 -0
- package/src/complexity/unused.ts +7 -56
- package/src/core/gates.ts +2 -1
- package/src/core/swc.ts +1 -1
- package/src/delivery/ci-wiring.ts +1 -1
- package/src/dependencies/vendor.ts +4 -5
- package/src/docs/doc-names.ts +128 -0
- package/src/docs/doc-references.ts +1 -1
- package/src/docs/doc-snapshot.ts +1 -1
- package/src/docs/doc-templates.ts +4 -4
- package/src/docs/docs.ts +17 -14
- package/src/docs/prose-matchers.ts +11 -1
- package/src/quality/comments.ts +2 -2
- package/src/quality/lint-coverage.sh +35 -2
- package/{presets → src/quality/presets}/effect.language-service.json +3 -1
- package/src/testing/mutation-compare.ts +51 -45
- package/src/testing/test-layout.ts +1 -1
- package/src/testing/test-report.ts +1 -1
- package/ts-reset.d.ts +2 -0
- package/tsconfig.effect.json +3 -0
- /package/{templates → dist/templates}/adr.md +0 -0
- /package/{templates → dist/templates}/agents.md +0 -0
- /package/{templates → dist/templates}/changelog.md +0 -0
- /package/{templates → dist/templates}/claude.md +0 -0
- /package/{templates → dist/templates}/explanation.md +0 -0
- /package/{templates → dist/templates}/how-to.md +0 -0
- /package/{templates → dist/templates}/readme.md +0 -0
- /package/{templates → dist/templates}/tutorial.md +0 -0
- /package/{presets → src/quality/presets}/effect.oxlint.json +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
3
|
+
import { collect, git, pathsAt } from "../core/git.ts";
|
|
4
|
+
import type { Unresolved } from "./doc-references.ts";
|
|
5
|
+
import { scanMarkdown } from "./prose-matchers.ts";
|
|
6
|
+
|
|
7
|
+
export type VanishedName = { readonly path: string; readonly line: number; readonly named: string; readonly message: string };
|
|
8
|
+
|
|
9
|
+
const OUTSIDE_DOCS = [".", ":(exclude)*.md"];
|
|
10
|
+
const NOT_ONE_NAME = /\s|<[^>]*>|^-/;
|
|
11
|
+
const HAS_LETTER = /[A-Za-z]/;
|
|
12
|
+
|
|
13
|
+
function nameOf(span: string, ownPackage: string | undefined): string | undefined {
|
|
14
|
+
if (span.length < 3 || !HAS_LETTER.test(span) || NOT_ONE_NAME.test(span)) return undefined;
|
|
15
|
+
let name = span
|
|
16
|
+
.replace(/^(["'])(.*)\1$/, "$2")
|
|
17
|
+
.replace(/^(?:\.|~)\//, "")
|
|
18
|
+
.replace(/^node_modules\//, "");
|
|
19
|
+
if (ownPackage !== undefined && name.startsWith(`${ownPackage}/`)) name = name.slice(ownPackage.length + 1);
|
|
20
|
+
return name.length < 3 ? undefined : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function namesIn(texts: ReadonlyMap<string, string>, ownPackage: string | undefined): readonly { path: string; line: number; name: string }[] {
|
|
24
|
+
return [...texts].flatMap(([path, text]) =>
|
|
25
|
+
scanMarkdown(text).flatMap((line) =>
|
|
26
|
+
line.kind === "code" || line.kind === "front-matter"
|
|
27
|
+
? []
|
|
28
|
+
: [...new Set(line.code)].flatMap((span) => {
|
|
29
|
+
const name = nameOf(span, ownPackage);
|
|
30
|
+
return name === undefined ? [] : [{ path, line: line.line, name }];
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class InstalledUnreadable extends Schema.TaggedError<InstalledUnreadable>()("InstalledUnreadable", {
|
|
37
|
+
message: Schema.String,
|
|
38
|
+
}) {}
|
|
39
|
+
|
|
40
|
+
const Group = Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown));
|
|
41
|
+
const Manifest = Schema.Struct({
|
|
42
|
+
name: Schema.optionalKey(Schema.String),
|
|
43
|
+
dependencies: Group,
|
|
44
|
+
devDependencies: Group,
|
|
45
|
+
peerDependencies: Group,
|
|
46
|
+
optionalDependencies: Group,
|
|
47
|
+
});
|
|
48
|
+
const decodeManifest = Schema.decodeUnknownOption(Schema.fromJsonString(Manifest));
|
|
49
|
+
const NO_MANIFEST: typeof Manifest.Type = {};
|
|
50
|
+
|
|
51
|
+
// git grep exits 1 when nothing matches, which is an answer rather than a failure.
|
|
52
|
+
const matchedAt = Effect.fn("matchedAt")(function* (root: string, rev: string, names: readonly string[]) {
|
|
53
|
+
if (names.length === 0) return new Set<string>();
|
|
54
|
+
const found = yield* git(["grep", "-I", "-F", "-o", "-h", "--no-color", "-f", "-", rev, "--", ...OUTSIDE_DOCS], root, {
|
|
55
|
+
input: `${names.join("\n")}\n`,
|
|
56
|
+
}).pipe(Effect.catchTag("GitFailure", () => Effect.succeed("")));
|
|
57
|
+
const paths = (yield* pathsAt(rev, [], root)).join("\n");
|
|
58
|
+
return new Set([...found.split("\n"), ...names.filter((name) => paths.includes(name))]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// -o reports one pattern per match, so a name inside a longer name's match is confirmed on its own.
|
|
62
|
+
const heldAt = Effect.fn("heldAt")(function* (root: string, rev: string, name: string) {
|
|
63
|
+
return yield* git(["grep", "-I", "-F", "-q", "-e", name, rev, "--", ...OUTSIDE_DOCS], root).pipe(
|
|
64
|
+
Effect.as(true),
|
|
65
|
+
Effect.catchTag("GitFailure", () => Effect.succeed(false)),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const manifestAt = Effect.fn("manifestAt")(function* (root: string, head: string) {
|
|
70
|
+
const manifest = yield* git(["show", `${head}:package.json`], root).pipe(Effect.catchTag("GitFailure", () => Effect.succeed("")));
|
|
71
|
+
const { name, dependencies, devDependencies, peerDependencies, optionalDependencies } = Option.getOrElse(decodeManifest(manifest), () => NO_MANIFEST);
|
|
72
|
+
const deps = [dependencies, devDependencies, peerDependencies, optionalDependencies].flatMap((group) => Object.keys(group ?? {}));
|
|
73
|
+
return { name, deps: [...new Set(deps)] };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const installedDirs = Effect.fn("installedDirs")(function* (root: string, deps: readonly string[]) {
|
|
77
|
+
const fs = yield* FileSystem.FileSystem;
|
|
78
|
+
const path = yield* Path.Path;
|
|
79
|
+
const dirs = deps.map((dep) => `node_modules/${dep}/`);
|
|
80
|
+
const present = yield* Effect.forEach(dirs, (dir) => fs.exists(path.join(root, dir)), { concurrency: 8 });
|
|
81
|
+
return dirs.filter((_, index) => present[index] === true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const installedHolds = Effect.fn("installedHolds")(function* (root: string, dirs: readonly string[], name: string) {
|
|
85
|
+
if (dirs.length === 0) return false;
|
|
86
|
+
const { stderr, exitCode } = yield* collect("grep", ["-r", "-I", "-F", "-q", "-e", name, "--", ...dirs], root);
|
|
87
|
+
if (exitCode === ChildProcessSpawner.ExitCode(0)) return true;
|
|
88
|
+
if (exitCode === ChildProcessSpawner.ExitCode(1)) return false;
|
|
89
|
+
return yield* new InstalledUnreadable({ message: `grep for \`${name}\` in ${dirs.join(", ")}: ${stderr.trim()}` });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const vanishedNames = Effect.fn("vanishedNames")(function* (
|
|
93
|
+
root: string,
|
|
94
|
+
base: string,
|
|
95
|
+
head: string,
|
|
96
|
+
texts: ReadonlyMap<string, string>,
|
|
97
|
+
failed: readonly (Unresolved & { readonly path: string })[],
|
|
98
|
+
) {
|
|
99
|
+
const manifest = yield* manifestAt(root, head);
|
|
100
|
+
const named = namesIn(texts, manifest.name);
|
|
101
|
+
const names = [...new Set(named.map(({ name }) => name))];
|
|
102
|
+
const atHead = yield* matchedAt(root, head, names);
|
|
103
|
+
const candidates = names.filter((name) => !atHead.has(name));
|
|
104
|
+
const atBase = yield* matchedAt(root, base, candidates);
|
|
105
|
+
const heldAtBase = yield* Effect.forEach(candidates, (name) => (atBase.has(name) ? Effect.succeed(true) : heldAt(root, base, name)), {
|
|
106
|
+
concurrency: 8,
|
|
107
|
+
});
|
|
108
|
+
const gone = candidates.filter((_, index) => heldAtBase[index] === true);
|
|
109
|
+
const confirmed = yield* Effect.forEach(gone, (name) => heldAt(root, head, name), { concurrency: 8 });
|
|
110
|
+
const unheld = gone.filter((_, index) => confirmed[index] === false);
|
|
111
|
+
const dirs = yield* installedDirs(root, manifest.deps);
|
|
112
|
+
const installed = yield* Effect.forEach(unheld, (name) => installedHolds(root, dirs, name), { concurrency: 8 });
|
|
113
|
+
const vanished = new Set(unheld.filter((_, index) => installed[index] === false));
|
|
114
|
+
const reported = new Set(
|
|
115
|
+
failed.flatMap((one) => (one.kind === "path" ? [`${one.path}:${one.line}:${nameOf(one.named, manifest.name) ?? one.named}`] : [])),
|
|
116
|
+
);
|
|
117
|
+
return named
|
|
118
|
+
.filter(({ name }) => vanished.has(name))
|
|
119
|
+
.filter(({ path, line, name }) => !reported.has(`${path}:${line}:${name}`))
|
|
120
|
+
.map(
|
|
121
|
+
({ path, line, name }): VanishedName => ({
|
|
122
|
+
path,
|
|
123
|
+
line,
|
|
124
|
+
named: name,
|
|
125
|
+
message: `names \`${name}\`, which the range removed from every file outside the docs. Say what holds now, or drop the line`,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
});
|
package/src/docs/doc-snapshot.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, FileSystem, Path, Schema } from "effect";
|
|
|
2
2
|
import { anchoredTargets, anchorsOf, PACKAGE_MANIFEST, packagesOf, snapshotOf, type Unresolved } from "./doc-references.ts";
|
|
3
3
|
import { git, pathsAt } from "../core/git.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
class ManifestUnreadable extends Schema.TaggedError<ManifestUnreadable>()("ManifestUnreadable", {
|
|
6
6
|
message: Schema.String,
|
|
7
7
|
}) {}
|
|
8
8
|
|
|
@@ -9,21 +9,21 @@ export type Title =
|
|
|
9
9
|
| { readonly type: "fixed"; readonly text: string }
|
|
10
10
|
| { readonly type: "open"; readonly placeholder: string; readonly rule: HeadingRule; readonly prefix?: string };
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type Outlined = {
|
|
13
13
|
readonly shape: "outline";
|
|
14
14
|
readonly title: Title;
|
|
15
15
|
readonly lead: readonly string[];
|
|
16
16
|
readonly sections: readonly Slot[];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
type Exact = {
|
|
20
20
|
readonly shape: "exact";
|
|
21
21
|
readonly text: string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export type Template = Outlined | Exact;
|
|
25
25
|
|
|
26
|
-
export const TEMPLATE_DIRECTORY = "templates";
|
|
26
|
+
export const TEMPLATE_DIRECTORY = "dist/templates";
|
|
27
27
|
|
|
28
28
|
export const ADR_STATUSES = ["Proposed", "Accepted", "Rejected", "Deprecated", "Superseded", "Retired"] as const;
|
|
29
29
|
|
|
@@ -154,7 +154,7 @@ export const TEMPLATES: Readonly<Record<Kind, Template>> = {
|
|
|
154
154
|
reference: {
|
|
155
155
|
shape: "outline",
|
|
156
156
|
title: { type: "open", placeholder: "<The thing this page describes, as a noun>", rule: "any" },
|
|
157
|
-
lead: ["<What the thing is, in one sentence
|
|
157
|
+
lead: ["<What the thing is, in one sentence.>"],
|
|
158
158
|
sections: [
|
|
159
159
|
open("<One part of it, as a noun>", "any", optional("the lead and one table describe all of it"), [
|
|
160
160
|
"<A table, a list or a short description, with no steps and no opinion.>",
|
package/src/docs/docs.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { Console, Effect } from "effect";
|
|
3
3
|
import { rootsOf, unresolvedIn, type Judging, type Unresolved } from "./doc-references.ts";
|
|
4
4
|
import { ADR_DIRECTORY, judge, placementOf, placementProblem, speaksToConsumers, type Placement } from "./doc-rules.ts";
|
|
5
|
+
import { vanishedNames } from "./doc-names.ts";
|
|
5
6
|
import { readTexts, snapshotAt, stillMissing } from "./doc-snapshot.ts";
|
|
6
7
|
import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
7
8
|
import { runMain } from "../core/main.ts";
|
|
8
|
-
import {
|
|
9
|
+
import { proseFindings, readerOf } from "./prose-matchers.ts";
|
|
9
10
|
|
|
10
11
|
type Finding = {
|
|
11
12
|
readonly path: string;
|
|
@@ -16,7 +17,7 @@ type Finding = {
|
|
|
16
17
|
type Judged = {
|
|
17
18
|
readonly held: readonly string[];
|
|
18
19
|
readonly edited: { readonly docs: number; readonly lines: number };
|
|
19
|
-
readonly
|
|
20
|
+
readonly named: number;
|
|
20
21
|
readonly findings: readonly Finding[];
|
|
21
22
|
readonly advisory: ReadonlyMap<string, number>;
|
|
22
23
|
readonly brokenBefore: readonly Finding[];
|
|
@@ -74,8 +75,10 @@ const referenceFindings = Effect.fn("referenceFindings")(function* (range: Range
|
|
|
74
75
|
const elsewhere = found.filter((one) => !onChangedLines(one));
|
|
75
76
|
const before = elsewhere.length === 0 ? new Set<string>() : yield* brokenBeforeRange(range, elsewhere, judging);
|
|
76
77
|
const finding = ({ path, line, message }: Located): Finding => ({ path, line, message });
|
|
78
|
+
const failed = found.filter((one) => onChangedLines(one) || !before.has(keyOf(one)));
|
|
77
79
|
return {
|
|
78
|
-
failing:
|
|
80
|
+
failing: failed.map(finding),
|
|
81
|
+
failed,
|
|
79
82
|
brokenBefore: elsewhere.filter((one) => before.has(keyOf(one))).map(finding),
|
|
80
83
|
};
|
|
81
84
|
});
|
|
@@ -87,7 +90,6 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
87
90
|
const changed = yield* changedLines(base, head, MARKDOWN, root);
|
|
88
91
|
const present = yield* pathsAt(head, MARKDOWN, root);
|
|
89
92
|
const records = present.filter((path) => path.startsWith(ADR_DIRECTORY));
|
|
90
|
-
const living = present.filter(isLivingDoc);
|
|
91
93
|
const proseDocs = present.flatMap((path) => {
|
|
92
94
|
const reader = readerOf(path);
|
|
93
95
|
return reader === undefined ? [] : [{ path, reader }];
|
|
@@ -104,18 +106,16 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
104
106
|
const judging = (path: string): Judging => ({ commands: !speaksToConsumers(text(path)) });
|
|
105
107
|
// A directory the range deletes still belongs to this repository, so a path under it is stale rather than another repository's.
|
|
106
108
|
const roots = rootsOf(yield* pathsAt(base, [], root));
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
judging,
|
|
111
|
-
);
|
|
109
|
+
const referenced = new Map(proseDocs.map(({ path }) => [path, text(path)]));
|
|
110
|
+
const references = yield* referenceFindings({ root, base, head, roots, changed, renamedFrom }, referenced, judging);
|
|
111
|
+
const vanished = yield* vanishedNames(root, base, head, referenced, references.failed);
|
|
112
112
|
const advisory = new Map<string, number>();
|
|
113
113
|
for (const { path } of templated.filter((finding) => !touched.has(finding.path))) advisory.set(path, (advisory.get(path) ?? 0) + 1);
|
|
114
114
|
return {
|
|
115
115
|
held: judged.map(({ path }) => path).filter((path) => touched.has(path)),
|
|
116
116
|
edited: { docs: edited.length, lines: edited.reduce((sum, { path }) => sum + (changed.get(path)?.size ?? 0), 0) },
|
|
117
|
-
|
|
118
|
-
findings: [...templated.filter((finding) => touched.has(finding.path)), ...prose, ...references.failing].toSorted(inPathOrder),
|
|
117
|
+
named: referenced.size,
|
|
118
|
+
findings: [...templated.filter((finding) => touched.has(finding.path)), ...prose, ...references.failing, ...vanished].toSorted(inPathOrder),
|
|
119
119
|
advisory,
|
|
120
120
|
brokenBefore: references.brokenBefore.toSorted(inPathOrder),
|
|
121
121
|
} satisfies Judged;
|
|
@@ -125,13 +125,13 @@ function describe({ path, line, message }: Finding): string {
|
|
|
125
125
|
return ` ${path}${line === undefined ? "" : `:${line}`}: ${message}`;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export function report({ held, edited,
|
|
128
|
+
export function report({ held, edited, named, findings, advisory, brokenBefore }: Judged): string {
|
|
129
129
|
const verdict =
|
|
130
130
|
findings.length === 0
|
|
131
131
|
? [
|
|
132
132
|
`${NAME}: ${held.length} doc file(s) the range touches hold to their templates`,
|
|
133
133
|
`${NAME}: ${edited.lines} line(s) the range adds or edits in ${edited.docs} living doc(s) or agent file(s) hold to the prose rules`,
|
|
134
|
-
`${NAME}: the range breaks no path, link or command the ${
|
|
134
|
+
`${NAME}: the range breaks no path, link or command the ${named} living doc(s) or agent file(s) name`,
|
|
135
135
|
]
|
|
136
136
|
: [`${NAME}: ${findings.length} violation(s):`, ...findings.map(describe)];
|
|
137
137
|
const unconformed =
|
|
@@ -144,7 +144,10 @@ export function report({ held, edited, living, findings, advisory, brokenBefore
|
|
|
144
144
|
const broken =
|
|
145
145
|
brokenBefore.length === 0
|
|
146
146
|
? []
|
|
147
|
-
: [
|
|
147
|
+
: [
|
|
148
|
+
`${NAME}: advisory, ${brokenBefore.length} path(s), link(s) or command(s) the living docs or agent files name were broken before the range:`,
|
|
149
|
+
...brokenBefore.map(describe),
|
|
150
|
+
];
|
|
148
151
|
return [...verdict, ...unconformed, ...broken].join("\n");
|
|
149
152
|
}
|
|
150
153
|
|
|
@@ -195,7 +195,7 @@ function unread(line: number, raw: string, kind: LineKind): MarkdownLine {
|
|
|
195
195
|
return { line, kind, raw, prose: BLANK.repeat(raw.length), code: [], links: [] };
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
function settext(lines: MarkdownLine[]): MarkdownLine[] {
|
|
198
|
+
function settext(lines: readonly MarkdownLine[]): MarkdownLine[] {
|
|
199
199
|
return lines.map((line, index) => {
|
|
200
200
|
const next = lines[index + 1];
|
|
201
201
|
return line.kind === "prose" && next?.kind === "break" && UNDERLINE.test(next.raw) ? { ...line, kind: "heading" } : line;
|
|
@@ -257,6 +257,12 @@ const PROMISES: readonly (readonly [shows: string, pattern: string])[] = [
|
|
|
257
257
|
["coming soon", "coming soon"],
|
|
258
258
|
["in a future release", "in a future (?:release|version)"],
|
|
259
259
|
];
|
|
260
|
+
const PAST: readonly (readonly [shows: string, pattern: string])[] = [
|
|
261
|
+
["formerly", String.raw`former(?:ly)?`], ["previously", "previously"], ["as before", "as before"],
|
|
262
|
+
["used to", String.raw`(?<!\b(?:is|are|was|were|be|been|being|get|gets|got|\w+['’](?:s|re))\s(?:(?:also|always|just|never|not|now|often|sometimes|still|then|\w+ly)\s)?)used to`],
|
|
263
|
+
["was replaced", String.raw`(?:was|were|has been|have been) (?:replaced|removed|renamed|dropped|deleted|retired|superseded)`],
|
|
264
|
+
["moved from", String.raw`(?:renamed|moved) from`], ["new owner", String.raw`new (?:home|owner)`],
|
|
265
|
+
];
|
|
260
266
|
|
|
261
267
|
const code = (text: string): string => `\`${text}\``;
|
|
262
268
|
const EVERY_READER: readonly Reader[] = ["people", "agents"];
|
|
@@ -287,6 +293,10 @@ const RULES: readonly MatchedRule[] = [
|
|
|
287
293
|
instead: "Say what is true now",
|
|
288
294
|
find: new RegExp(PROMISES.map(([, pattern]) => String.raw`\b${pattern}\b`).join("|"), "gi"),
|
|
289
295
|
},
|
|
296
|
+
{ readers: EVERY_READER, refuses: "a report about the past",
|
|
297
|
+
example: PAST.map(([shows]) => code(shows)).join(", "),
|
|
298
|
+
instead: "Say what is true now, and leave what changed to the changelog, a commit message or a decision record",
|
|
299
|
+
find: new RegExp(PAST.map(([, pattern]) => String.raw`(?<![\w-])${pattern}(?![\w-])`).join("|"), "gi") },
|
|
290
300
|
{
|
|
291
301
|
readers: PEOPLE,
|
|
292
302
|
refuses: "a sentence that opens by talking about the page",
|
package/src/quality/comments.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { commentsIn, refusalsIn, syntaxOf, unreadable, type Syntax } from "./comment-matchers.ts";
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export { syntaxOf } from "./comment-matchers.ts";
|
|
5
5
|
|
|
6
6
|
export class UnreadableCode extends Schema.TaggedError<UnreadableCode>()("UnreadableCode", {
|
|
7
7
|
message: Schema.String,
|
|
8
8
|
}) {}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function syntaxFor(path: string): Effect.Effect<Syntax, UnreadableCode> {
|
|
11
11
|
const syntax = syntaxOf(path);
|
|
12
12
|
return syntax ? Effect.succeed(syntax) : Effect.fail(new UnreadableCode({ message: unreadable(path) }));
|
|
13
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# lint-coverage: fail when oxlint silently skips a tracked TypeScript source
|
|
2
|
+
# lint-coverage: fail when oxlint silently skips a tracked TypeScript source,
|
|
3
|
+
# or when tsconfig.json's program silently drops the ts-reset rules.
|
|
3
4
|
set -eu
|
|
4
5
|
|
|
5
6
|
tmp="$(mktemp -d)"
|
|
@@ -21,13 +22,45 @@ grep -E '\.tsx?$' "$tmp/walk" | LC_ALL=C sort > "$tmp/walked" || true
|
|
|
21
22
|
|
|
22
23
|
expected_count="$(wc -l < "$tmp/expected" | tr -d ' ')"
|
|
23
24
|
walked_count="$(grep -c . "$tmp/walked" || true)"
|
|
25
|
+
status=0
|
|
24
26
|
|
|
25
27
|
comm -23 "$tmp/expected" "$tmp/walked" > "$tmp/missing"
|
|
26
28
|
if [ -s "$tmp/missing" ]; then
|
|
27
29
|
missing_count="$(wc -l < "$tmp/missing" | tr -d ' ')"
|
|
28
30
|
echo "lint-coverage: oxlint skips ${missing_count}/${expected_count} tracked .ts/.tsx files; missing:"
|
|
29
31
|
cat "$tmp/missing"
|
|
32
|
+
status=1
|
|
33
|
+
else
|
|
34
|
+
echo "lint-coverage: ${walked_count}/${expected_count} tracked .ts/.tsx files"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if ! [ -f tsconfig.json ]; then
|
|
38
|
+
echo "lint-coverage: no tsconfig.json, so no program to hold the ts-reset rules"
|
|
39
|
+
exit "$status"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if ! tsc --listFilesOnly -p tsconfig.json > "$tmp/program" 2> "$tmp/program-error"; then
|
|
43
|
+
echo "lint-coverage: tsc could not list the program tsconfig.json builds:"
|
|
44
|
+
# tsc lists each file of the program as an absolute path beside its errors.
|
|
45
|
+
grep -v '^/' "$tmp/program" || true
|
|
46
|
+
cat "$tmp/program-error"
|
|
47
|
+
if [ "$status" -eq 1 ]; then
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
dropped=""
|
|
54
|
+
for rule in is-array json-parse; do
|
|
55
|
+
if ! grep -q "/@total-typescript/ts-reset/dist/${rule}\.d\.ts\$" "$tmp/program"; then
|
|
56
|
+
dropped="${dropped} ${rule}"
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
if [ -n "$dropped" ]; then
|
|
60
|
+
echo "lint-coverage: the program tsconfig.json builds drops the ts-reset rules:${dropped}"
|
|
61
|
+
echo " extend @avi2dg/checks/tsconfig.effect.json, and set files or include in tsconfig.json but not both"
|
|
30
62
|
exit 1
|
|
31
63
|
fi
|
|
32
64
|
|
|
33
|
-
echo "lint-coverage:
|
|
65
|
+
echo "lint-coverage: the program tsconfig.json builds holds the ts-reset rules is-array and json-parse"
|
|
66
|
+
exit "$status"
|
|
@@ -2,27 +2,43 @@
|
|
|
2
2
|
import { Console, Effect, FileSystem, Schema } from "effect";
|
|
3
3
|
import { runMain, Usage } from "../core/main.ts";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const PositionSchema = Schema.Struct({ line: Schema.Finite, column: Schema.Finite });
|
|
6
|
+
const LocationSchema = Schema.Struct({ start: PositionSchema, end: PositionSchema });
|
|
7
|
+
|
|
8
|
+
export type Location = typeof LocationSchema.Type;
|
|
9
|
+
|
|
10
|
+
const MutantStatusSchema = Schema.Literals([
|
|
11
|
+
"Killed",
|
|
12
|
+
"Survived",
|
|
13
|
+
"Timeout",
|
|
14
|
+
"NoCoverage",
|
|
15
|
+
"Ignored",
|
|
16
|
+
"CompileError",
|
|
17
|
+
"RuntimeError",
|
|
18
|
+
"Pending",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export type MutantStatus = typeof MutantStatusSchema.Type;
|
|
22
|
+
|
|
23
|
+
const MutantSchema = Schema.Struct({
|
|
24
|
+
status: MutantStatusSchema,
|
|
25
|
+
mutatorName: Schema.String,
|
|
26
|
+
replacement: Schema.String,
|
|
27
|
+
killedBy: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
28
|
+
location: LocationSchema,
|
|
29
|
+
});
|
|
9
30
|
|
|
10
|
-
export type Mutant =
|
|
11
|
-
readonly status: string;
|
|
12
|
-
readonly mutatorName: string;
|
|
13
|
-
readonly replacement: string;
|
|
14
|
-
readonly killedBy?: readonly string[];
|
|
15
|
-
readonly location: Location;
|
|
16
|
-
};
|
|
31
|
+
export type Mutant = typeof MutantSchema.Type;
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
readonly source: string;
|
|
20
|
-
readonly mutants: readonly Mutant[];
|
|
21
|
-
};
|
|
33
|
+
const ReportFileSchema = Schema.Struct({ source: Schema.String, mutants: Schema.Array(MutantSchema) });
|
|
22
34
|
|
|
23
|
-
export type
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
export type ReportFile = typeof ReportFileSchema.Type;
|
|
36
|
+
|
|
37
|
+
const TestFileSchema = Schema.Struct({
|
|
38
|
+
tests: Schema.Array(Schema.Struct({ id: Schema.String, name: Schema.String })),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type TestFile = typeof TestFileSchema.Type;
|
|
26
42
|
|
|
27
43
|
export type KillRun = {
|
|
28
44
|
readonly files: ReadonlyMap<string, ReportFile>;
|
|
@@ -35,8 +51,8 @@ export type MutantChange = {
|
|
|
35
51
|
readonly location: Location;
|
|
36
52
|
readonly mutatorName: string;
|
|
37
53
|
readonly replacement: string;
|
|
38
|
-
readonly from:
|
|
39
|
-
readonly to:
|
|
54
|
+
readonly from: MutantStatus;
|
|
55
|
+
readonly to: MutantStatus;
|
|
40
56
|
};
|
|
41
57
|
|
|
42
58
|
export type UnmatchedMutant = {
|
|
@@ -44,7 +60,7 @@ export type UnmatchedMutant = {
|
|
|
44
60
|
readonly location: Location;
|
|
45
61
|
readonly mutatorName: string;
|
|
46
62
|
readonly replacement: string;
|
|
47
|
-
readonly status:
|
|
63
|
+
readonly status: MutantStatus;
|
|
48
64
|
};
|
|
49
65
|
|
|
50
66
|
export type Comparison = {
|
|
@@ -65,35 +81,25 @@ export class ReportError extends Schema.TaggedError<ReportError>()("ReportError"
|
|
|
65
81
|
message: Schema.String,
|
|
66
82
|
}) {}
|
|
67
83
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
const OUTCOME = {
|
|
85
|
+
Killed: "detected",
|
|
86
|
+
Timeout: "detected",
|
|
87
|
+
Survived: "undetected",
|
|
88
|
+
NoCoverage: "undetected",
|
|
89
|
+
CompileError: "leaves-score",
|
|
90
|
+
RuntimeError: "leaves-score",
|
|
91
|
+
Ignored: "leaves-score",
|
|
92
|
+
Pending: "leaves-score",
|
|
93
|
+
} as const satisfies Record<MutantStatus, "detected" | "undetected" | "leaves-score">;
|
|
71
94
|
const USAGE = "usage: mutation-compare.ts [--advisory] <base-report> <head-report>";
|
|
72
95
|
|
|
73
|
-
const Files = Schema.Record(
|
|
74
|
-
Schema.String,
|
|
75
|
-
Schema.Struct({
|
|
76
|
-
source: Schema.String,
|
|
77
|
-
mutants: Schema.Array(
|
|
78
|
-
Schema.Struct({
|
|
79
|
-
status: Schema.String,
|
|
80
|
-
mutatorName: Schema.String,
|
|
81
|
-
replacement: Schema.String,
|
|
82
|
-
killedBy: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
83
|
-
location: Schema.Struct({
|
|
84
|
-
start: Schema.Struct({ line: Schema.Finite, column: Schema.Finite }),
|
|
85
|
-
end: Schema.Struct({ line: Schema.Finite, column: Schema.Finite }),
|
|
86
|
-
}),
|
|
87
|
-
}),
|
|
88
|
-
),
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
96
|
+
const Files = Schema.Record(Schema.String, ReportFileSchema);
|
|
91
97
|
const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ files: Files })));
|
|
92
98
|
const decodeKillRun = Schema.decodeUnknownEffect(
|
|
93
99
|
Schema.fromJsonString(
|
|
94
100
|
Schema.Struct({
|
|
95
101
|
files: Files,
|
|
96
|
-
testFiles: Schema.Record(Schema.String,
|
|
102
|
+
testFiles: Schema.Record(Schema.String, TestFileSchema),
|
|
97
103
|
config: Schema.optionalKey(Schema.Struct({ disableBail: Schema.optionalKey(Schema.Boolean) })),
|
|
98
104
|
}),
|
|
99
105
|
),
|
|
@@ -211,9 +217,9 @@ function judgeMatch(path: string, mutant: UnmatchedMutant, counterpart: Unmatche
|
|
|
211
217
|
from: mutant.status,
|
|
212
218
|
to: counterpart.status,
|
|
213
219
|
};
|
|
214
|
-
if (mutant.status !== counterpart.status && (
|
|
220
|
+
if (mutant.status !== counterpart.status && (OUTCOME[mutant.status] === "leaves-score" || OUTCOME[counterpart.status] === "leaves-score")) {
|
|
215
221
|
findings.moves.push(change);
|
|
216
|
-
} else if (
|
|
222
|
+
} else if (OUTCOME[mutant.status] === "detected" && OUTCOME[counterpart.status] === "undetected") {
|
|
217
223
|
findings.regressions.push(change);
|
|
218
224
|
}
|
|
219
225
|
}
|
|
@@ -177,7 +177,7 @@ function callUse(node: Record<string, unknown>): string | undefined {
|
|
|
177
177
|
const callee = node["callee"];
|
|
178
178
|
const calleeName = identifierName(callee);
|
|
179
179
|
if (calleeName !== undefined && BANNED_GLOBAL_CALLS.includes(calleeName)) return `calls ${calleeName}`;
|
|
180
|
-
const argument = Array.isArray(node["arguments"]) ? node["arguments"][0] : undefined;
|
|
180
|
+
const argument: unknown = Array.isArray(node["arguments"]) ? node["arguments"][0] : undefined;
|
|
181
181
|
const specifier = isRecord(argument) ? stringValue(argument["expression"]) : undefined;
|
|
182
182
|
const banned = specifier === undefined ? undefined : bannedModule(specifier);
|
|
183
183
|
if (banned === undefined) return undefined;
|
package/ts-reset.d.ts
ADDED
package/tsconfig.effect.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|