@shivaedev/quality 0.7.2 → 0.8.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 +23 -0
- package/README.md +102 -8
- package/biome/plugins/constant-names.grit +17 -0
- package/biome/plugins/effect-fn-spans.grit +13 -3
- package/biome/plugins/handler-names.grit +10 -0
- package/biome/plugins/schema-names.grit +13 -0
- package/biome/plugins/schema-struct-keys.grit +7 -0
- package/biome/plugins/service-layers.grit +13 -0
- package/biome/plugins/type-re-exports.grit +8 -0
- package/biome/preset.json +48 -1
- package/dist/baseline/compare.d.ts +1 -1
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +1 -1
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +1 -1
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +2 -2
- 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 +2 -2
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/baseline/rewrite.js +1 -1
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/update.d.ts +1 -1
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +1 -1
- 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/baseline.d.ts +3 -3
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +7 -7
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +1 -1
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +7 -7
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts +2 -2
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/lint.js +4 -4
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +9 -9
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/run-main.d.ts +1 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +1 -1
- 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 +2 -2
- package/dist/cli/shrink.js.map +1 -1
- package/dist/cli.js +7 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +2 -2
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +1 -1
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +2 -2
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +2 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +3 -3
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +1 -1
- 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/engine/baseline-file.d.ts +3 -3
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/baseline-file.js +5 -5
- package/dist/engine/baseline-file.js.map +1 -1
- package/dist/engine/evaluate.d.ts +3 -3
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +2 -2
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/run-rules.d.ts +2 -2
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +2 -2
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts +4 -4
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +6 -6
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +2 -2
- package/dist/engine/violation.d.ts.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 +2 -2
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/git/base.d.ts +1 -1
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js +1 -1
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +1 -1
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js +1 -1
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/imports/departures.d.ts +1 -1
- package/dist/imports/departures.d.ts.map +1 -1
- package/dist/imports/departures.js +1 -1
- package/dist/imports/departures.js.map +1 -1
- package/dist/imports/fences/evaluate.d.ts +2 -2
- package/dist/imports/fences/evaluate.d.ts.map +1 -1
- package/dist/imports/fences/match.d.ts +2 -2
- package/dist/imports/fences/match.d.ts.map +1 -1
- package/dist/imports/fences/match.js +2 -2
- package/dist/imports/fences/match.js.map +1 -1
- package/dist/imports/fences/policy.js +1 -1
- package/dist/imports/fences/policy.js.map +1 -1
- package/dist/imports/fences/reach.d.ts +2 -2
- package/dist/imports/fences/reach.d.ts.map +1 -1
- package/dist/imports/fences/vocabulary.d.ts +1 -1
- package/dist/imports/fences/vocabulary.d.ts.map +1 -1
- package/dist/imports/generated.d.ts +1 -1
- package/dist/imports/generated.d.ts.map +1 -1
- package/dist/imports/generated.js +1 -1
- package/dist/imports/generated.js.map +1 -1
- package/dist/imports/graph.d.ts +1 -1
- package/dist/imports/graph.d.ts.map +1 -1
- package/dist/imports/graph.js +1 -1
- package/dist/imports/graph.js.map +1 -1
- package/dist/imports/resolve.js +1 -1
- package/dist/imports/resolve.js.map +1 -1
- package/dist/imports/workspace.d.ts +1 -1
- package/dist/imports/workspace.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory/collect.d.ts +1 -1
- package/dist/inventory/collect.d.ts.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 +3 -3
- package/dist/manifests/sorted.js.map +1 -1
- package/dist/naming/exportFits.d.ts +2 -0
- package/dist/naming/exportFits.d.ts.map +1 -0
- package/dist/naming/exportFits.js +20 -0
- package/dist/naming/exportFits.js.map +1 -0
- package/dist/naming/mainExport.d.ts +3 -0
- package/dist/naming/mainExport.d.ts.map +1 -0
- package/dist/naming/mainExport.js +10 -0
- package/dist/naming/mainExport.js.map +1 -0
- package/dist/naming/ownExports.d.ts +8 -0
- package/dist/naming/ownExports.d.ts.map +1 -0
- package/dist/naming/ownExports.js +78 -0
- package/dist/naming/ownExports.js.map +1 -0
- package/dist/naming/packageLayout.d.ts +8 -0
- package/dist/naming/packageLayout.d.ts.map +1 -0
- package/dist/naming/packageLayout.js +57 -0
- package/dist/naming/packageLayout.js.map +1 -0
- package/dist/naming/testName.d.ts +11 -0
- package/dist/naming/testName.d.ts.map +1 -0
- package/dist/naming/testName.js +13 -0
- package/dist/naming/testName.js.map +1 -0
- package/dist/naming/words.d.ts +8 -0
- package/dist/naming/words.d.ts.map +1 -0
- package/dist/naming/words.js +21 -0
- package/dist/naming/words.js.map +1 -0
- package/dist/report/render.d.ts +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +2 -2
- 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/rules/biome.d.ts +1 -1
- package/dist/rules/biome.d.ts.map +1 -1
- package/dist/rules/biome.js +3 -3
- package/dist/rules/biome.js.map +1 -1
- package/dist/rules/built-in.d.ts +13 -1
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +24 -14
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.js +1 -1
- package/dist/rules/comments/kinds.js.map +1 -1
- package/dist/rules/comments/max-per-file.d.ts +1 -1
- package/dist/rules/comments/max-per-file.js +2 -2
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/no-jsdoc.d.ts +1 -1
- package/dist/rules/comments/no-jsdoc.js +1 -1
- package/dist/rules/comments/no-jsdoc.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 +1 -1
- package/dist/rules/comments/pattern-rule.js.map +1 -1
- package/dist/rules/comments/scan.d.ts +1 -1
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/comments/scan.js +1 -1
- package/dist/rules/comments/scan.js.map +1 -1
- package/dist/rules/files/folderNames.d.ts +5 -0
- package/dist/rules/files/folderNames.d.ts.map +1 -0
- package/dist/rules/files/folderNames.js +74 -0
- package/dist/rules/files/folderNames.js.map +1 -0
- package/dist/rules/files/namedAfterExport.d.ts +4 -0
- package/dist/rules/files/namedAfterExport.d.ts.map +1 -0
- package/dist/rules/files/namedAfterExport.js +76 -0
- package/dist/rules/files/namedAfterExport.js.map +1 -0
- package/dist/rules/files/otherNames.d.ts +5 -0
- package/dist/rules/files/otherNames.d.ts.map +1 -0
- package/dist/rules/files/otherNames.js +99 -0
- package/dist/rules/files/otherNames.js.map +1 -0
- package/dist/rules/imports/aliased.d.ts +1 -1
- package/dist/rules/imports/aliased.d.ts.map +1 -1
- package/dist/rules/imports/aliased.js +2 -2
- package/dist/rules/imports/aliased.js.map +1 -1
- package/dist/rules/imports/cycles.d.ts +1 -1
- package/dist/rules/imports/cycles.d.ts.map +1 -1
- package/dist/rules/imports/cycles.js +4 -4
- package/dist/rules/imports/cycles.js.map +1 -1
- package/dist/rules/imports/fences.d.ts +2 -2
- package/dist/rules/imports/fences.d.ts.map +1 -1
- package/dist/rules/imports/fences.js +3 -3
- package/dist/rules/imports/fences.js.map +1 -1
- package/dist/rules/imports/resolvable.d.ts +1 -1
- package/dist/rules/imports/resolvable.js +3 -3
- package/dist/rules/imports/resolvable.js.map +1 -1
- package/dist/rules/manifests-sorted.d.ts +1 -1
- package/dist/rules/manifests-sorted.d.ts.map +1 -1
- package/dist/rules/manifests-sorted.js +2 -2
- package/dist/rules/manifests-sorted.js.map +1 -1
- package/dist/rules/max-lines.d.ts +1 -1
- package/dist/rules/max-lines.js +1 -1
- package/dist/rules/max-lines.js.map +1 -1
- 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/declarations.d.ts +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.js +1 -1
- package/dist/rules/suppressions/biome/declarations.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 +1 -1
- package/dist/rules/suppressions/biome/resolve.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
- package/dist/rules/suppressions/biome-overrides.js +4 -4
- 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/no-double-cast.d.ts +1 -1
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -1
- package/dist/rules/suppressions/no-double-cast.js +2 -2
- package/dist/rules/suppressions/no-double-cast.js.map +1 -1
- package/dist/rules/suppressions/no-inline.d.ts +1 -1
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +2 -2
- 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 +1 -1
- 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/test-names/colocated.d.ts +4 -0
- package/dist/rules/test-names/colocated.d.ts.map +1 -0
- package/dist/rules/test-names/colocated.js +31 -0
- package/dist/rules/test-names/colocated.js.map +1 -0
- package/dist/rules/test-names/follow.d.ts +4 -0
- package/dist/rules/test-names/follow.d.ts.map +1 -0
- package/dist/rules/test-names/follow.js +66 -0
- package/dist/rules/test-names/follow.js.map +1 -0
- package/dist/rules/test-names/usesDom.d.ts +3 -0
- package/dist/rules/test-names/usesDom.d.ts.map +1 -0
- package/dist/rules/test-names/usesDom.js +18 -0
- package/dist/rules/test-names/usesDom.js.map +1 -0
- package/dist/vitest.d.ts +3 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +11 -11
- package/dist/vitest.js.map +1 -1
- package/package.json +10 -2
- package/src/baseline/compare.ts +1 -1
- package/src/baseline/format.ts +2 -2
- package/src/baseline/legacy.ts +2 -2
- package/src/baseline/rewrite.ts +1 -1
- package/src/baseline/update.ts +1 -1
- package/src/cli/args.ts +1 -1
- package/src/cli/baseline.ts +8 -8
- package/src/cli/fix.ts +7 -7
- package/src/cli/lint.ts +4 -4
- package/src/cli/migrate.ts +9 -9
- package/src/cli/run-main.ts +2 -2
- package/src/cli/shrink.ts +5 -5
- package/src/cli.ts +7 -7
- package/src/config/decode.ts +2 -2
- package/src/config/load.ts +3 -3
- package/src/config/resolve.ts +6 -6
- package/src/config.ts +1 -1
- package/src/engine/baseline-file.ts +6 -6
- package/src/engine/evaluate.ts +3 -3
- package/src/engine/run-rules.ts +3 -3
- package/src/engine/session.ts +6 -6
- package/src/engine/violation.ts +2 -2
- package/src/exceptions/registry.ts +2 -2
- package/src/git/base.ts +1 -1
- package/src/git/command.ts +1 -1
- package/src/git/history.ts +1 -1
- package/src/imports/departures.ts +2 -2
- package/src/imports/fences/evaluate.ts +2 -2
- package/src/imports/fences/match.ts +2 -2
- package/src/imports/fences/policy.ts +2 -2
- package/src/imports/fences/reach.ts +2 -2
- package/src/imports/fences/vocabulary.ts +1 -1
- package/src/imports/generated.ts +2 -2
- package/src/imports/graph.ts +2 -2
- package/src/imports/resolve.ts +1 -1
- package/src/imports/workspace.ts +1 -1
- package/src/index.ts +3 -3
- package/src/inventory/collect.ts +1 -1
- package/src/manifests/sorted.ts +3 -3
- package/src/naming/exportFits.ts +21 -0
- package/src/naming/mainExport.ts +11 -0
- package/src/naming/ownExports.ts +95 -0
- package/src/naming/packageLayout.ts +75 -0
- package/src/naming/testName.ts +25 -0
- package/src/naming/words.ts +28 -0
- package/src/report/render.ts +3 -3
- package/src/report/stale.ts +3 -3
- package/src/rules/biome.ts +3 -3
- package/src/rules/built-in.ts +24 -14
- package/src/rules/comments/kinds.ts +1 -1
- package/src/rules/comments/max-per-file.ts +2 -2
- package/src/rules/comments/no-jsdoc.ts +1 -1
- package/src/rules/comments/pattern-rule.ts +1 -1
- package/src/rules/comments/scan.ts +2 -2
- package/src/rules/files/folderNames.ts +92 -0
- package/src/rules/files/namedAfterExport.ts +87 -0
- package/src/rules/files/otherNames.ts +118 -0
- package/src/rules/imports/aliased.ts +2 -2
- package/src/rules/imports/cycles.ts +4 -4
- package/src/rules/imports/fences.ts +4 -4
- package/src/rules/imports/resolvable.ts +3 -3
- package/src/rules/manifests-sorted.ts +2 -2
- package/src/rules/max-lines.ts +1 -1
- package/src/rules/suppressions/biome/configs.ts +1 -1
- package/src/rules/suppressions/biome/declarations.ts +1 -1
- package/src/rules/suppressions/biome/resolve.ts +2 -2
- package/src/rules/suppressions/biome-overrides.ts +4 -4
- package/src/rules/suppressions/directives.ts +1 -1
- package/src/rules/suppressions/no-double-cast.ts +2 -2
- package/src/rules/suppressions/no-inline.ts +2 -2
- package/src/rules/suppressions/stylesheet-comments.ts +1 -1
- package/src/rules/syntax.ts +1 -1
- package/src/rules/test-names/colocated.ts +34 -0
- package/src/rules/test-names/follow.ts +78 -0
- package/src/rules/test-names/usesDom.ts +22 -0
- package/src/vitest.ts +14 -11
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import type { RuleInputs } from "#rule.ts";
|
|
3
|
+
|
|
4
|
+
export interface PackageLayout {
|
|
5
|
+
readonly foldersOf: (path: string) => readonly string[];
|
|
6
|
+
readonly names: ReadonlyMap<string, string>;
|
|
7
|
+
readonly scripts: ReadonlySet<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MANIFEST = "package.json";
|
|
11
|
+
|
|
12
|
+
const SOURCE_FOLDER = "src";
|
|
13
|
+
|
|
14
|
+
const RUNNABLE = /\.[cm]?[jt]sx?$/u;
|
|
15
|
+
|
|
16
|
+
const BUILT = /^dist\/(?<entry>.+)\.[cm]?js$/u;
|
|
17
|
+
|
|
18
|
+
function parsed(text: string | undefined): unknown {
|
|
19
|
+
if (text === undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text);
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function field(value: unknown, key: string): unknown {
|
|
30
|
+
return typeof value === "object" && value !== null && key in value ? Reflect.get(value, key) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function texts(value: unknown): readonly string[] {
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
return [value];
|
|
36
|
+
}
|
|
37
|
+
return typeof value === "object" && value !== null ? Object.values(value).filter((entry): entry is string => typeof entry === "string") : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sourcesOf(target: string): readonly string[] {
|
|
41
|
+
const built = BUILT.exec(target)?.groups?.entry;
|
|
42
|
+
return built === undefined ? [target] : [target, `src/${built}.ts`, `src/${built}.tsx`];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function packageOf(path: string, roots: readonly string[]): string {
|
|
46
|
+
return roots.find((root) => root === "" || path.startsWith(`${root}/`)) ?? "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runBy(directory: string, manifest: unknown): readonly string[] {
|
|
50
|
+
const commands = texts(field(manifest, "scripts")).flatMap((command) => command.split(/\s+/u).filter((token) => RUNNABLE.test(token)));
|
|
51
|
+
const bins = texts(field(manifest, "bin")).flatMap(sourcesOf);
|
|
52
|
+
return [...commands, ...bins].map((target) => posix.join(directory, target));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function packageLayout({ files, readText }: Pick<RuleInputs, "files" | "readText">): Promise<PackageLayout> {
|
|
56
|
+
const directories = ["", ...files.filter((path) => posix.basename(path) === MANIFEST).map((path) => posix.dirname(path))];
|
|
57
|
+
const manifests = await Promise.all(
|
|
58
|
+
[...new Set(directories)].map(async (directory) => ({
|
|
59
|
+
directory: directory === "." ? "" : directory,
|
|
60
|
+
manifest: parsed(await readText(directory === "" ? MANIFEST : `${directory}/${MANIFEST}`)),
|
|
61
|
+
})),
|
|
62
|
+
);
|
|
63
|
+
const present = manifests.filter((entry) => entry.manifest !== undefined);
|
|
64
|
+
const roots = present.map((entry) => entry.directory).toSorted((left, right) => right.length - left.length);
|
|
65
|
+
const names = new Map(present.flatMap(({ directory, manifest }) => texts(field(manifest, "name")).map((name) => [directory, name] as const)));
|
|
66
|
+
return {
|
|
67
|
+
foldersOf: (path) => {
|
|
68
|
+
const root = packageOf(path, roots);
|
|
69
|
+
const folders = posix.dirname(root === "" ? path : path.slice(root.length + 1)).split("/");
|
|
70
|
+
return folders.filter((folder, index) => folder !== "." && !(index === 0 && folder === SOURCE_FOLDER));
|
|
71
|
+
},
|
|
72
|
+
names,
|
|
73
|
+
scripts: new Set(present.flatMap(({ directory, manifest }) => runBy(directory, manifest))),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const ENVIRONMENTS = ["dom", "slow", "typecheck"] as const;
|
|
4
|
+
|
|
5
|
+
export type Environment = (typeof ENVIRONMENTS)[number];
|
|
6
|
+
|
|
7
|
+
export interface TestName {
|
|
8
|
+
readonly extension: string;
|
|
9
|
+
readonly folder: string;
|
|
10
|
+
readonly kind: "spec" | "test";
|
|
11
|
+
readonly modifiers: readonly string[];
|
|
12
|
+
readonly stem: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TEST_FILE = /^(?<base>.+)\.(?<kind>test|spec)(?<extension>\.[cm]?[jt]sx?)$/u;
|
|
16
|
+
|
|
17
|
+
export function testName(path: string): TestName | undefined {
|
|
18
|
+
const groups = TEST_FILE.exec(posix.basename(path))?.groups;
|
|
19
|
+
const kind = groups?.kind;
|
|
20
|
+
if (groups?.base === undefined || groups.extension === undefined || (kind !== "test" && kind !== "spec")) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const [stem = "", ...modifiers] = groups.base.split(".");
|
|
24
|
+
return { extension: groups.extension, folder: posix.dirname(path), kind, modifiers, stem };
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const WORD = /[A-Z]?[a-z0-9]+|[A-Z]+(?![a-z])/gu;
|
|
2
|
+
|
|
3
|
+
export const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
4
|
+
|
|
5
|
+
export const CAMEL = /^[a-z][a-zA-Z0-9]*$/u;
|
|
6
|
+
|
|
7
|
+
export const PASCAL = /^[A-Z][a-zA-Z0-9]*$/u;
|
|
8
|
+
|
|
9
|
+
const UPPER_START = /^[A-Z]/u;
|
|
10
|
+
|
|
11
|
+
export function wordsOf(name: string): readonly string[] {
|
|
12
|
+
return [...name.matchAll(WORD)].map((match) => match[0].toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function singular(word: string): string {
|
|
16
|
+
if (word.endsWith("ies") && word.length > 3) {
|
|
17
|
+
return `${word.slice(0, -3)}y`;
|
|
18
|
+
}
|
|
19
|
+
return word.endsWith("s") && !word.endsWith("ss") && word.length > 1 ? word.slice(0, -1) : word;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sameWord(left: string, right: string): boolean {
|
|
23
|
+
return singular(left) === singular(right);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isUpper(text: string): boolean {
|
|
27
|
+
return UPPER_START.test(text);
|
|
28
|
+
}
|
package/src/report/render.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Regression } from "
|
|
2
|
-
import { type Outcome, passes } from "
|
|
3
|
-
import { groupBy, keyOf, type Violation } from "
|
|
1
|
+
import type { Regression } from "#baseline/compare.ts";
|
|
2
|
+
import { type Outcome, passes } from "#engine/evaluate.ts";
|
|
3
|
+
import { groupBy, keyOf, type Violation } from "#engine/violation.ts";
|
|
4
4
|
import { plural } from "./plural.ts";
|
|
5
5
|
import { staleSections } from "./stale.ts";
|
|
6
6
|
|
package/src/report/stale.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { StaleBaselineEntry } from "
|
|
2
|
-
import type { Outcome } from "
|
|
3
|
-
import type { StaleRegistryEntry } from "
|
|
1
|
+
import type { StaleBaselineEntry } from "#baseline/compare.ts";
|
|
2
|
+
import type { Outcome } from "#engine/evaluate.ts";
|
|
3
|
+
import type { StaleRegistryEntry } from "#exceptions/registry.ts";
|
|
4
4
|
import { plural } from "./plural.ts";
|
|
5
5
|
|
|
6
6
|
interface StaleFiles {
|
package/src/rules/biome.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { biomeReport, type Diagnostic } from "
|
|
2
|
-
import { defineRule, type Finding, type Rule, type RuleInputs } from "
|
|
3
|
-
import { itemsOf, member, parseJsonc, textOf } from "
|
|
1
|
+
import { biomeReport, type Diagnostic } from "#biome/report.ts";
|
|
2
|
+
import { defineRule, type Finding, type Rule, type RuleInputs } from "#rule.ts";
|
|
3
|
+
import { itemsOf, member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
|
|
4
4
|
|
|
5
5
|
export const PRESET = "@shivaedev/quality/biome";
|
|
6
6
|
|
package/src/rules/built-in.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
+
import { maxPerFile } from "#rules/comments/max-per-file.ts";
|
|
2
|
+
import { noBanner } from "#rules/comments/no-banner.ts";
|
|
3
|
+
import { noEnvironmentPragma } from "#rules/comments/no-environment-pragma.ts";
|
|
4
|
+
import { noJsdoc } from "#rules/comments/no-jsdoc.ts";
|
|
5
|
+
import { noLineReference } from "#rules/comments/no-line-reference.ts";
|
|
6
|
+
import { noPrReference } from "#rules/comments/no-pr-reference.ts";
|
|
7
|
+
import { noTodo } from "#rules/comments/no-todo.ts";
|
|
8
|
+
import { folderNames } from "#rules/files/folderNames.ts";
|
|
9
|
+
import { namedAfterExport } from "#rules/files/namedAfterExport.ts";
|
|
10
|
+
import { otherNames } from "#rules/files/otherNames.ts";
|
|
11
|
+
import { importsAliased } from "#rules/imports/aliased.ts";
|
|
12
|
+
import { importCycles } from "#rules/imports/cycles.ts";
|
|
13
|
+
import { importFences } from "#rules/imports/fences.ts";
|
|
14
|
+
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
15
|
+
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
16
|
+
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
17
|
+
import { noInline } from "#rules/suppressions/no-inline.ts";
|
|
18
|
+
import { testsColocated } from "#rules/test-names/colocated.ts";
|
|
19
|
+
import { testsFollow } from "#rules/test-names/follow.ts";
|
|
1
20
|
import { biome } from "./biome.ts";
|
|
2
|
-
import { maxPerFile } from "./comments/max-per-file.ts";
|
|
3
|
-
import { noBanner } from "./comments/no-banner.ts";
|
|
4
|
-
import { noEnvironmentPragma } from "./comments/no-environment-pragma.ts";
|
|
5
|
-
import { noJsdoc } from "./comments/no-jsdoc.ts";
|
|
6
|
-
import { noLineReference } from "./comments/no-line-reference.ts";
|
|
7
|
-
import { noPrReference } from "./comments/no-pr-reference.ts";
|
|
8
|
-
import { noTodo } from "./comments/no-todo.ts";
|
|
9
|
-
import { importsAliased } from "./imports/aliased.ts";
|
|
10
|
-
import { importCycles } from "./imports/cycles.ts";
|
|
11
|
-
import { importFences } from "./imports/fences.ts";
|
|
12
|
-
import { importsResolvable } from "./imports/resolvable.ts";
|
|
13
21
|
import { manifestsSorted } from "./manifests-sorted.ts";
|
|
14
22
|
import { maxLines } from "./max-lines.ts";
|
|
15
|
-
import { biomeOverrides } from "./suppressions/biome-overrides.ts";
|
|
16
|
-
import { noDoubleCast } from "./suppressions/no-double-cast.ts";
|
|
17
|
-
import { noInline } from "./suppressions/no-inline.ts";
|
|
18
23
|
|
|
19
24
|
export const builtInRules = [
|
|
20
25
|
maxLines,
|
|
@@ -34,4 +39,9 @@ export const builtInRules = [
|
|
|
34
39
|
importsResolvable,
|
|
35
40
|
importsAliased,
|
|
36
41
|
manifestsSorted,
|
|
42
|
+
namedAfterExport,
|
|
43
|
+
folderNames,
|
|
44
|
+
otherNames,
|
|
45
|
+
testsFollow,
|
|
46
|
+
testsColocated,
|
|
37
47
|
] as const;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { SUPPRESSIONS } from "
|
|
2
|
+
import { SUPPRESSIONS } from "#rules/suppressions/directives.ts";
|
|
3
3
|
import type { SourceComment } from "./scan.ts";
|
|
4
4
|
|
|
5
5
|
export const Pragmas = Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed<readonly string[]>([])));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { plural } from "
|
|
3
|
-
import { defineRule, type Finding, type SourceFile } from "
|
|
2
|
+
import { plural } from "#report/plural.ts";
|
|
3
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
4
4
|
import { isDirective, isPragma, Pragmas } from "./kinds.ts";
|
|
5
5
|
import { commentsOf, type SourceComment } from "./scan.ts";
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import type { SourceFile } from "
|
|
3
|
-
import { parse } from "
|
|
2
|
+
import type { SourceFile } from "#rule.ts";
|
|
3
|
+
import { parse } from "#rules/syntax.ts";
|
|
4
4
|
|
|
5
5
|
export interface SourceComment {
|
|
6
6
|
readonly body: readonly string[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import ignore, { type Ignore } from "ignore";
|
|
4
|
+
import { packageLayout } from "#naming/packageLayout.ts";
|
|
5
|
+
import { KEBAB, PASCAL } from "#naming/words.ts";
|
|
6
|
+
import { defineRule, type Finding } from "#rule.ts";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = ["generated/", "migrations/"];
|
|
9
|
+
|
|
10
|
+
const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
|
|
11
|
+
|
|
12
|
+
const CODE = /\.[cm]?[jt]sx?$/u;
|
|
13
|
+
|
|
14
|
+
const FolderNamesOptions = Schema.Struct({
|
|
15
|
+
content: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
16
|
+
toolOwned: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TOOL_OWNED))),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function foldersIn(files: readonly string[]): readonly string[] {
|
|
20
|
+
const folders = new Set<string>();
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
for (let folder = posix.dirname(file); folder !== "."; folder = posix.dirname(folder)) {
|
|
23
|
+
folders.add(folder);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...folders].sort();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stemsIn(files: readonly string[]): ReadonlySet<string> {
|
|
30
|
+
return new Set(files.filter((file) => CODE.test(file)).map((file) => `${posix.dirname(file)}/${posix.basename(file).replace(CODE, "")}`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isModule(folder: string, stems: ReadonlySet<string>): boolean {
|
|
34
|
+
const name = posix.basename(folder);
|
|
35
|
+
return stems.has(`${folder}/${name}`) || stems.has(`${posix.dirname(folder)}/${name}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function packageMessage(name: string, packageName: string): string | undefined {
|
|
39
|
+
const expected = packageName.replace(/^@[^/]+\//u, "");
|
|
40
|
+
return name === expected ? undefined : `The folder of the package ${packageName} is named "${expected}", in kebab-case after the package name.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function groupMessage(name: string): string | undefined {
|
|
44
|
+
if (PASCAL.test(name)) {
|
|
45
|
+
return `"${name}" is PascalCase but holds no ${name}.ts or ${name}.tsx. A PascalCase folder is a module named after its main file: add that file, or name the folder in kebab-case.`;
|
|
46
|
+
}
|
|
47
|
+
return KEBAB.test(name.replace(/^\./u, ""))
|
|
48
|
+
? undefined
|
|
49
|
+
: `"${name}" groups files, so it is named in kebab-case, such as test-support/. A folder named after a file beside or inside it is a module and takes that file's name.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Context {
|
|
53
|
+
readonly content: Ignore;
|
|
54
|
+
readonly names: ReadonlyMap<string, string>;
|
|
55
|
+
readonly stems: ReadonlySet<string>;
|
|
56
|
+
readonly toolOwned: Ignore;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function messageFor(folder: string, context: Context): string | undefined {
|
|
60
|
+
const name = posix.basename(folder);
|
|
61
|
+
if (context.toolOwned.ignores(`${folder}/`) || isModule(folder, context.stems)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const packageName = context.names.get(folder);
|
|
65
|
+
if (packageName !== undefined) {
|
|
66
|
+
return packageMessage(name, packageName);
|
|
67
|
+
}
|
|
68
|
+
if (context.content.ignores(`${folder}/`)) {
|
|
69
|
+
return SNAKE.test(name) ? undefined : `"${name}" holds content, so it is named in snake_case, such as forest_path/.`;
|
|
70
|
+
}
|
|
71
|
+
return groupMessage(name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const folderNames = defineRule({
|
|
75
|
+
check: async ({ files, options, readText }) => {
|
|
76
|
+
const layout = await packageLayout({ files, readText });
|
|
77
|
+
const context: Context = {
|
|
78
|
+
content: ignore().add([...options.content]),
|
|
79
|
+
names: layout.names,
|
|
80
|
+
stems: stemsIn(files),
|
|
81
|
+
toolOwned: ignore().add([...options.toolOwned]),
|
|
82
|
+
};
|
|
83
|
+
return foldersIn(files).flatMap((folder): readonly Finding[] => {
|
|
84
|
+
const message = messageFor(folder, context);
|
|
85
|
+
return message === undefined ? [] : [{ file: folder, message }];
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
description:
|
|
89
|
+
"A package folder is named after its package, a module folder after its main file, a content folder in snake_case and every other folder in kebab-case.",
|
|
90
|
+
id: "files/folder-names",
|
|
91
|
+
options: Schema.toStandardSchemaV1(FolderNamesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
92
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import ignore from "ignore";
|
|
4
|
+
import { exportFits } from "#naming/exportFits.ts";
|
|
5
|
+
import { mainExport } from "#naming/mainExport.ts";
|
|
6
|
+
import { ownExports } from "#naming/ownExports.ts";
|
|
7
|
+
import { type PackageLayout, packageLayout } from "#naming/packageLayout.ts";
|
|
8
|
+
import { CAMEL, KEBAB, sameWord, wordsOf } from "#naming/words.ts";
|
|
9
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
10
|
+
import { parse } from "#rules/syntax.ts";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = ["*.config.*", "*.d.ts", "*.d.mts", "*.d.cts"];
|
|
13
|
+
|
|
14
|
+
const TEST = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
15
|
+
|
|
16
|
+
const SHEBANG = "#!";
|
|
17
|
+
|
|
18
|
+
const SHAPES =
|
|
19
|
+
"The file name and the names of its folders spell the export, in any order, and the file's first letter follows the export's case: listItems in items/ is items/list.ts, ItemPanel is ItemPanel.tsx or item/Panel.tsx.";
|
|
20
|
+
|
|
21
|
+
const NamedAfterExportOptions = Schema.Struct({
|
|
22
|
+
toolOwned: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TOOL_OWNED))),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function stemOf(path: string): string {
|
|
26
|
+
return posix.basename(path).replace(/\.[^.]+$/u, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function repeated(stem: string, folders: readonly string[]): string | undefined {
|
|
30
|
+
if (folders.at(-1) === stem) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const words = wordsOf(stem);
|
|
34
|
+
return folders.find((folder) => wordsOf(folder).some((word) => words.some((own) => sameWord(own, word))));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nameFinding(file: SourceFile, stem: string, folders: readonly string[]): string | undefined {
|
|
38
|
+
const source = parse(file);
|
|
39
|
+
const exports = source === undefined ? [] : ownExports(source);
|
|
40
|
+
const [only] = exports;
|
|
41
|
+
if (only === undefined) {
|
|
42
|
+
return CAMEL.test(stem)
|
|
43
|
+
? undefined
|
|
44
|
+
: `"${stem}" exports nothing, so it is named in camelCase, such as setupTests.ts. A file that a package.json script or bin runs is kebab-case.`;
|
|
45
|
+
}
|
|
46
|
+
if (exports.length === 1 && only.constant) {
|
|
47
|
+
return `"${stem}" exports only the constant ${only.name}. Constants live in a topic file with related constants, such as limits.ts holding MAX_ITEMS and MAX_DEPTH.`;
|
|
48
|
+
}
|
|
49
|
+
if (exports.some((entry) => exportFits(entry.name, stem, folders))) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const main = mainExport(exports);
|
|
53
|
+
if (main !== undefined) {
|
|
54
|
+
return `"${stem}" does not name its export ${main.name}. ${SHAPES} Rename the file or the export.`;
|
|
55
|
+
}
|
|
56
|
+
return CAMEL.test(stem)
|
|
57
|
+
? undefined
|
|
58
|
+
: `"${stem}" exports several things, so it is a topic file named in camelCase, such as limits.ts. Or split it so each file has one main export. ${SHAPES}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findingsOf(file: SourceFile, layout: PackageLayout): readonly Finding[] {
|
|
62
|
+
const stem = stemOf(file.path);
|
|
63
|
+
const folders = layout.foldersOf(file.path);
|
|
64
|
+
if (layout.scripts.has(file.path) || file.text.startsWith(SHEBANG)) {
|
|
65
|
+
return KEBAB.test(stem)
|
|
66
|
+
? []
|
|
67
|
+
: [{ file: file.path, message: `"${stem}" is run as a script, so it is named in kebab-case, such as build-docs.ts.` }];
|
|
68
|
+
}
|
|
69
|
+
const folder = repeated(stem, folders);
|
|
70
|
+
const messages = [
|
|
71
|
+
nameFinding(file, stem, folders),
|
|
72
|
+
folder === undefined ? undefined : `"${stem}" repeats its folder "${folder}". The folder is the prefix: items/listItems.ts is items/list.ts.`,
|
|
73
|
+
];
|
|
74
|
+
return messages.flatMap((message) => (message === undefined ? [] : [{ file: file.path, message }]));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const namedAfterExport = defineRule({
|
|
78
|
+
check: async ({ files, options, readText, sources }) => {
|
|
79
|
+
const layout = await packageLayout({ files, readText });
|
|
80
|
+
const toolOwned = ignore().add([...options.toolOwned]);
|
|
81
|
+
return sources.filter((file) => !(TEST.test(file.path) || toolOwned.ignores(file.path))).flatMap((file) => findingsOf(file, layout));
|
|
82
|
+
},
|
|
83
|
+
description:
|
|
84
|
+
"A file is named after its main export, with its folders as the prefix; a file of several exports is a camelCase topic file, and a script is kebab-case. Rename the file or the export; nothing renames them for you.",
|
|
85
|
+
id: "files/named-after-export",
|
|
86
|
+
options: Schema.toStandardSchemaV1(NamedAfterExportOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
87
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import ignore, { type Ignore } from "ignore";
|
|
4
|
+
import { CAMEL, KEBAB } from "#naming/words.ts";
|
|
5
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = [
|
|
8
|
+
".*",
|
|
9
|
+
"package.json",
|
|
10
|
+
"tsconfig*.json",
|
|
11
|
+
"biome.json",
|
|
12
|
+
"biome.jsonc",
|
|
13
|
+
"*.config.*",
|
|
14
|
+
"migrations/",
|
|
15
|
+
"generated/",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const CONVENTIONAL: ReadonlySet<string> = new Set(["AGENTS", "CHANGELOG", "CLAUDE", "CONTRIBUTING", "LICENSE", "README", "SECURITY", "SKILL"]);
|
|
19
|
+
|
|
20
|
+
const KEBAB_KINDS: ReadonlySet<string> = new Set([
|
|
21
|
+
"avif",
|
|
22
|
+
"gif",
|
|
23
|
+
"grit",
|
|
24
|
+
"ico",
|
|
25
|
+
"jpeg",
|
|
26
|
+
"jpg",
|
|
27
|
+
"json",
|
|
28
|
+
"jsonc",
|
|
29
|
+
"md",
|
|
30
|
+
"mdx",
|
|
31
|
+
"otf",
|
|
32
|
+
"png",
|
|
33
|
+
"svg",
|
|
34
|
+
"ttf",
|
|
35
|
+
"webp",
|
|
36
|
+
"woff",
|
|
37
|
+
"woff2",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const STYLESHEETS: ReadonlySet<string> = new Set(["css", "scss"]);
|
|
41
|
+
|
|
42
|
+
const DOTTED_KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$/u;
|
|
43
|
+
|
|
44
|
+
const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
|
|
45
|
+
|
|
46
|
+
const STYLESHEET_IMPORT = /["'](?<stylesheet>\.{1,2}\/[^"'\n]+\.s?css)["']/gu;
|
|
47
|
+
|
|
48
|
+
const CODE = /\.[cm]?[jt]sx?$/u;
|
|
49
|
+
|
|
50
|
+
const OtherNamesOptions = Schema.Struct({
|
|
51
|
+
content: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
52
|
+
toolOwned: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TOOL_OWNED))),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function importers(sources: readonly SourceFile[]): ReadonlyMap<string, readonly string[]> {
|
|
56
|
+
const found = new Map<string, string[]>();
|
|
57
|
+
for (const source of sources) {
|
|
58
|
+
for (const match of source.text.matchAll(STYLESHEET_IMPORT)) {
|
|
59
|
+
const target = posix.normalize(posix.join(posix.dirname(source.path), match.groups?.stylesheet ?? ""));
|
|
60
|
+
found.set(target, [...new Set([...(found.get(target) ?? []), source.path])]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stylesheetMessage(path: string, stem: string, importedBy: readonly string[]): string | undefined {
|
|
67
|
+
const [only] = importedBy;
|
|
68
|
+
if (importedBy.length === 1 && only !== undefined) {
|
|
69
|
+
const owner = posix.basename(only).replace(CODE, "");
|
|
70
|
+
return stem === owner ? undefined : `"${stem}" is styling for ${posix.basename(only)} alone, so it is named ${owner}${posix.extname(path)}.`;
|
|
71
|
+
}
|
|
72
|
+
return KEBAB.test(stem) ? undefined : `"${stem}" is a shared stylesheet, so it is named in kebab-case, such as form-controls.css.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Context {
|
|
76
|
+
readonly content: Ignore;
|
|
77
|
+
readonly stylesheets: ReadonlyMap<string, readonly string[]>;
|
|
78
|
+
readonly toolOwned: Ignore;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function messageFor(path: string, context: Context): string | undefined {
|
|
82
|
+
const name = posix.basename(path);
|
|
83
|
+
const kind = posix.extname(name).slice(1);
|
|
84
|
+
const stem = name.slice(0, name.length - kind.length - 1);
|
|
85
|
+
if (kind === "" || context.toolOwned.ignores(path) || CONVENTIONAL.has(stem)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (context.content.ignores(path)) {
|
|
89
|
+
return SNAKE.test(stem) ? undefined : `"${name}" is content, so it is named in snake_case, such as forest_path.json.`;
|
|
90
|
+
}
|
|
91
|
+
if (STYLESHEETS.has(kind)) {
|
|
92
|
+
return stylesheetMessage(path, stem, context.stylesheets.get(path) ?? []);
|
|
93
|
+
}
|
|
94
|
+
if (kind === "prisma") {
|
|
95
|
+
return CAMEL.test(stem) ? undefined : `"${name}" is a Prisma schema, so it is named in camelCase, such as schema.prisma.`;
|
|
96
|
+
}
|
|
97
|
+
return KEBAB_KINDS.has(kind) && !DOTTED_KEBAB.test(stem)
|
|
98
|
+
? `"${name}" is named in kebab-case, such as release-notes.md. Only conventional names such as README.md and CHANGELOG.md are upper case.`
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const otherNames = defineRule({
|
|
103
|
+
check: ({ files, options, sources }) => {
|
|
104
|
+
const context: Context = {
|
|
105
|
+
content: ignore().add([...options.content]),
|
|
106
|
+
stylesheets: importers(sources),
|
|
107
|
+
toolOwned: ignore().add([...options.toolOwned]),
|
|
108
|
+
};
|
|
109
|
+
return files.flatMap((path): readonly Finding[] => {
|
|
110
|
+
const message = messageFor(path, context);
|
|
111
|
+
return message === undefined ? [] : [{ file: path, message }];
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
description:
|
|
115
|
+
"Documents, data, assets and GritQL files are kebab-case, a stylesheet that one component imports is named after it, a Prisma schema is camelCase and content is snake_case.",
|
|
116
|
+
id: "files/other-names",
|
|
117
|
+
options: Schema.toStandardSchemaV1(OtherNamesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
118
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Departure, departures } from "
|
|
2
|
-
import { defineRule } from "
|
|
1
|
+
import { type Departure, departures } from "#imports/departures.ts";
|
|
2
|
+
import { defineRule } from "#rule.ts";
|
|
3
3
|
|
|
4
4
|
function messageOf({ installed, specifier, workspace }: Departure): string {
|
|
5
5
|
const leaves = `"${specifier}" leaves its folder.`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { cyclicComponents } from "
|
|
2
|
-
import { type ImportEdge, importGraph } from "
|
|
3
|
-
import { shortestPath } from "
|
|
4
|
-
import { defineRule, type Finding } from "
|
|
1
|
+
import { cyclicComponents } from "#imports/components.ts";
|
|
2
|
+
import { type ImportEdge, importGraph } from "#imports/graph.ts";
|
|
3
|
+
import { shortestPath } from "#imports/paths.ts";
|
|
4
|
+
import { defineRule, type Finding } from "#rule.ts";
|
|
5
5
|
|
|
6
6
|
type RuntimeEdge = ImportEdge & { readonly to: { readonly kind: "file"; readonly path: string } };
|
|
7
7
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import type { Fence } from "
|
|
3
|
-
import { compilePolicy } from "
|
|
4
|
-
import { importGraph } from "
|
|
5
|
-
import { defineRule } from "
|
|
2
|
+
import type { Fence } from "#imports/fences/model.ts";
|
|
3
|
+
import { compilePolicy } from "#imports/fences/policy.ts";
|
|
4
|
+
import { importGraph } from "#imports/graph.ts";
|
|
5
|
+
import { defineRule } from "#rule.ts";
|
|
6
6
|
|
|
7
7
|
function isFence(value: unknown): value is Fence {
|
|
8
8
|
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "Fence";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { withoutGenerated } from "
|
|
3
|
-
import { importGraph } from "
|
|
4
|
-
import { defineRule } from "
|
|
2
|
+
import { withoutGenerated } from "#imports/generated.ts";
|
|
3
|
+
import { importGraph } from "#imports/graph.ts";
|
|
4
|
+
import { defineRule } from "#rule.ts";
|
|
5
5
|
|
|
6
6
|
const ResolvableOptions = Schema.Struct({
|
|
7
7
|
generated: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import { manifestsIn, sortingOf } from "
|
|
4
|
-
import { defineRule, type Finding } from "
|
|
3
|
+
import { manifestsIn, sortingOf } from "#manifests/sorted.ts";
|
|
4
|
+
import { defineRule, type Finding } from "#rule.ts";
|
|
5
5
|
|
|
6
6
|
const MESSAGES = {
|
|
7
7
|
Invalid: "Is not valid JSON.",
|
package/src/rules/max-lines.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import ignore from "ignore";
|
|
3
|
-
import { defineRule, type Finding, type SourceFile } from "
|
|
3
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TEST_FILES: readonly string[] = ["*.test.*", "*.spec.*", "test/", "tests/", "__tests__/"];
|
|
6
6
|
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
-
import type { Finding, RuleInputs } from "
|
|
2
|
+
import type { Finding, RuleInputs } from "#rule.ts";
|
|
3
3
|
import { type Declaration, decodeShipped, PRESET_DECLARATIONS } from "./declarations.ts";
|
|
4
4
|
import { itemsOf, type Json, member, type Parsed, parseJsonc, textOf } from "./json.ts";
|
|
5
5
|
import { resolvePackage } from "./resolve.ts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
-
import { decodeWith } from "
|
|
4
|
-
import type { RuleInputs } from "
|
|
3
|
+
import { decodeWith } from "#decoded.ts";
|
|
4
|
+
import type { RuleInputs } from "#rule.ts";
|
|
5
5
|
|
|
6
6
|
const Manifest = Schema.Struct({
|
|
7
7
|
exports: Schema.optionalKey(Schema.Unknown),
|