pr-checkmate 2.1.2 → 3.0.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.
Files changed (97) hide show
  1. package/README.md +63 -268
  2. package/cspell.json +155 -133
  3. package/dist/config/constants.d.ts +6 -1
  4. package/dist/config/constants.js +8 -7
  5. package/dist/config/index.d.ts +23 -11
  6. package/dist/config/index.js +27 -33
  7. package/dist/core/checks/dependencies/banned-imports.js +4 -1
  8. package/dist/core/checks/dependencies/circular-deps.js +14 -5
  9. package/dist/core/checks/dependencies/dependency-check.js +1 -1
  10. package/dist/core/checks/dependencies/grype-scan.js +2 -1
  11. package/dist/core/checks/dependencies/vuln-scan.js +2 -1
  12. package/dist/core/checks/findings.d.ts +46 -2
  13. package/dist/core/checks/findings.js +140 -2
  14. package/dist/core/checks/git/custom-rules.js +2 -0
  15. package/dist/core/checks/git/diff-smell.js +6 -1
  16. package/dist/core/checks/git/leftover-debug.js +8 -2
  17. package/dist/core/checks/git/lockfile-drift.js +6 -1
  18. package/dist/core/checks/git/merge-conflict.js +1 -1
  19. package/dist/core/checks/git/missing-tests.js +1 -1
  20. package/dist/core/checks/git/todo-fixme.js +10 -2
  21. package/dist/core/checks/languages/cpp-format.js +19 -42
  22. package/dist/core/checks/languages/external-checks.d.ts +77 -0
  23. package/dist/core/checks/languages/external-checks.js +200 -0
  24. package/dist/core/checks/languages/external-runner.d.ts +119 -0
  25. package/dist/core/checks/languages/external-runner.js +143 -0
  26. package/dist/core/checks/languages/go-format.d.ts +4 -4
  27. package/dist/core/checks/languages/go-format.js +151 -30
  28. package/dist/core/checks/languages/lint.js +19 -19
  29. package/dist/core/checks/languages/prettier.js +2 -7
  30. package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
  31. package/dist/core/checks/languages/python-typecheck.js +161 -37
  32. package/dist/core/checks/languages/typecheck.js +10 -8
  33. package/dist/core/checks/pr/commitlint.js +7 -1
  34. package/dist/core/checks/quality/case-collision.js +6 -4
  35. package/dist/core/checks/quality/config-validation.d.ts +0 -7
  36. package/dist/core/checks/quality/config-validation.js +30 -0
  37. package/dist/core/checks/quality/duplicate-check.js +54 -27
  38. package/dist/core/checks/quality/license-header.js +7 -4
  39. package/dist/core/checks/quality/markdown-lint.js +1 -6
  40. package/dist/core/checks/quality/spellcheck.js +34 -5
  41. package/dist/core/checks/quality/symlinks.js +6 -4
  42. package/dist/core/checks/security/diff-security.js +26 -18
  43. package/dist/core/checks/security/dockerfile-security.js +7 -1
  44. package/dist/core/checks/security/migration-safety.js +32 -4
  45. package/dist/core/checks/security/security.d.ts +56 -0
  46. package/dist/core/checks/security/security.js +135 -11
  47. package/dist/core/checks/security/sensitive-file-guard.js +40 -6
  48. package/dist/core/checks/security/workflow-security.js +7 -1
  49. package/dist/core/checks/tool-install.d.ts +102 -0
  50. package/dist/core/checks/tool-install.js +69 -0
  51. package/dist/core/context.js +9 -1
  52. package/dist/core/install/cache.js +42 -11
  53. package/dist/core/install/doctor.js +9 -0
  54. package/dist/core/install/ensure.js +22 -3
  55. package/dist/core/install/lock.d.ts +2 -1
  56. package/dist/core/install/prepare.d.ts +75 -0
  57. package/dist/core/install/prepare.js +151 -0
  58. package/dist/core/install/profiles.d.ts +17 -3
  59. package/dist/core/install/profiles.js +27 -0
  60. package/dist/core/install/reuse.d.ts +41 -0
  61. package/dist/core/install/reuse.js +107 -0
  62. package/dist/core/registry.js +10 -18
  63. package/dist/core/run-checks.js +74 -3
  64. package/dist/core/targets.d.ts +116 -5
  65. package/dist/core/targets.js +101 -16
  66. package/dist/core/types.d.ts +8 -2
  67. package/dist/init.js +21 -1
  68. package/dist/interfaces/index.d.ts +1 -0
  69. package/dist/utils/resolve-bin.d.ts +56 -6
  70. package/dist/utils/resolve-bin.js +136 -26
  71. package/gitleaks.toml +14 -0
  72. package/install-manifests/go/package-lock.json +24 -0
  73. package/install-manifests/go/package.json +8 -0
  74. package/install-manifests/pytypes/package-lock.json +45 -0
  75. package/install-manifests/pytypes/package.json +8 -0
  76. package/package.json +4 -2
  77. package/pr-checkmate.schema.json +8 -2
  78. package/src/config/cspell.json +155 -133
  79. package/src/config/pr-checkmate.schema.json +8 -2
  80. package/dist/core/checks/languages/csharp-format.d.ts +0 -13
  81. package/dist/core/checks/languages/csharp-format.js +0 -52
  82. package/dist/core/checks/languages/go-lint.d.ts +0 -8
  83. package/dist/core/checks/languages/go-lint.js +0 -48
  84. package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
  85. package/dist/core/checks/languages/kotlin-lint.js +0 -42
  86. package/dist/core/checks/languages/php-format.d.ts +0 -9
  87. package/dist/core/checks/languages/php-format.js +0 -49
  88. package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
  89. package/dist/core/checks/languages/ruby-lint.js +0 -50
  90. package/dist/core/checks/languages/rust-format.d.ts +0 -9
  91. package/dist/core/checks/languages/rust-format.js +0 -45
  92. package/dist/core/checks/languages/rust-lint.d.ts +0 -8
  93. package/dist/core/checks/languages/rust-lint.js +0 -50
  94. package/dist/core/checks/languages/shellcheck.d.ts +0 -8
  95. package/dist/core/checks/languages/shellcheck.js +0 -55
  96. package/dist/core/checks/languages/swift-lint.d.ts +0 -2
  97. package/dist/core/checks/languages/swift-lint.js +0 -46
@@ -3,51 +3,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerToolPaths = registerToolPaths;
7
+ exports.clearToolPaths = clearToolPaths;
8
+ exports.ownPackageRoot = ownPackageRoot;
9
+ exports.packagePresent = packagePresent;
10
+ exports.resolveTool = resolveTool;
6
11
  exports.resolvePackageBin = resolvePackageBin;
7
12
  const node_fs_1 = __importDefault(require("node:fs"));
8
13
  const node_path_1 = __importDefault(require("node:path"));
9
14
  /**
10
- * Resolves a binary path for a bundled package, bypassing exports map restrictions.
15
+ * Extra roots to search, registered once per run by the install layer.
11
16
  *
12
- * Strategy (tries each in order):
13
- * 1. Direct subpath — fast path for packages that explicitly export their bin
14
- * 2. Via package.json — for packages that export ./package.json but not the bin subpath (e.g. eslint)
15
- * 3. Walk up from main entry — for packages that export neither (e.g. cspell)
17
+ * Module state rather than a parameter threaded through fifteen call sites: a
18
+ * profile directory is a property of the *run*, not of any one check, and every
19
+ * check that resolves a binary would otherwise have to be handed a value it has
20
+ * no opinion about. `registerToolPaths` is called before the first check starts
21
+ * and never during one, so nothing observes it changing underneath.
22
+ */
23
+ let profileRoots = [];
24
+ /**
25
+ * Point tool resolution at the profile directories fetched for this run.
16
26
  *
17
- * @param pkgName - The package whose binary to resolve (e.g. `eslint`).
18
- * @param binRelPath - The bin path relative to the package root (e.g. `bin/eslint.js`).
19
- * @param options - Resolution options.
20
- * @param options.paths - Directories to resolve from (passed to `require.resolve`).
21
- * Use `[process.cwd()]` to prefer the consumer project's copy of the package.
27
+ * Called by the install layer once the cache is known. Passing an empty list —
28
+ * or never calling it at all — leaves resolution exactly as it was before
29
+ * profiles existed, which is what keeps a run that needs no fetching identical
30
+ * to a run from before there was anything to fetch.
22
31
  */
23
- function resolvePackageBin(pkgName, binRelPath, options) {
24
- const resolveOpts = options?.paths ? { paths: options.paths } : undefined;
25
- // 1. Direct subpath
32
+ function registerToolPaths(dirs) {
33
+ profileRoots = [...dirs];
34
+ }
35
+ /** Drop every registered profile root. For tests, and for a second run in-process. */
36
+ function clearToolPaths() {
37
+ profileRoots = [];
38
+ }
39
+ /** The root of the installed pr-checkmate package — where our own tooling lives. */
40
+ function ownPackageRoot() {
41
+ return node_path_1.default.resolve(__dirname, '../..');
42
+ }
43
+ /**
44
+ * Whether a package is physically present in our own dependency tree.
45
+ *
46
+ * Checked by walking `node_modules` upwards rather than by `require.resolve`,
47
+ * because a package that restricts its `exports` map resolves for neither its
48
+ * main entry nor its `package.json` while being perfectly well installed —
49
+ * `cspell` is one. The question here is only "is it there", and the directory
50
+ * answers that without an opinion about what it chooses to export.
51
+ *
52
+ * Walking up matters as much as the check itself: installed as a dependency, our
53
+ * own packages are hoisted to the client's `node_modules`, one level above ours.
54
+ */
55
+ function packagePresent(pkgName, from = ownPackageRoot()) {
56
+ let dir = from;
57
+ while (dir !== node_path_1.default.dirname(dir)) {
58
+ if (node_fs_1.default.existsSync(node_path_1.default.join(dir, 'node_modules', pkgName, 'package.json')))
59
+ return true;
60
+ dir = node_path_1.default.dirname(dir);
61
+ }
62
+ return false;
63
+ }
64
+ /** One resolution attempt against a single set of search paths. */
65
+ function tryResolve(pkgName, binRelPath, paths) {
66
+ const resolveOpts = paths ? { paths } : undefined;
67
+ // 1. Direct subpath — the fast path for packages that export their bin.
26
68
  try {
27
69
  return require.resolve(`${pkgName}/${binRelPath}`, resolveOpts);
28
70
  }
29
71
  catch { }
30
- // 2. Via package.json
72
+ // 2. Via package.json — for packages that export `./package.json` but not the
73
+ // bin subpath (eslint).
31
74
  try {
32
75
  const pkgJsonPath = require.resolve(`${pkgName}/package.json`, resolveOpts);
33
76
  return node_path_1.default.join(node_path_1.default.dirname(pkgJsonPath), binRelPath);
34
77
  }
35
78
  catch { }
36
- // 3. Walk up from main entry to find package root by matching package name
37
- const main = require.resolve(pkgName, resolveOpts);
38
- let dir = node_path_1.default.dirname(main);
39
- while (dir !== node_path_1.default.dirname(dir)) {
40
- const pkgJsonPath = node_path_1.default.join(dir, 'package.json');
41
- if (node_fs_1.default.existsSync(pkgJsonPath)) {
42
- try {
43
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
44
- if (pkg.name === pkgName) {
45
- return node_path_1.default.join(dir, binRelPath);
79
+ // 3. Walk up from the main entry, matching the package name — for packages
80
+ // that export neither (cspell).
81
+ try {
82
+ let dir = node_path_1.default.dirname(require.resolve(pkgName, resolveOpts));
83
+ while (dir !== node_path_1.default.dirname(dir)) {
84
+ const pkgJsonPath = node_path_1.default.join(dir, 'package.json');
85
+ if (node_fs_1.default.existsSync(pkgJsonPath)) {
86
+ try {
87
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
88
+ if (pkg.name === pkgName)
89
+ return node_path_1.default.join(dir, binRelPath);
46
90
  }
91
+ catch { }
47
92
  }
48
- catch { }
93
+ dir = node_path_1.default.dirname(dir);
49
94
  }
50
- dir = node_path_1.default.dirname(dir);
51
95
  }
96
+ catch { }
97
+ return null;
98
+ }
99
+ /**
100
+ * The roots to try, in order, and what finding a tool in each one means.
101
+ *
102
+ * The order is the policy, and it is deliberate:
103
+ *
104
+ * 1. **The client's own copy**, when a caller asked for it. A repository pinned
105
+ * to ESLint 8 with a legacy `.eslintrc` must keep working, and only its copy
106
+ * can do that.
107
+ * 2. **A fetched profile**, for tooling that does not ship inside the package.
108
+ * 3. **Our bundle** — module-relative, exactly as resolution worked before
109
+ * profiles existed.
110
+ *
111
+ * Branch 3 is passed `undefined` rather than our package root on purpose: that is
112
+ * literally the previous behaviour, so a run with nothing registered resolves
113
+ * every tool along the identical path it always did.
114
+ */
115
+ function candidates(optPaths) {
116
+ const list = [];
117
+ if (optPaths && optPaths.length > 0)
118
+ list.push({ paths: optPaths, source: 'client' });
119
+ for (const root of profileRoots)
120
+ list.push({ paths: [root], source: 'profile' });
121
+ list.push({ source: 'bundled' });
122
+ return list;
123
+ }
124
+ /**
125
+ * Resolve a tool's binary and say which copy answered.
126
+ *
127
+ * Use this over {@link resolvePackageBin} when the answer is worth logging — the
128
+ * source is the difference between "the client's ESLint 8 ran" and "ours did",
129
+ * and that is the first thing anyone asks when a lint result looks wrong.
130
+ *
131
+ * @param pkgName Package whose binary to resolve (e.g. `eslint`).
132
+ * @param binRelPath Bin path relative to the package root (e.g. `bin/eslint.js`).
133
+ * @param options Resolution options.
134
+ * @param options.paths Roots to prefer ahead of everything else. Use
135
+ * `[process.cwd()]` to prefer the consumer project's copy.
136
+ */
137
+ function resolveTool(pkgName, binRelPath, options) {
138
+ for (const candidate of candidates(options?.paths)) {
139
+ const bin = tryResolve(pkgName, binRelPath, candidate.paths);
140
+ if (bin)
141
+ return { bin, source: candidate.source, root: candidate.paths?.[0] };
142
+ }
143
+ return null;
144
+ }
145
+ /**
146
+ * Resolves a binary path for a bundled package, bypassing exports map restrictions.
147
+ *
148
+ * Throws when no copy can be found anywhere, which is the honest outcome: a check
149
+ * that cannot find its tool has nothing to report, and the install layer turns
150
+ * that into a skip with a reason rather than letting it surface as a stack trace.
151
+ *
152
+ * @param pkgName - The package whose binary to resolve (e.g. `eslint`).
153
+ * @param binRelPath - The bin path relative to the package root (e.g. `bin/eslint.js`).
154
+ * @param options - Resolution options.
155
+ * @param options.paths - Directories to resolve from, preferred over everything
156
+ * else. Use `[process.cwd()]` to prefer the consumer project's copy.
157
+ */
158
+ function resolvePackageBin(pkgName, binRelPath, options) {
159
+ const resolved = resolveTool(pkgName, binRelPath, options);
160
+ if (resolved)
161
+ return resolved.bin;
52
162
  throw new Error(`Cannot resolve binary "${binRelPath}" for package "${pkgName}"`);
53
163
  }
package/gitleaks.toml CHANGED
@@ -3,6 +3,20 @@ title = "pr-checkmate gitleaks config"
3
3
  [extend]
4
4
  useDefault = true
5
5
 
6
+ # THIS FILE SHIPS, AND IT IS APPLIED TO EVERY CLIENT WITHOUT A GITLEAKS CONFIG OF
7
+ # THEIR OWN. Read `src/core/checks/security/security.ts`: `--config` points here
8
+ # whenever `hasLocalConfig` finds nothing in the repository being scanned.
9
+ #
10
+ # So an entry added here to quieten OUR OWN corpus silences it for everybody. That
11
+ # was measured, not imagined: adding `tests/` to the paths below removed six real
12
+ # `Generic API Key` findings from a client repository — under
13
+ # `packages/commons/tests/`, `packages/node/tests/`, and three more — in a check
14
+ # that BLOCKS. Our own noise is never a reason to widen this list; use
15
+ # `.gitleaksignore` in this repository instead, which does not ship.
16
+ #
17
+ # `__tests__` predates that and is left alone: it is a convention for a directory
18
+ # of test files specifically, not a whole path segment named `test`.
19
+
6
20
  [allowlist]
7
21
  description = "pr-checkmate global allowlist"
8
22
  paths = [
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "pr-checkmate-profile-go",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pr-checkmate-profile-go",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@wasm-fmt/gofmt": "0.7.3"
12
+ }
13
+ },
14
+ "node_modules/@wasm-fmt/gofmt": {
15
+ "version": "0.7.3",
16
+ "resolved": "https://registry.npmjs.org/@wasm-fmt/gofmt/-/gofmt-0.7.3.tgz",
17
+ "integrity": "sha512-nzVTwMxHDOYrHonT7DzjqicbaTYOYPNYeXKPJnDrVUbJjx67Sg/LS1uzlOTWKToGMF8Y8l0u7G5/5Q3/8nHMMA==",
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=16.17.0"
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "pr-checkmate-profile-go",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "@wasm-fmt/gofmt": "0.7.3"
6
+ },
7
+ "private": true
8
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "pr-checkmate-profile-pytypes",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pr-checkmate-profile-pytypes",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "pyright": "1.1.413"
12
+ }
13
+ },
14
+ "node_modules/fsevents": {
15
+ "version": "2.3.3",
16
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
17
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
18
+ "hasInstallScript": true,
19
+ "license": "MIT",
20
+ "optional": true,
21
+ "os": [
22
+ "darwin"
23
+ ],
24
+ "engines": {
25
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
26
+ }
27
+ },
28
+ "node_modules/pyright": {
29
+ "version": "1.1.413",
30
+ "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.413.tgz",
31
+ "integrity": "sha512-1lpxKrh0DHHpfAQOfciZo2ojua2jase3wwO9at8kldc+F/p1PBscxA5CQ3G1qg5lMOMhXo6ZaiMLMXEAqADIAg==",
32
+ "license": "MIT",
33
+ "bin": {
34
+ "pyright": "index.js",
35
+ "pyright-langserver": "langserver.index.js"
36
+ },
37
+ "engines": {
38
+ "node": ">=14.0.0"
39
+ },
40
+ "optionalDependencies": {
41
+ "fsevents": "~2.3.3"
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "pr-checkmate-profile-pytypes",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "pyright": "1.1.413"
6
+ },
7
+ "private": true
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "2.1.2",
3
+ "version": "3.0.0",
4
4
  "description": "Security-first PR gate for CI: secret scanning, grype dependency scanning, SBOM — plus linting and formatting across 11 languages",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -52,7 +52,7 @@
52
52
  "spellcheck": "cspell '**/*.{ts,tsx,js,jsx,md,json}' --no-progress",
53
53
  "docs": "typedoc",
54
54
  "scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
55
- "test": "node --require ts-node/register --test --test-timeout=60000 --test-concurrency=1 \"tests/**/*.test.ts\"",
55
+ "test": "node --require ts-node/register --test --test-timeout=60000 \"tests/**/*.test.ts\"",
56
56
  "test:ci": "bash scripts/test-per-file.sh",
57
57
  "verify:tarball": "bash scripts/verify-tarball.sh",
58
58
  "security-scan": "npx pr-checkmate security",
@@ -82,6 +82,7 @@
82
82
  "@typescript-eslint/eslint-plugin": "^8.62.1",
83
83
  "@typescript-eslint/parser": "^8.62.1",
84
84
  "@wasm-fmt/clang-format": "^22.1.8",
85
+ "@wasm-fmt/gofmt": "^0.7.3",
85
86
  "cspell": "^10.0.1",
86
87
  "dpdm": "^4.2.0",
87
88
  "eslint": "^10.6.0",
@@ -106,6 +107,7 @@
106
107
  "devDependencies": {
107
108
  "conventional-commits-filter": "^5.0.0",
108
109
  "conventional-commits-parser": "^6.4.0",
110
+ "pyright": "^1.1.413",
109
111
  "standard-version": "^9.5.0",
110
112
  "typedoc": "^0.28.20"
111
113
  },
@@ -727,7 +727,7 @@
727
727
  "default": 1
728
728
  },
729
729
  "reuseLocal": {
730
- "description": "Reuse a tool already in the client's node_modules: \"compatible\" (default), \"always\", or \"off\".",
730
+ "description": "Reuse a tool already present in the repository's node_modules instead of our copy. \"compatible\" (default) reuses when the major matches ours, or when the tool reads the repository's own config (a legacy .eslintrc can only be read by the ESLint that wrote it, and a tsconfig by its own compiler). \"always\" reuses across a major gap at your own risk. \"off\" always uses our pinned copy, for reproducibility.",
731
731
  "type": "string",
732
732
  "enum": ["compatible", "always", "off"],
733
733
  "default": "compatible"
@@ -761,7 +761,7 @@
761
761
  "default": "~/.cache/pr-checkmate"
762
762
  },
763
763
  "prewarm": {
764
- "description": "Warm the cache in postinstall. false leaves everything to the lazy path.",
764
+ "description": "NOT YET IMPLEMENTED — accepted and ignored. Warming the cache needs a postinstall hook, and there is nothing to warm while every profile's tooling ships inside the package; the hook lands with the dependency extraction (ticket 064, release 2). Documented as inert rather than quietly doing nothing.",
765
765
  "type": "boolean",
766
766
  "default": true
767
767
  },
@@ -1174,6 +1174,12 @@
1174
1174
  }
1175
1175
  }
1176
1176
  }
1177
+ },
1178
+ "dataflowWindow": {
1179
+ "type": "integer",
1180
+ "minimum": 0,
1181
+ "default": 10,
1182
+ "description": "Added lines a value stays tainted for, connecting an interpolated string assigned on one line to a query on a later line. 0 disables the cross-line heuristic. Advisory only — it has no scopes and no reassignment tracking, so it never blocks a run."
1177
1183
  }
1178
1184
  }
1179
1185
  },