Skip to content
Source
Sign inSign up
Open History to see the last commit

changedFileKind.ts

87 lines2.8 KB

1import type { PullRequestChangeBreakdown } from "@t3tools/contracts";
2​
3export type ChangedFileKind = keyof NonNullable<PullRequestChangeBreakdown>;
4​
5const GENERATED_FILES = new Set([
6 "bun.lock",
7 "bun.lockb",
8 "cargo.lock",
9 "composer.lock",
10 "deno.lock",
11 "flake.lock",
12 "gemfile.lock",
13 "go.sum",
14 "gradle.lockfile",
15 "mix.lock",
16 "npm-shrinkwrap.json",
17 "package-lock.json",
18 "packages.lock.json",
19 "pipfile.lock",
20 "pnpm-lock.yaml",
21 "podfile.lock",
22 "poetry.lock",
23 "pubspec.lock",
24 "uv.lock",
25 "yarn.lock",
26]);
27const GENERATED_NAME =
28 /(\.generated\.|\.gen\.|\.g\.dart$|\.pb\.go$|_pb2(_grpc)?\.py$|\.pb\.(cc|h)$|\.min\.(js|css)$|\.(js|css)\.map$)/;
29const GENERATED_DIRS = new Set(["__generated__", "_generated", "generated"]);
30​
31const MOCK_NAME =
32 /(\.snap$|\.(mocks?|fixtures?|stubs?|fakes?)\.[^.]+$|^(mocks?|fixtures?|stubs?|fakes?)([-_.A-Z]|$))/;
33const MOCK_DIRS = new Set([
34 "__fixtures__",
35 "__mocks__",
36 "__snapshots__",
37 "fakes",
38 "fixture",
39 "fixtures",
40 "mock",
41 "mocks",
42 "stubs",
43 "testdata",
44]);
45​
46const TEST_NAME =
47 /(\.(test|spec|e2e|eval)\.[^.]+$|_(test|spec)\.[^.]+$|^test_.+\.py$|^conftest\.py$|^(vitest|jest)\.setup\.[^.]+$)/i;
48// Case-sensitive: `FooTest.java` is a test class, `Latest.java` is not.
49const CLASS_TEST_NAME = /(Tests?|Spec)\.(java|kt|kts|scala|groovy|cs|php|swift|m)$/;
50const TEST_DIRS = new Set([
51 "__tests__",
52 "__test__",
53 "cypress",
54 "e2e",
55 "evals",
56 "spec",
57 "specs",
58 "test",
59 "tests",
60]);
61​
62const DOCS_NAME = /\.(md|mdx|markdown|rst|adoc|txt)$/i;
63// Plain-text files that tools read as config or input, not prose.
64const NOT_DOCS_NAME = /^(requirements.*|constraints.*|CMakeLists|robots|llms)\.txt$/i;
65​
66/**
67 * Sorts a changed file by its path alone, using common naming conventions. Lockfiles and build
68 * output count as generated, and test support (mocks, fixtures, snapshots) is kept apart from the
69 * tests themselves, and prose (Markdown, plain text) is its own kind, so a reviewer can see how much of a change is logic.
70 */
71export function isTestChangedFile(path: string): boolean {
72 const kind = classifyChangedFile(path);
73 return kind === "tests" || kind === "mocks";
75​
76export function classifyChangedFile(path: string): ChangedFileKind {
77 const segments = path.replaceAll("\\", "/").split("/");
78 const name = segments.pop() ?? "";
79 const dirs = segments.map((segment) => segment.toLowerCase());
80 const inDir = (set: ReadonlySet<string>) => dirs.some((dir) => set.has(dir));
81 if (GENERATED_FILES.has(name.toLowerCase()) || GENERATED_NAME.test(name) || inDir(GENERATED_DIRS))
82 return "generated";
83 if (MOCK_NAME.test(name) || inDir(MOCK_DIRS)) return "mocks";
84 if (TEST_NAME.test(name) || CLASS_TEST_NAME.test(name) || inDir(TEST_DIRS)) return "tests";
85 if (DOCS_NAME.test(name) && !NOT_DOCS_NAME.test(name)) return "docs";
86 return "code";