@shivaedev/quality 0.17.0 → 0.19.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 +54 -0
- package/README.md +151 -508
- 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/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js.map +1 -1
- 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/run.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 +10 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +25 -3
- 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.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/lint.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.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/cli/shrink.d.ts.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +3 -3
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +5 -2
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +11 -6
- 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 -11
- 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/baseline-file.d.ts.map +1 -1
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/session.d.ts.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/exceptions/registry.d.ts.map +1 -1
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/hooks/command.d.ts +13 -0
- package/dist/hooks/command.d.ts.map +1 -0
- package/dist/hooks/command.js +20 -0
- package/dist/hooks/command.js.map +1 -0
- package/dist/hooks/install.d.ts +11 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +52 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/location.d.ts +9 -0
- package/dist/hooks/location.d.ts.map +1 -0
- package/dist/hooks/location.js +22 -0
- package/dist/hooks/location.js.map +1 -0
- package/dist/hooks/preCommit.d.ts +5 -0
- package/dist/hooks/preCommit.d.ts.map +1 -0
- package/dist/hooks/preCommit.js +50 -0
- package/dist/hooks/preCommit.js.map +1 -0
- package/dist/hooks/shim.d.ts +4 -0
- package/dist/hooks/shim.d.ts.map +1 -0
- package/dist/hooks/shim.js +26 -0
- package/dist/hooks/shim.js.map +1 -0
- package/dist/hooks/uninstall.d.ts +4 -0
- package/dist/hooks/uninstall.d.ts.map +1 -0
- package/dist/hooks/uninstall.js +19 -0
- package/dist/hooks/uninstall.js.map +1 -0
- 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/fences/match.js.map +1 -1
- package/dist/imports/graph.js +2 -2
- package/dist/imports/graph.js.map +1 -1
- 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 +32 -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/filesystem.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.d.ts.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/naming/testName.d.ts.map +1 -1
- package/dist/report/plural.d.ts.map +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/stale.d.ts.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/kinds.d.ts.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/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/scan.d.ts.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 +98 -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 +41 -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/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts.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/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +1 -1
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/syntax.d.ts.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/vitest.d.ts +1 -3
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +11 -6
- package/dist/vitest.js.map +1 -1
- 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 +308 -0
- package/docs/tooling.md +120 -0
- package/package.json +8 -6
- 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 +29 -3
- package/src/cli/baseline.ts +3 -3
- package/src/cli/fix.ts +13 -13
- package/src/cli/run-main.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/config/decode.ts +7 -2
- package/src/config/load.ts +15 -6
- package/src/config/resolve.ts +10 -15
- package/src/config.ts +8 -9
- package/src/decoded.ts +3 -3
- package/src/engine/session.ts +3 -3
- package/src/engine/violation.ts +2 -2
- package/src/hooks/command.ts +34 -0
- package/src/hooks/install.ts +69 -0
- package/src/hooks/location.ts +28 -0
- package/src/hooks/preCommit.ts +68 -0
- package/src/hooks/shim.ts +28 -0
- package/src/hooks/uninstall.ts +19 -0
- 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/projects.ts +2 -2
- package/src/imports/runtime.ts +37 -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/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 +116 -0
- package/src/rules/imports/noTestCode.ts +18 -0
- package/src/rules/imports/runtime.ts +43 -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/vitest.ts +11 -6
package/src/inventory/walk.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { join, relative, sep } from "node:path";
|
|
1
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
2
2
|
import { Effect, FileSystem } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { FilesystemFailure, orWhenAbsent, readOptionalText } from "./filesystem.ts";
|
|
4
4
|
import { emptyScope, type IgnoreScope, insideKept, verdictFor, withIgnoreFile } from "./ignore-scope.ts";
|
|
5
|
+
import { trackedPaths } from "./trackedPaths.ts";
|
|
5
6
|
|
|
6
7
|
const SKIPPED = new Set([".git", "node_modules"]);
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export function ignoreScopeAt(
|
|
9
10
|
dir: string,
|
|
10
11
|
inherited: IgnoreScope = emptyScope,
|
|
11
|
-
): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem>
|
|
12
|
-
Effect.map(readOptionalText(join(dir, ".gitignore")), (contents) =>
|
|
12
|
+
): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> {
|
|
13
|
+
return Effect.map(readOptionalText(join(dir, ".gitignore")), (contents) =>
|
|
13
14
|
contents === undefined || contents === "" ? inherited : withIgnoreFile(inherited, dir, contents),
|
|
14
15
|
);
|
|
16
|
+
}
|
|
15
17
|
|
|
16
18
|
// The walk itself never visits the directories between the root and a source, so their ignore files are read here.
|
|
17
|
-
export
|
|
19
|
+
export function scopeAbove(root: string, dir: string): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> {
|
|
18
20
|
const parts = relative(root, dir)
|
|
19
21
|
.split(sep)
|
|
20
22
|
.filter((part) => part !== "");
|
|
@@ -22,37 +24,64 @@ export const scopeAbove = (root: string, dir: string): Effect.Effect<IgnoreScope
|
|
|
22
24
|
return Effect.reduce(
|
|
23
25
|
ancestors,
|
|
24
26
|
() => emptyScope,
|
|
25
|
-
(scope, ancestor) => ignoreScopeAt(ancestor, scope),
|
|
27
|
+
(scope, ancestor) => (verdictFor(scope, ancestor, true) === "ignored" ? Effect.succeed(scope) : ignoreScopeAt(ancestor, scope)),
|
|
26
28
|
);
|
|
27
|
-
}
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
export const walk =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
31
|
+
export const walk = Effect.fnUntraced(function* (
|
|
32
|
+
dir: string,
|
|
33
|
+
inherited: IgnoreScope = emptyScope,
|
|
34
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
35
|
+
const fs = yield* FileSystem.FileSystem;
|
|
36
|
+
if ((yield* orWhenAbsent<FileSystem.File.Info | undefined>(dir, fs.stat(dir), undefined)) === undefined) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const paths = yield* Effect.tryPromise({
|
|
40
|
+
catch: (cause) => new FilesystemFailure({ message: `cannot list tracked files in ${dir}: ${String(cause)}`, path: dir }),
|
|
41
|
+
try: () => trackedPaths(dir),
|
|
40
42
|
});
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const info = yield* orWhenAbsent<FileSystem.File.Info | undefined>(path, fs.stat(path), undefined);
|
|
46
|
-
if (info === undefined) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
const directory = info.type === "Directory";
|
|
50
|
-
const verdict = verdictFor(scope, path, directory);
|
|
51
|
-
if (verdict === "ignored") {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
if (!directory) {
|
|
55
|
-
return [path];
|
|
43
|
+
const tracked = new Set(paths);
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
for (let folder = dirname(path); folder !== dir && folder !== dirname(folder); folder = dirname(folder)) {
|
|
46
|
+
tracked.add(folder);
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
}
|
|
49
|
+
return yield* walkDirectory(dir, inherited, tracked);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const walkDirectory = Effect.fnUntraced(function* (
|
|
53
|
+
dir: string,
|
|
54
|
+
inherited: IgnoreScope,
|
|
55
|
+
tracked: ReadonlySet<string>,
|
|
56
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
57
|
+
const fs = yield* FileSystem.FileSystem;
|
|
58
|
+
const scope = verdictFor(inherited, dir, true) === "ignored" ? inherited : yield* ignoreScopeAt(dir, inherited);
|
|
59
|
+
const entries = yield* orWhenAbsent<readonly string[]>(dir, fs.readDirectory(dir), []);
|
|
60
|
+
const nested = yield* Effect.forEach(
|
|
61
|
+
entries.filter((entry) => !SKIPPED.has(entry)),
|
|
62
|
+
(entry) => walkEntry(join(dir, entry), scope, tracked),
|
|
63
|
+
{ concurrency: 1 },
|
|
64
|
+
);
|
|
65
|
+
return nested.flat();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const walkEntry = Effect.fnUntraced(function* (
|
|
69
|
+
path: string,
|
|
70
|
+
scope: IgnoreScope,
|
|
71
|
+
tracked: ReadonlySet<string>,
|
|
72
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
73
|
+
const fs = yield* FileSystem.FileSystem;
|
|
74
|
+
const info = yield* orWhenAbsent<FileSystem.File.Info | undefined>(path, fs.stat(path), undefined);
|
|
75
|
+
if (info === undefined) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const directory = info.type === "Directory";
|
|
79
|
+
const verdict = verdictFor(scope, path, directory);
|
|
80
|
+
if (verdict === "ignored" && !tracked.has(path)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (!directory) {
|
|
84
|
+
return [path];
|
|
85
|
+
}
|
|
86
|
+
return yield* walkDirectory(path, verdict === "kept" ? insideKept(scope, path) : scope, tracked);
|
|
87
|
+
});
|
package/src/rules/built-in.ts
CHANGED
|
@@ -11,7 +11,10 @@ import { otherNames } from "#rules/files/otherNames.ts";
|
|
|
11
11
|
import { importsAliased } from "#rules/imports/aliased.ts";
|
|
12
12
|
import { importCycles } from "#rules/imports/cycles.ts";
|
|
13
13
|
import { importFences } from "#rules/imports/fences.ts";
|
|
14
|
+
import { importsManifest } from "#rules/imports/manifest.ts";
|
|
15
|
+
import { noTestCode } from "#rules/imports/noTestCode.ts";
|
|
14
16
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
17
|
+
import { importsRuntime } from "#rules/imports/runtime.ts";
|
|
15
18
|
import { storySetup } from "#rules/stories/setup.ts";
|
|
16
19
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
17
20
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
@@ -41,8 +44,11 @@ export const builtInRules = [
|
|
|
41
44
|
biome,
|
|
42
45
|
importCycles,
|
|
43
46
|
importFences,
|
|
47
|
+
importsRuntime,
|
|
44
48
|
importsResolvable,
|
|
49
|
+
importsManifest,
|
|
45
50
|
importsAliased,
|
|
51
|
+
noTestCode,
|
|
46
52
|
manifestsSorted,
|
|
47
53
|
namedAfterExport,
|
|
48
54
|
folderNames,
|
|
@@ -6,7 +6,7 @@ import { commentsOf, type SourceComment } from "./scan.ts";
|
|
|
6
6
|
|
|
7
7
|
const MaxPerFileOptions = Schema.Struct({
|
|
8
8
|
allow: Pragmas,
|
|
9
|
-
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
9
|
+
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
const continues = (previous: SourceComment | undefined, comment: SourceComment): boolean =>
|
|
@@ -5,7 +5,7 @@ import { packageLayout } from "#naming/packageLayout.ts";
|
|
|
5
5
|
import { KEBAB, PASCAL } from "#naming/words.ts";
|
|
6
6
|
import { defineRule, type Finding } from "#rule.ts";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_TOOL_OWNED: readonly string[] = ["
|
|
8
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = ["migrations/"];
|
|
9
9
|
|
|
10
10
|
const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
|
|
11
11
|
|
|
@@ -5,16 +5,7 @@ import { CAMEL, KEBAB } from "#naming/words.ts";
|
|
|
5
5
|
import { defineRule, type Finding } from "#rule.ts";
|
|
6
6
|
import { stylesheetImporters } from "./stylesheetImporters.ts";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_TOOL_OWNED: readonly string[] = [
|
|
9
|
-
".*",
|
|
10
|
-
"package.json",
|
|
11
|
-
"tsconfig*.json",
|
|
12
|
-
"biome.json",
|
|
13
|
-
"biome.jsonc",
|
|
14
|
-
"*.config.*",
|
|
15
|
-
"migrations/",
|
|
16
|
-
"generated/",
|
|
17
|
-
];
|
|
8
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = [".*", "package.json", "tsconfig*.json", "biome.json", "biome.jsonc", "*.config.*", "migrations/"];
|
|
18
9
|
|
|
19
10
|
const CONVENTIONAL: ReadonlySet<string> = new Set(["AGENTS", "CHANGELOG", "CLAUDE", "CONTRIBUTING", "LICENSE", "README", "SECURITY", "SKILL"]);
|
|
20
11
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { outgoing } from "#imports/fences/reach.ts";
|
|
3
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
4
|
+
import { packageOf, type WorkspacePackage } from "#imports/workspace.ts";
|
|
5
|
+
|
|
6
|
+
function blocked(exports: unknown, subpath: string): boolean {
|
|
7
|
+
if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const entries = Object.entries(exports);
|
|
11
|
+
const exact = entries.find(([key]) => key === subpath);
|
|
12
|
+
if (exact !== undefined) {
|
|
13
|
+
return exact[1] === null;
|
|
14
|
+
}
|
|
15
|
+
const matching = entries.filter(([key]) => {
|
|
16
|
+
const [prefix = "", suffix, ...rest] = key.split("*");
|
|
17
|
+
return (
|
|
18
|
+
suffix !== undefined
|
|
19
|
+
&& rest.length === 0
|
|
20
|
+
&& subpath.length > prefix.length + suffix.length
|
|
21
|
+
&& subpath.startsWith(prefix)
|
|
22
|
+
&& subpath.endsWith(suffix)
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
const best = matching.toSorted(([left], [right]) => right.indexOf("*") - left.indexOf("*") || right.length - left.length)[0];
|
|
26
|
+
return best?.[1] === null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function targets(value: unknown): string[] {
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
return [value];
|
|
32
|
+
}
|
|
33
|
+
return typeof value === "object" && value !== null ? Object.values(value).flatMap(targets) : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function exported(exports: unknown, local: string): boolean {
|
|
37
|
+
if (typeof exports !== "object" || exports === null) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const paths = [
|
|
41
|
+
`./${local}`,
|
|
42
|
+
`./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js")}`,
|
|
43
|
+
`./${local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".d.$<mode>ts")}`,
|
|
44
|
+
];
|
|
45
|
+
return Object.entries(exports).some(([key, value]) =>
|
|
46
|
+
targets(value).some((target) =>
|
|
47
|
+
paths.some((path) => {
|
|
48
|
+
const [prefix = "", suffix] = target.split("*");
|
|
49
|
+
if (suffix === undefined) {
|
|
50
|
+
return path === target && !blocked(exports, key);
|
|
51
|
+
}
|
|
52
|
+
if (!(path.startsWith(prefix) && path.endsWith(suffix)) || path.length <= prefix.length + suffix.length) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const star = path.slice(prefix.length, path.length - suffix.length);
|
|
56
|
+
return !blocked(exports, key.replace("*", star));
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function executable(manifest: object, local: string): boolean {
|
|
63
|
+
const binTargets = targets(Reflect.get(manifest, "bin"));
|
|
64
|
+
const built = local.replace(/^src\//u, "dist/").replace(/\.(?<mode>[cm]?)tsx?$/u, ".$<mode>js");
|
|
65
|
+
return binTargets.some((target) => typeof target === "string" && [local, built].includes(posix.normalize(target)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function bundleInputs(graph: ImportGraph, owner: WorkspacePackage, manifest: object, developmentFile: (path: string) => boolean) {
|
|
69
|
+
const modules = graph.modules.filter((path) => (packageOf(graph.packages, path) ?? (owner.directory === "." ? owner : undefined)) === owner);
|
|
70
|
+
const privateFiles = new Set(
|
|
71
|
+
modules.filter((path) => {
|
|
72
|
+
const local = posix.relative(owner.directory, path);
|
|
73
|
+
return (
|
|
74
|
+
local.startsWith("src/")
|
|
75
|
+
&& blocked(Reflect.get(manifest, "exports"), `./${local.slice(4)}`)
|
|
76
|
+
&& !exported(Reflect.get(manifest, "exports"), local)
|
|
77
|
+
);
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
const queue = modules.filter(
|
|
81
|
+
(path) => executable(manifest, posix.relative(owner.directory, path)) || !(developmentFile(path) || privateFiles.has(path)),
|
|
82
|
+
);
|
|
83
|
+
const runtime = new Set(queue);
|
|
84
|
+
const edges = outgoing(graph.edges);
|
|
85
|
+
for (const path of queue) {
|
|
86
|
+
for (const edge of edges.get(path) ?? []) {
|
|
87
|
+
if (edge.to.kind === "file" && !runtime.has(edge.to.path)) {
|
|
88
|
+
runtime.add(edge.to.path);
|
|
89
|
+
queue.push(edge.to.path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { privateFiles, runtime };
|
|
94
|
+
}
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { importGraph } from "#imports/graph.ts";
|
|
2
|
+
import { checkFences } from "#imports/fences/check.ts";
|
|
3
|
+
import { Declarations } from "#imports/fences/declarations.ts";
|
|
5
4
|
import { defineRule } from "#rule.ts";
|
|
6
5
|
|
|
7
|
-
function isFence(value: unknown): value is Fence {
|
|
8
|
-
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "Fence";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
const FencesOptions = Schema.Struct({
|
|
12
|
-
fences:
|
|
7
|
+
fences: Declarations.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
13
8
|
});
|
|
14
9
|
|
|
15
10
|
export const importFences = defineRule({
|
|
16
|
-
check:
|
|
17
|
-
if (options.fences.length === 0) {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
const graph = await importGraph(inputs);
|
|
21
|
-
const policy = compilePolicy(options.fences, { files: inputs.files, packages: graph.packages });
|
|
22
|
-
return policy.flatMap((compiled) => compiled.evaluate(graph));
|
|
23
|
-
},
|
|
11
|
+
check: ({ options, ...inputs }) => checkFences(inputs, options.fences),
|
|
24
12
|
description:
|
|
25
13
|
"Imports of shipped code respect the fences the repository declares; a test file ships nowhere, so no fence holds it. Move the code to the side of the fence its imports belong to.",
|
|
26
14
|
id: "imports/fences",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { type ImportGraph, importGraph } from "#imports/graph.ts";
|
|
3
|
+
import { builtinName } from "#imports/resolve.ts";
|
|
4
|
+
import { packageOf, type WorkspacePackage } from "#imports/workspace.ts";
|
|
5
|
+
import { isTestCode } from "#naming/testName.ts";
|
|
6
|
+
import { defineRule, type RuleInputs } from "#rule.ts";
|
|
7
|
+
import { bundleInputs } from "./bundleInputs.ts";
|
|
8
|
+
|
|
9
|
+
const QUERY = /\?.*$/u;
|
|
10
|
+
|
|
11
|
+
function packageName(specifier: string): string {
|
|
12
|
+
const bare = specifier.replace(QUERY, "");
|
|
13
|
+
return (
|
|
14
|
+
builtinName(bare)
|
|
15
|
+
?? bare
|
|
16
|
+
.split("/")
|
|
17
|
+
.slice(0, bare.startsWith("@") ? 2 : 1)
|
|
18
|
+
.join("/")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ManifestDependencies {
|
|
23
|
+
readonly development: ReadonlySet<string>;
|
|
24
|
+
readonly privateFiles: ReadonlySet<string>;
|
|
25
|
+
readonly runtime: ReadonlySet<string>;
|
|
26
|
+
readonly shipped: ReadonlySet<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function names(manifest: object, field: string): readonly string[] {
|
|
30
|
+
const value: unknown = Reflect.get(manifest, field);
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`${field} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
return Object.keys(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function manifestDependencies(inputs: RuleInputs, owner: WorkspacePackage, graph: ImportGraph): Promise<ManifestDependencies> {
|
|
41
|
+
const path = posix.join(owner.directory, "package.json");
|
|
42
|
+
try {
|
|
43
|
+
const text = await inputs.readText(path);
|
|
44
|
+
if (text === undefined) {
|
|
45
|
+
throw new Error("the manifest is missing");
|
|
46
|
+
}
|
|
47
|
+
const manifest: unknown = JSON.parse(text);
|
|
48
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
49
|
+
throw new Error("the manifest must be an object");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...bundleInputs(graph, owner, manifest, (candidate) => developmentFile(owner, candidate)),
|
|
53
|
+
development: new Set(names(manifest, "devDependencies")),
|
|
54
|
+
shipped: new Set(["dependencies", "peerDependencies", "optionalDependencies"].flatMap((field) => names(manifest, field))),
|
|
55
|
+
};
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
throw new Error(`cannot read dependency policy at ${path}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const TOOL_FOLDERS = new Set(["script", "scripts", "tooling"]);
|
|
62
|
+
|
|
63
|
+
const CONFIG_FILE = /(?:^|\.)config\.[cm]?[jt]sx?$/u;
|
|
64
|
+
|
|
65
|
+
function developmentFile(owner: WorkspacePackage, path: string): boolean {
|
|
66
|
+
if (isTestCode(path)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const local = posix.relative(owner.directory, path);
|
|
70
|
+
const [folder = ""] = local.split("/");
|
|
71
|
+
return TOOL_FOLDERS.has(folder) || (!local.includes("/") && CONFIG_FILE.test(local));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function developmentImport(policy: ManifestDependencies | undefined, owner: WorkspacePackage, path: string, runtime: ReadonlySet<string>) {
|
|
75
|
+
if (runtime.has(path)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return developmentFile(owner, path) || policy?.privateFiles.has(path);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const importsManifest = defineRule({
|
|
82
|
+
check: async (inputs) => {
|
|
83
|
+
const graph = await importGraph(inputs);
|
|
84
|
+
const root: WorkspacePackage | undefined = (await inputs.readText("package.json")) === undefined ? undefined : { directory: ".", name: "." };
|
|
85
|
+
const owners = [...graph.packages, ...(root === undefined ? [] : [root])];
|
|
86
|
+
const policies = new Map(await Promise.all(owners.map(async (owner) => [owner, await manifestDependencies(inputs, owner, graph)] as const)));
|
|
87
|
+
const runtime = new Set([...policies.values()].flatMap((policy) => [...policy.runtime]));
|
|
88
|
+
const named = new Map(graph.packages.map((owner) => [owner.name, owner]));
|
|
89
|
+
const requests = [...graph.edges, ...graph.unresolved.filter((request) => request.missing === undefined)];
|
|
90
|
+
return requests.flatMap((request) => {
|
|
91
|
+
const owner = packageOf(graph.packages, request.from) ?? root;
|
|
92
|
+
const target =
|
|
93
|
+
("to" in request && request.to.kind === "file" ? packageOf(graph.packages, request.to.path) : undefined)
|
|
94
|
+
?? named.get(packageName(request.specifier));
|
|
95
|
+
if (owner === undefined || target === undefined || owner === target) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const policy = policies.get(owner);
|
|
99
|
+
const development = developmentImport(policy, owner, request.from, runtime);
|
|
100
|
+
if (policy?.shipped.has(target.name) || (development && policy?.development.has(target.name))) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
file: request.from,
|
|
106
|
+
line: request.line,
|
|
107
|
+
message: `Imports workspace package "${target.name}" without declaring it in ${posix.join(owner.directory, "package.json")}. Add it to dependencies, peerDependencies or optionalDependencies, or remove the import.${development ? " Tooling, test code and export-blocked bundle inputs may also use devDependencies." : " devDependencies are only available to tests, test support, tooling and export-blocked bundle inputs unreachable from runtime code."}`,
|
|
108
|
+
subject: target.name,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
description:
|
|
114
|
+
"Workspace imports follow the importing package's manifest; devDependencies are available only outside runtime code, including export-blocked bundle inputs.",
|
|
115
|
+
id: "imports/manifest",
|
|
116
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { importGraph } from "#imports/graph.ts";
|
|
2
|
+
import { isTestCode } from "#naming/testName.ts";
|
|
3
|
+
import { defineRule } from "#rule.ts";
|
|
4
|
+
|
|
5
|
+
export const noTestCode = defineRule({
|
|
6
|
+
check: async (inputs) =>
|
|
7
|
+
(await importGraph(inputs)).edges
|
|
8
|
+
.filter((edge) => !isTestCode(edge.from) && edge.to.kind === "file" && isTestCode(edge.to.path))
|
|
9
|
+
.map((edge) => ({
|
|
10
|
+
file: edge.from,
|
|
11
|
+
line: edge.line,
|
|
12
|
+
message: `Imports test code through "${edge.specifier}". Move the shared code into a production module and import it from both production and tests.`,
|
|
13
|
+
subject: edge.specifier,
|
|
14
|
+
})),
|
|
15
|
+
description: "Production modules import production code. Move shared code out of test files and test-support/ into a production module.",
|
|
16
|
+
id: "imports/no-test-code",
|
|
17
|
+
registrable: false,
|
|
18
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { browserGlobals } from "#imports/browserGlobals.ts";
|
|
2
|
+
import { importGraph } from "#imports/graph.ts";
|
|
3
|
+
import { builtinName } from "#imports/resolve.ts";
|
|
4
|
+
import { type Runtime, runtimeOf } from "#imports/runtime.ts";
|
|
5
|
+
import { isTestCode } from "#naming/testName.ts";
|
|
6
|
+
import { defineRule } from "#rule.ts";
|
|
7
|
+
|
|
8
|
+
function forbiddenImport(from: Runtime, to: Runtime | undefined, builtin: boolean): boolean {
|
|
9
|
+
if (from === "shared") {
|
|
10
|
+
return to === "client" || to === "server" || builtin;
|
|
11
|
+
}
|
|
12
|
+
return from === "client" && (to === "server" || builtin);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const importsRuntime = defineRule({
|
|
16
|
+
check: async (inputs) => {
|
|
17
|
+
const graph = await importGraph(inputs);
|
|
18
|
+
const runtime = runtimeOf(inputs, graph.packages);
|
|
19
|
+
const imports = graph.edges.flatMap((edge) => {
|
|
20
|
+
const from = runtime(edge.from);
|
|
21
|
+
if (from === undefined || isTestCode(edge.from)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const to = edge.to.kind === "file" ? runtime(edge.to.path) : undefined;
|
|
25
|
+
const builtin = edge.to.kind === "external" && builtinName(edge.to.specifier) !== undefined;
|
|
26
|
+
if (!forbiddenImport(from, to, builtin)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
file: edge.from,
|
|
32
|
+
line: edge.line,
|
|
33
|
+
message: `${from === "client" ? "Client" : "Shared"} code imports ${builtin ? "Node built-in" : `${to} code`} through "${edge.specifier}". Move runtime-dependent code into src/client/ or src/server/ and keep shared code independent of both runtimes.`,
|
|
34
|
+
subject: edge.specifier,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
});
|
|
38
|
+
return [...imports, ...inputs.sources.filter((file) => !isTestCode(file.path) && runtime(file.path) === "shared").flatMap(browserGlobals)];
|
|
39
|
+
},
|
|
40
|
+
description:
|
|
41
|
+
"In packages with src/client/ or src/server/, folder names define runtime boundaries. Shared code runs in either runtime without browser globals or Node built-ins.",
|
|
42
|
+
id: "imports/runtime",
|
|
43
|
+
});
|
package/src/rules/max-lines.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import ignore from "ignore";
|
|
3
2
|
import { isTestCode } from "#naming/testName.ts";
|
|
4
3
|
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
5
4
|
|
|
@@ -8,9 +7,8 @@ const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
|
8
7
|
const Limit = Schema.Int.check(Schema.isGreaterThan(0));
|
|
9
8
|
|
|
10
9
|
const MaxLinesOptions = Schema.Struct({
|
|
11
|
-
source: Limit.pipe(Schema.withDecodingDefaultKey(Effect.succeed(150))),
|
|
12
|
-
test: Limit.pipe(Schema.withDecodingDefaultKey(Effect.succeed(300))),
|
|
13
|
-
testFiles: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
10
|
+
source: Limit.check(Schema.isLessThanOrEqualTo(150)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(150))),
|
|
11
|
+
test: Limit.check(Schema.isLessThanOrEqualTo(300)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(300))),
|
|
14
12
|
});
|
|
15
13
|
|
|
16
14
|
const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
|
|
@@ -26,12 +24,8 @@ const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
|
|
|
26
24
|
];
|
|
27
25
|
|
|
28
26
|
export const maxLines = defineRule({
|
|
29
|
-
check: ({ options, sources }) =>
|
|
30
|
-
|
|
31
|
-
return sources
|
|
32
|
-
.filter((file) => !DECLARATION.test(file.path))
|
|
33
|
-
.flatMap((file) => oversized(file, isTestCode(file.path) || suites.ignores(file.path) ? options.test : options.source));
|
|
34
|
-
},
|
|
27
|
+
check: ({ options, sources }) =>
|
|
28
|
+
sources.filter((file) => !DECLARATION.test(file.path)).flatMap((file) => oversized(file, isTestCode(file.path) ? options.test : options.source)),
|
|
35
29
|
description: "Keep each module to one job. Split a long file along its responsibilities; never golf it under the limit.",
|
|
36
30
|
id: "structure/max-lines",
|
|
37
31
|
options: Schema.toStandardSchemaV1(MaxLinesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
@@ -4,8 +4,30 @@ import { biomeConfigs, type Chain } from "#rules/suppressions/biome/configs.ts";
|
|
|
4
4
|
import { Declaration, matches, type Scoped, scoped, scopeText } from "#rules/suppressions/biome/declarations.ts";
|
|
5
5
|
import { type Weakening, weakeningsOf } from "#rules/suppressions/biome/weakenings.ts";
|
|
6
6
|
|
|
7
|
+
const GLOB = /[*!?[\]{}]/u;
|
|
8
|
+
const DIRECTORY = /^\.?[^./\\*!?[\]{}]+$/u;
|
|
9
|
+
const INDIVIDUAL = /^(?:lint|assist)\/(?!domains\/)[a-zA-Z]+\/[a-zA-Z]+$/u;
|
|
10
|
+
|
|
11
|
+
function namedExclusion(pattern: string): boolean {
|
|
12
|
+
const segments = pattern.slice(pattern.startsWith("!**/") ? 4 : 1).split("/");
|
|
13
|
+
return pattern.startsWith("!") && DIRECTORY.test(segments.at(-1) ?? "") && segments.every((segment) => segment === "*" || DIRECTORY.test(segment));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ConsumerDeclaration = Declaration.check(
|
|
17
|
+
Schema.makeFilter(
|
|
18
|
+
({ includes, rule }) =>
|
|
19
|
+
rule === "files/includes"
|
|
20
|
+
? includes.includes("**") && includes.length > 1 && includes.every((pattern) => pattern === "**" || namedExclusion(pattern))
|
|
21
|
+
: INDIVIDUAL.test(rule) && includes.every((path) => !GLOB.test(path) && /[^/]+\.[^/]+$/u.test(path)),
|
|
22
|
+
{
|
|
23
|
+
message:
|
|
24
|
+
"Declare one lint rule or assist action for explicit files, or files/includes with ** and named directory exclusions. Whole checks, groups and blanket scopes cannot be weakened.",
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
7
29
|
const BiomeOverridesOptions = Schema.Struct({
|
|
8
|
-
declared: Schema.Array(
|
|
30
|
+
declared: Schema.Array(ConsumerDeclaration).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
9
31
|
});
|
|
10
32
|
|
|
11
33
|
interface Shipped extends Scoped {
|
|
@@ -21,11 +43,16 @@ const shippedBy = ({ base, layers }: Chain): readonly Shipped[] =>
|
|
|
21
43
|
|
|
22
44
|
const undeclared = ({ includes, layer, line, rule }: Weakening): Finding =>
|
|
23
45
|
layer.preset === undefined
|
|
24
|
-
? {
|
|
46
|
+
? {
|
|
47
|
+
file: layer.path,
|
|
48
|
+
line,
|
|
49
|
+
message: `Weakens "${rule}" for ${scopeText(includes)} without a declaration. Repair the code and remove the weakening setting.`,
|
|
50
|
+
subject: rule,
|
|
51
|
+
}
|
|
25
52
|
: {
|
|
26
53
|
file: layer.preset.file,
|
|
27
54
|
line: layer.preset.line,
|
|
28
|
-
message: `Extends "${layer.preset.specifier}", which weakens "${rule}" for ${scopeText(includes)} without a declaration.`,
|
|
55
|
+
message: `Extends "${layer.preset.specifier}", which weakens "${rule}" for ${scopeText(includes)} without a declaration. Repair the code and restore the check in the preset.`,
|
|
29
56
|
subject: rule,
|
|
30
57
|
};
|
|
31
58
|
|
|
@@ -63,7 +90,7 @@ export const biomeOverrides = defineRule({
|
|
|
63
90
|
];
|
|
64
91
|
},
|
|
65
92
|
description:
|
|
66
|
-
"A Biome setting that turns a check off or down, or keeps files out of it,
|
|
93
|
+
"A Biome setting that turns a check off or down, or keeps files out of it, hides findings for every file it covers. Repair the code and remove the weakening setting.",
|
|
67
94
|
id: "suppressions/biome-overrides",
|
|
68
95
|
options: Schema.toStandardSchemaV1(BiomeOverridesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
69
96
|
registrable: false,
|
|
@@ -22,7 +22,7 @@ function settingOf(rules: Json | undefined, rule: string): Setting {
|
|
|
22
22
|
return { level: textOf(node) ?? textOf(member(node, "level")), line: node?.line };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const FIX =
|
|
25
|
+
const FIX = 'Set it to "error" in the preset and repair the code it reports.';
|
|
26
26
|
|
|
27
27
|
function looseRule(file: string, rule: string, { level, line }: Setting): Finding {
|
|
28
28
|
const state = level === undefined ? "leaves at Biome's default level" : `sets to "${level}"`;
|
|
@@ -65,7 +65,7 @@ export function biomeRecommendedFrom(recommended: () => Promise<readonly string[
|
|
|
65
65
|
return defineRule({
|
|
66
66
|
check: ({ readText }) => check(readText, recommended),
|
|
67
67
|
description:
|
|
68
|
-
"Every rule
|
|
68
|
+
"Every recommended Biome rule without a shipped internal exception runs at error in the shared preset. Set missing rules to error in the preset and repair the code they report.",
|
|
69
69
|
id: "suppressions/biome-recommended",
|
|
70
70
|
registrable: false,
|
|
71
71
|
});
|
|
@@ -33,7 +33,7 @@ export const noInline = defineRule({
|
|
|
33
33
|
(site) => !allowed(site),
|
|
34
34
|
),
|
|
35
35
|
description:
|
|
36
|
-
"A suppression silences a check at one site instead of fixing the cause.
|
|
36
|
+
"A suppression silences a check at one site instead of fixing the cause. Repair the code that fails the check and remove the suppression. A type test asserts a compile error with @ts-expect-error in a *.typecheck.test.ts or *.typecheck.spec.ts file.",
|
|
37
37
|
id: "suppressions/no-inline",
|
|
38
38
|
registrable: false,
|
|
39
39
|
});
|
package/src/standard-schema.ts
CHANGED
|
@@ -16,7 +16,15 @@ export interface StandardIssue {
|
|
|
16
16
|
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Biome's noBaseToString does not narrow the ternary in segment, so the key is stringified behind a PropertyKey parameter.
|
|
20
|
+
function keyText(key: PropertyKey): string {
|
|
21
|
+
return key.toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function segment(part: PropertyKey | { readonly key: PropertyKey }): string {
|
|
25
|
+
return keyText(typeof part === "object" ? part.key : part);
|
|
26
|
+
}
|
|
20
27
|
|
|
21
|
-
export
|
|
22
|
-
issue.path === undefined || issue.path.length === 0 ? issue.message : `${issue.path.map(segment).join(".")}: ${issue.message}`;
|
|
28
|
+
export function describeIssue(issue: StandardIssue): string {
|
|
29
|
+
return issue.path === undefined || issue.path.length === 0 ? issue.message : `${issue.path.map(segment).join(".")}: ${issue.message}`;
|
|
30
|
+
}
|