pr-checkmate 1.21.10 → 1.21.12

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 (57) hide show
  1. package/dist/cli.js +39 -3
  2. package/dist/config/loader.js +12 -0
  3. package/dist/core/checks/circular-deps.js +34 -0
  4. package/dist/core/checks/commitlint.js +83 -0
  5. package/dist/core/checks/cpp-format.js +120 -0
  6. package/dist/core/checks/dependency-check.js +54 -0
  7. package/dist/core/checks/diff-security.js +162 -0
  8. package/dist/core/checks/diff-smell.js +65 -0
  9. package/dist/core/checks/duplicate-check.js +60 -0
  10. package/dist/core/checks/license-check.js +49 -0
  11. package/dist/core/checks/lint.js +62 -0
  12. package/dist/core/checks/markdown-lint.js +115 -0
  13. package/dist/core/checks/npm-audit.js +50 -0
  14. package/dist/core/checks/outdated-deps.js +48 -0
  15. package/dist/core/checks/pr-body.js +49 -0
  16. package/dist/core/checks/pr-size-check.js +42 -0
  17. package/dist/core/checks/pr-title-lint.js +37 -0
  18. package/dist/core/checks/prettier.js +96 -0
  19. package/dist/core/checks/python-format.js +68 -0
  20. package/dist/core/checks/python-lint.js +62 -0
  21. package/dist/core/checks/security.js +50 -0
  22. package/dist/core/checks/sensitive-file-guard.js +103 -0
  23. package/dist/core/checks/spellcheck.js +98 -0
  24. package/dist/core/checks/typecheck.js +48 -0
  25. package/dist/core/context.js +55 -0
  26. package/dist/core/index.js +24 -0
  27. package/dist/core/registry.js +59 -0
  28. package/dist/{scripts/pr-summary.js → core/reporters.js} +34 -31
  29. package/dist/core/run-checks.js +66 -0
  30. package/dist/core/targets.js +40 -0
  31. package/dist/core/types.js +16 -0
  32. package/dist/index.js +10 -98
  33. package/dist/init.js +7 -0
  34. package/dist/utils/detect-language.js +81 -0
  35. package/dist/utils/index.js +1 -0
  36. package/dist/utils/result-collector.js +2 -2
  37. package/package.json +3 -1
  38. package/dist/scripts/circular-deps.js +0 -37
  39. package/dist/scripts/commitlint.js +0 -114
  40. package/dist/scripts/dependency-check.js +0 -62
  41. package/dist/scripts/diff-security.js +0 -199
  42. package/dist/scripts/diff-smell.js +0 -100
  43. package/dist/scripts/duplicate-check.js +0 -68
  44. package/dist/scripts/index.js +0 -187
  45. package/dist/scripts/license-check.js +0 -61
  46. package/dist/scripts/lint.js +0 -61
  47. package/dist/scripts/markdown-lint.js +0 -128
  48. package/dist/scripts/npm-audit.js +0 -65
  49. package/dist/scripts/outdated-deps.js +0 -52
  50. package/dist/scripts/pr-body.js +0 -62
  51. package/dist/scripts/pr-size-check.js +0 -56
  52. package/dist/scripts/pr-title-lint.js +0 -40
  53. package/dist/scripts/prettier-autoformat.js +0 -116
  54. package/dist/scripts/security.js +0 -97
  55. package/dist/scripts/sensitive-file-guard.js +0 -145
  56. package/dist/scripts/spellcheck.js +0 -127
  57. package/dist/scripts/typecheck.js +0 -51
package/dist/cli.js CHANGED
@@ -1,9 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- const scripts_1 = require("./scripts");
4
+ const core_1 = require("./core");
5
5
  const init_1 = require("./init");
6
6
  const utils_1 = require("./utils");
7
+ /** Maps short CLI command names to their registry check names. */
8
+ const COMMAND_TO_CHECK = {
9
+ lint: 'ESLint',
10
+ deps: 'Dependencies',
11
+ dependencies: 'Dependencies',
12
+ 'npm-audit': 'NPM Audit',
13
+ prettier: 'Prettier',
14
+ spellcheck: 'Spellcheck',
15
+ security: 'Security Scan',
16
+ circular: 'Circular Deps',
17
+ license: 'License Check',
18
+ 'pr-size': 'PR Size',
19
+ 'pr-title': 'PR Title',
20
+ outdated: 'Outdated Deps',
21
+ markdown: 'Markdown',
22
+ typecheck: 'TypeScript',
23
+ 'diff-smell': 'Diff Smells',
24
+ 'diff-security': 'Diff Security',
25
+ 'sensitive-files': 'Sensitive Files',
26
+ 'pr-body': 'PR Body',
27
+ duplicate: 'Duplicate Code',
28
+ commitlint: 'Commitlint',
29
+ 'cpp-format': 'C++ Format',
30
+ 'python-lint': 'Ruff Lint',
31
+ 'python-format': 'Ruff Format',
32
+ };
7
33
  const cmd = process.argv[2];
8
34
  void (async () => {
9
35
  try {
@@ -11,8 +37,18 @@ void (async () => {
11
37
  await (0, init_1.init)();
12
38
  process.exit(0);
13
39
  }
14
- await (0, scripts_1.runJob)(cmd ?? 'all');
15
- process.exit(0);
40
+ // Full run: post a GitHub summary comment and allow formatters to write.
41
+ if (!cmd || cmd === 'all') {
42
+ const report = await (0, core_1.runChecks)({ reporter: 'github', write: true });
43
+ process.exit(report.ok ? 0 : 1);
44
+ }
45
+ const checkName = COMMAND_TO_CHECK[cmd];
46
+ if (!checkName) {
47
+ utils_1.logger.warn(`[cli]: Unknown command "${cmd}". Available: all, init, ${Object.keys(COMMAND_TO_CHECK).join(', ')}`);
48
+ process.exit(1);
49
+ }
50
+ const report = await (0, core_1.runChecks)({ only: [checkName], reporter: 'stdout', write: true });
51
+ process.exit(report.ok ? 0 : 1);
16
52
  }
17
53
  catch (err) {
18
54
  utils_1.logger.error(`❌ CLI failed: ${err}`);
@@ -56,6 +56,16 @@ function mergeWithDefaults(userConfig) {
56
56
  },
57
57
  security: userConfig.security || defaults.security,
58
58
  typecheck: { ...defaults.typecheck, ...userConfig.typecheck },
59
+ cpp: {
60
+ ...defaults.cpp,
61
+ ...userConfig.cpp,
62
+ clangFormat: { ...defaults.cpp?.clangFormat, ...userConfig.cpp?.clangFormat },
63
+ },
64
+ python: {
65
+ ...defaults.python,
66
+ ...userConfig.python,
67
+ ruff: { ...defaults.python?.ruff, ...userConfig.python?.ruff },
68
+ },
59
69
  diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
60
70
  prBody: { ...defaults.prBody, ...userConfig.prBody },
61
71
  duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
@@ -127,6 +137,8 @@ function getDefaultConfig() {
127
137
  'npm-audit': { enabled: true, auditLevel: 'moderate' },
128
138
  },
129
139
  typecheck: { enabled: true },
140
+ cpp: { enabled: true, clangFormat: { style: 'LLVM' } },
141
+ python: { enabled: true, ruff: { lint: true, format: true } },
130
142
  diffSmell: { enabled: true },
131
143
  prBody: { enabled: true, minLength: 20, requireTicket: true },
132
144
  duplicate: { enabled: true, minLines: 5, minTokens: 50 },
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.circularDepsCheck = void 0;
7
+ const execa_1 = require("execa");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("../../utils");
10
+ const config_1 = require("../../config");
11
+ const types_1 = require("../types");
12
+ exports.circularDepsCheck = {
13
+ name: 'Circular Deps',
14
+ phase: 'blocking',
15
+ applies(ctx) {
16
+ return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
17
+ },
18
+ async run(ctx) {
19
+ const pattern = node_path_1.default.join(ctx.cwd, (0, config_1.getSourcePath)(), '**/*.{ts,tsx,js,jsx}');
20
+ const result = await (0, execa_1.execa)('npx', ['dpdm', '--no-warning', '--no-tree', '--circular', pattern], {
21
+ cwd: ctx.cwd,
22
+ reject: false,
23
+ });
24
+ const output = (result.stdout + result.stderr).trim();
25
+ // dpdm's success message also contains "circular" — match only its error shapes:
26
+ // the "Circular Dependencies:" header or a numbered entry "[1] src/a.ts -> src/b.ts".
27
+ const hasCircular = /circular dependencies:/i.test(output) || /^\s*\[\d+\]/m.test(output);
28
+ if (hasCircular) {
29
+ utils_1.logger.error(`[Circular Deps]: ❌ circular dependencies detected:\n${output}`);
30
+ return (0, types_1.fail)('circular dependencies found');
31
+ }
32
+ return (0, types_1.pass)();
33
+ },
34
+ };
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.commitlintCheck = void 0;
7
+ const execa_1 = require("execa");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const utils_1 = require("../../utils");
12
+ const config_1 = require("../../config");
13
+ const types_1 = require("../types");
14
+ const LOCAL_CONFIG_FILES = [
15
+ 'commitlint.config.cjs',
16
+ 'commitlint.config.js',
17
+ 'commitlint.config.mjs',
18
+ 'commitlint.config.ts',
19
+ '.commitlintrc',
20
+ '.commitlintrc.json',
21
+ '.commitlintrc.yaml',
22
+ '.commitlintrc.yml',
23
+ '.commitlintrc.js',
24
+ '.commitlintrc.cjs',
25
+ ];
26
+ const DEFAULT_TYPES = [
27
+ 'feat',
28
+ 'fix',
29
+ 'docs',
30
+ 'chore',
31
+ 'ci',
32
+ 'refactor',
33
+ 'perf',
34
+ 'style',
35
+ 'revert',
36
+ 'test',
37
+ 'build',
38
+ ];
39
+ function buildTempConfig(types) {
40
+ const config = {
41
+ // Resolve via Node module resolution — robust to npm hoisting in the client.
42
+ extends: [require.resolve('@commitlint/config-conventional')],
43
+ rules: { 'type-enum': [2, 'always', types] },
44
+ };
45
+ const tmpFile = node_path_1.default.join(node_os_1.default.tmpdir(), `.commitlint-${process.pid}.json`);
46
+ node_fs_1.default.writeFileSync(tmpFile, JSON.stringify(config, null, 2));
47
+ return tmpFile;
48
+ }
49
+ exports.commitlintCheck = {
50
+ name: 'Commitlint',
51
+ phase: 'informational',
52
+ applies(ctx) {
53
+ return ctx.config.commitlint?.enabled !== false;
54
+ },
55
+ async run(ctx) {
56
+ const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
57
+ const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
58
+ let tempConfigFile = null;
59
+ const from = ctx.baseSha ?? 'HEAD~1';
60
+ const to = ctx.headSha ?? 'HEAD';
61
+ const args = ['--from', from, '--to', to, '--verbose'];
62
+ try {
63
+ if (hasLocal) {
64
+ utils_1.logger.info('[Commitlint]: Using local config');
65
+ }
66
+ else {
67
+ const types = ctx.config.commitlint?.types ?? DEFAULT_TYPES;
68
+ tempConfigFile = buildTempConfig(types);
69
+ args.push('--config', tempConfigFile);
70
+ }
71
+ const result = await (0, execa_1.execa)(process.execPath, [commitlintBin, ...args], {
72
+ cwd: ctx.cwd,
73
+ reject: false,
74
+ stdio: 'inherit',
75
+ });
76
+ return result.exitCode === 0 ? (0, types_1.pass)() : (0, types_1.warn)('commit message violations found');
77
+ }
78
+ finally {
79
+ if (tempConfigFile && node_fs_1.default.existsSync(tempConfigFile))
80
+ node_fs_1.default.unlinkSync(tempConfigFile);
81
+ }
82
+ },
83
+ };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cppFormatCheck = void 0;
7
+ const execa_1 = require("execa");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const utils_1 = require("../../utils");
11
+ const types_1 = require("../types");
12
+ /**
13
+ * C/C++ source & header extensions clang-format understands. `.h` is intentionally
14
+ * included even though it is ambiguous with C — clang-format handles both.
15
+ */
16
+ const CPP_GLOBS = ['*.c', '*.cc', '*.cpp', '*.cxx', '*.c++', '*.h', '*.hh', '*.hpp', '*.hxx'];
17
+ const DEFAULT_STYLE = 'LLVM';
18
+ /**
19
+ * Resolves the clang-format style argument. Prefers the project's own `.clang-format`
20
+ * (via `--style=file`) so pr-checkmate never overrides a repo's established rules;
21
+ * falls back to the configured style, then LLVM.
22
+ */
23
+ function resolveStyleArg(ctx) {
24
+ if (node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, '.clang-format'))) {
25
+ return '--style=file';
26
+ }
27
+ const style = ctx.config.cpp?.clangFormat?.style ?? DEFAULT_STYLE;
28
+ return `--style=${style}`;
29
+ }
30
+ /**
31
+ * Returns the C/C++ files to check. Uses the PR diff range when available
32
+ * (delta mode), else scans all tracked C/C++ files. Returns an empty array
33
+ * when there is nothing to check, `null` when git is unusable.
34
+ */
35
+ async function resolveTargets(ctx) {
36
+ try {
37
+ if (ctx.baseSha) {
38
+ const { stdout } = await (0, execa_1.execa)('git', [
39
+ 'diff',
40
+ '--name-only',
41
+ '--diff-filter=ACMR',
42
+ ctx.baseSha,
43
+ ctx.headSha ?? 'HEAD',
44
+ '--',
45
+ ...CPP_GLOBS,
46
+ ], { cwd: ctx.cwd, reject: false });
47
+ const files = stdout
48
+ .split('\n')
49
+ .map(f => f.trim())
50
+ .filter(Boolean);
51
+ utils_1.logger.info(`[C++ Format]: 📂 Delta mode: ${files.length} changed C/C++ file(s)`);
52
+ return files;
53
+ }
54
+ const { stdout } = await (0, execa_1.execa)('git', ['ls-files', ...CPP_GLOBS], {
55
+ cwd: ctx.cwd,
56
+ reject: false,
57
+ });
58
+ const files = stdout
59
+ .split('\n')
60
+ .map(f => f.trim())
61
+ .filter(Boolean);
62
+ utils_1.logger.info(`[C++ Format]: Full scan: ${files.length} tracked C/C++ file(s)`);
63
+ return files;
64
+ }
65
+ catch (err) {
66
+ utils_1.logger.warn(`[C++ Format]: ⚠️ Could not list C/C++ files\n${err}`);
67
+ return null;
68
+ }
69
+ }
70
+ /** Counts distinct files flagged in clang-format's `--dry-run --Werror` diagnostics. */
71
+ function countOffendingFiles(stderr) {
72
+ const files = new Set();
73
+ for (const line of stderr.split('\n')) {
74
+ const match = line.match(/^(.+?):\d+:\d+: (?:error|warning):/);
75
+ if (match)
76
+ files.add(match[1]);
77
+ }
78
+ return files.size;
79
+ }
80
+ /**
81
+ * Formats C/C++ code with clang-format, bundled as a self-contained WASM CLI
82
+ * (`@wasm-fmt/clang-format`) so it runs on a bare Node image with no system
83
+ * clang install. Read-only in check mode; rewrites files in place when `write`.
84
+ */
85
+ exports.cppFormatCheck = {
86
+ name: 'C++ Format',
87
+ phase: 'format',
88
+ applies(ctx) {
89
+ return ctx.config.cpp?.enabled !== false && ctx.languages.includes('cpp');
90
+ },
91
+ async run(ctx) {
92
+ const targets = await resolveTargets(ctx);
93
+ if (targets === null)
94
+ return (0, types_1.skip)('git unavailable');
95
+ if (targets.length === 0)
96
+ return (0, types_1.pass)('no C/C++ files changed');
97
+ const cfBin = (0, utils_1.resolvePackageBin)('@wasm-fmt/clang-format', 'clang-format-cli.cjs');
98
+ const styleArg = resolveStyleArg(ctx);
99
+ // Detect unformatted files without mutating anything.
100
+ const dry = await (0, execa_1.execa)(process.execPath, [cfBin, '--dry-run', '--Werror', styleArg, ...targets], { cwd: ctx.cwd, reject: false });
101
+ if (dry.exitCode === 0)
102
+ return (0, types_1.pass)();
103
+ const offending = countOffendingFiles(dry.stderr) || targets.length;
104
+ if (!ctx.write) {
105
+ utils_1.logger.warn(`[C++ Format]: ⚠️ ${offending} C/C++ file(s) need formatting`);
106
+ return (0, types_1.warn)(`${offending} file(s) need formatting (run with write to fix)`);
107
+ }
108
+ // write mode: rewrite the offending files in place so CI can auto-commit them.
109
+ const fix = await (0, execa_1.execa)(process.execPath, [cfBin, '-i', styleArg, ...targets], {
110
+ cwd: ctx.cwd,
111
+ reject: false,
112
+ });
113
+ if (fix.exitCode !== 0) {
114
+ utils_1.logger.warn(`[C++ Format]: ⚠️ clang-format -i failed\n${fix.stderr}`);
115
+ return (0, types_1.warn)('clang-format failed to rewrite files');
116
+ }
117
+ utils_1.logger.info(`[C++ Format]: ✅ Reformatted ${offending} C/C++ file(s)`);
118
+ return (0, types_1.pass)(`reformatted ${offending} file(s)`);
119
+ },
120
+ };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dependencyCheck = void 0;
7
+ const execa_1 = require("execa");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const utils_1 = require("../../utils");
11
+ const types_1 = require("../types");
12
+ exports.dependencyCheck = {
13
+ name: 'Dependencies',
14
+ phase: 'blocking',
15
+ applies(ctx) {
16
+ return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
17
+ },
18
+ async run(ctx) {
19
+ let stdout;
20
+ try {
21
+ ({ stdout } = await (0, execa_1.execa)('npx', ['depcheck', '--json'], { cwd: ctx.cwd }));
22
+ }
23
+ catch (err) {
24
+ // depcheck exits non-zero when it finds issues but still emits JSON on stdout.
25
+ if (err instanceof execa_1.ExecaError && err.stdout) {
26
+ stdout = err.stdout;
27
+ }
28
+ else {
29
+ throw err;
30
+ }
31
+ }
32
+ const start = stdout.indexOf('{');
33
+ const end = stdout.lastIndexOf('}');
34
+ const json = start !== -1 && end !== -1 ? stdout.slice(start, end + 1) : stdout;
35
+ let result;
36
+ try {
37
+ result = JSON.parse(json);
38
+ }
39
+ catch {
40
+ return (0, types_1.fail)('depcheck produced invalid JSON');
41
+ }
42
+ const unused = result.dependencies ?? [];
43
+ const missing = Object.keys(result.missing ?? {});
44
+ if (missing.length > 0) {
45
+ missing.forEach(dep => utils_1.logger.error(`[Dependencies]: ❌ missing ${dep} (used in: ${result.missing[dep]?.join(', ')})`));
46
+ return (0, types_1.fail)(`missing: ${missing.join(', ')}`);
47
+ }
48
+ if (unused.length > 0) {
49
+ unused.forEach(dep => utils_1.logger.warn(`[Dependencies]: ⚠️ unused ${dep}`));
50
+ return (0, types_1.warn)(`${unused.length} unused dependency(ies)`);
51
+ }
52
+ return (0, types_1.pass)();
53
+ },
54
+ };
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffSecurityCheck = void 0;
4
+ const execa_1 = require("execa");
5
+ const utils_1 = require("../../utils");
6
+ const types_1 = require("../types");
7
+ const SECURITY_PATTERNS = [
8
+ {
9
+ pattern: /^\+(?!\+).*dangerouslySetInnerHTML\s*=/,
10
+ label: 'dangerouslySetInnerHTML (XSS risk)',
11
+ severity: 'error',
12
+ },
13
+ {
14
+ pattern: /^\+(?!\+).*\.innerHTML\s*=/,
15
+ label: 'innerHTML assignment (XSS risk)',
16
+ severity: 'warn',
17
+ },
18
+ {
19
+ pattern: /^\+(?!\+).*document\.write\s*\(/,
20
+ label: 'document.write() (XSS risk)',
21
+ severity: 'error',
22
+ },
23
+ {
24
+ pattern: /^\+(?!\+)(?![^'"`]*['"`]).*\beval\s*\(/,
25
+ label: 'eval() (code injection risk)',
26
+ severity: 'error',
27
+ },
28
+ {
29
+ pattern: /^\+(?!\+).*\bnew\s+Function\s*\(/,
30
+ label: 'new Function() (code injection risk)',
31
+ severity: 'error',
32
+ },
33
+ {
34
+ pattern: /^\+(?!\+).*__proto__\s*[=:[]/,
35
+ label: '__proto__ mutation (prototype pollution)',
36
+ severity: 'error',
37
+ },
38
+ {
39
+ pattern: /^\+(?!\+).*\.constructor\.prototype\b/,
40
+ label: 'constructor.prototype access (prototype pollution)',
41
+ severity: 'warn',
42
+ },
43
+ {
44
+ pattern: /^\+(?!\+).*createHash\s*\(\s*['"]md5['"]/,
45
+ label: 'MD5 (weak hash — use sha256 or stronger)',
46
+ severity: 'warn',
47
+ },
48
+ {
49
+ pattern: /^\+(?!\+).*createHash\s*\(\s*['"]sha1['"]/,
50
+ label: 'SHA-1 (weak hash — use sha256 or stronger)',
51
+ severity: 'warn',
52
+ },
53
+ {
54
+ pattern: /^\+(?!\+).*\bcreateDecipher\s*\(/,
55
+ label: 'deprecated createDecipher() — use createDecipheriv()',
56
+ severity: 'warn',
57
+ },
58
+ {
59
+ pattern: /^\+(?!\+).*\bcreateCipher\s*\(/,
60
+ label: 'deprecated createCipher() — use createCipheriv()',
61
+ severity: 'warn',
62
+ },
63
+ {
64
+ pattern: /^\+(?!\+).*cors\s*\(\s*\{[^}]*origin\s*:\s*['"]\*['"]/,
65
+ label: "CORS wildcard origin: '*' (allows any domain)",
66
+ severity: 'warn',
67
+ },
68
+ {
69
+ pattern: /^\+(?!\+).*['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/,
70
+ label: "Access-Control-Allow-Origin: '*' header (CORS wildcard)",
71
+ severity: 'warn',
72
+ },
73
+ {
74
+ pattern: /^\+(?!\+).*(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|http\.get|http\.request)\s*\(\s*['"]http:\/\//,
75
+ label: 'Insecure HTTP (non-HTTPS) in external call',
76
+ severity: 'warn',
77
+ },
78
+ {
79
+ pattern: /^\+(?!\+).*(exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
80
+ label: 'exec/spawn with template literal (command injection risk)',
81
+ severity: 'error',
82
+ },
83
+ {
84
+ pattern: /^\+(?!\+).*(exec|execSync)\s*\(\s*['"][^'"]*['"\s]*\+/,
85
+ label: 'exec/spawn with string concatenation (command injection risk)',
86
+ severity: 'error',
87
+ },
88
+ {
89
+ pattern: /^\+(?!\+).*\b(query|execute|raw)\s*\(\s*`[^`]*\$\{/,
90
+ label: 'SQL query with template literal interpolation (SQL injection risk)',
91
+ severity: 'error',
92
+ },
93
+ {
94
+ // A quoted string that contains a whole SQL keyword, concatenated with a
95
+ // variable — the real injection shape. Word boundaries + the quote context
96
+ // stop identifiers like `insertions + deletions` from false-positiving.
97
+ pattern: /^\+(?!\+).*['"`][^'"`]*\b(SELECT|INSERT|UPDATE|DELETE|WHERE)\b[^'"`]*['"`]\s*\+\s*\w/i,
98
+ label: 'SQL string concatenation (SQL injection risk)',
99
+ severity: 'error',
100
+ },
101
+ ];
102
+ exports.diffSecurityCheck = {
103
+ name: 'Diff Security',
104
+ phase: 'informational',
105
+ applies(ctx) {
106
+ return ctx.config.diffSecurity?.enabled !== false && Boolean(ctx.baseSha);
107
+ },
108
+ async run(ctx) {
109
+ try {
110
+ const { stdout } = await (0, execa_1.execa)('git', [
111
+ 'diff',
112
+ '--unified=0',
113
+ '--diff-filter=ACMR',
114
+ ctx.baseSha,
115
+ 'HEAD',
116
+ '--',
117
+ '*.ts',
118
+ '*.tsx',
119
+ '*.js',
120
+ '*.jsx',
121
+ '*.mjs',
122
+ '*.cjs',
123
+ ], { cwd: ctx.cwd });
124
+ const errors = {};
125
+ const warnings = {};
126
+ for (const line of stdout.split('\n')) {
127
+ for (const { pattern, label, severity } of SECURITY_PATTERNS) {
128
+ if (pattern.test(line)) {
129
+ const bucket = severity === 'error' ? errors : warnings;
130
+ (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
131
+ }
132
+ }
133
+ }
134
+ const totalErrors = Object.values(errors).reduce((s, a) => s + a.length, 0);
135
+ const totalWarnings = Object.values(warnings).reduce((s, a) => s + a.length, 0);
136
+ if (totalErrors === 0 && totalWarnings === 0)
137
+ return (0, types_1.pass)();
138
+ for (const [label, occurrences] of Object.entries(errors)) {
139
+ utils_1.logger.error(`[Diff Security]: ❌ ${label}: ${occurrences.length} occurrence(s)`);
140
+ occurrences.slice(0, 3).forEach(l => utils_1.logger.error(` ${l}`));
141
+ if (occurrences.length > 3)
142
+ utils_1.logger.error(` ... and ${occurrences.length - 3} more`);
143
+ }
144
+ for (const [label, occurrences] of Object.entries(warnings)) {
145
+ utils_1.logger.warn(`[Diff Security]: ⚠️ ${label}: ${occurrences.length} occurrence(s)`);
146
+ occurrences.slice(0, 3).forEach(l => utils_1.logger.warn(` ${l}`));
147
+ if (occurrences.length > 3)
148
+ utils_1.logger.warn(` ... and ${occurrences.length - 3} more`);
149
+ }
150
+ const parts = [];
151
+ if (totalErrors > 0)
152
+ parts.push(`${totalErrors} issue(s)`);
153
+ if (totalWarnings > 0)
154
+ parts.push(`${totalWarnings} warning(s)`);
155
+ return (0, types_1.warn)(parts.join(', '));
156
+ }
157
+ catch (err) {
158
+ utils_1.logger.warn(`[Diff Security]: ⚠️ Could not analyze diff\n${err}`);
159
+ return (0, types_1.skip)('diff unavailable');
160
+ }
161
+ },
162
+ };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffSmellCheck = void 0;
4
+ const execa_1 = require("execa");
5
+ const utils_1 = require("../../utils");
6
+ const types_1 = require("../types");
7
+ const SMELL_PATTERNS = [
8
+ {
9
+ pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/,
10
+ label: 'console.*()',
11
+ },
12
+ { pattern: /^\+(?!\+).*\bdebugger\b/, label: 'debugger statement' },
13
+ { pattern: /^\+\s*\/\/\s*TODO\b/i, label: 'TODO comment' },
14
+ { pattern: /^\+\s*\/\/\s*FIXME\b/i, label: 'FIXME comment' },
15
+ { pattern: /^\+\s*\/\/\s*HACK\b/i, label: 'HACK comment' },
16
+ { pattern: /^\+\s*\/\/\s*XXX\b/i, label: 'XXX marker' },
17
+ ];
18
+ exports.diffSmellCheck = {
19
+ name: 'Diff Smells',
20
+ phase: 'informational',
21
+ applies(ctx) {
22
+ return ctx.config.diffSmell?.enabled !== false && Boolean(ctx.baseSha);
23
+ },
24
+ async run(ctx) {
25
+ try {
26
+ const { stdout } = await (0, execa_1.execa)('git', [
27
+ 'diff',
28
+ '--unified=0',
29
+ '--diff-filter=ACMR',
30
+ ctx.baseSha,
31
+ 'HEAD',
32
+ '--',
33
+ '*.ts',
34
+ '*.tsx',
35
+ '*.js',
36
+ '*.jsx',
37
+ ], { cwd: ctx.cwd });
38
+ const findings = {};
39
+ for (const line of stdout.split('\n')) {
40
+ for (const { pattern, label } of SMELL_PATTERNS) {
41
+ if (pattern.test(line)) {
42
+ (findings[label] ?? (findings[label] = [])).push(line.slice(1).trim().substring(0, 100));
43
+ }
44
+ }
45
+ }
46
+ const total = Object.values(findings).reduce((sum, arr) => sum + arr.length, 0);
47
+ if (total === 0)
48
+ return (0, types_1.pass)();
49
+ for (const [label, occurrences] of Object.entries(findings)) {
50
+ utils_1.logger.warn(`[Diff Smells]: ⚠️ ${label}: ${occurrences.length} occurrence(s)`);
51
+ occurrences.slice(0, 3).forEach(l => utils_1.logger.warn(` ${l}`));
52
+ if (occurrences.length > 3)
53
+ utils_1.logger.warn(` ... and ${occurrences.length - 3} more`);
54
+ }
55
+ const summary = Object.entries(findings)
56
+ .map(([label, arr]) => `${arr.length}× ${label}`)
57
+ .join(', ');
58
+ return (0, types_1.warn)(summary);
59
+ }
60
+ catch (err) {
61
+ utils_1.logger.warn(`[Diff Smells]: ⚠️ Could not analyze diff\n${err}`);
62
+ return (0, types_1.skip)('diff unavailable');
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.duplicateCheck = void 0;
7
+ const execa_1 = require("execa");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const utils_1 = require("../../utils");
11
+ const delta_1 = require("../../utils/delta");
12
+ const types_1 = require("../types");
13
+ exports.duplicateCheck = {
14
+ name: 'Duplicate Code',
15
+ phase: 'informational',
16
+ applies(ctx) {
17
+ if (ctx.config.duplicate?.enabled === false)
18
+ return false;
19
+ return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
20
+ },
21
+ async run(ctx) {
22
+ const minLines = ctx.config.duplicate?.minLines ?? 5;
23
+ const minTokens = ctx.config.duplicate?.minTokens ?? 50;
24
+ const changedFiles = await (0, delta_1.getChangedFiles)(['.ts', '.tsx', '.js', '.jsx']);
25
+ if (changedFiles !== null && changedFiles.length === 0)
26
+ return (0, types_1.skip)('no relevant files changed');
27
+ const srcPath = node_path_1.default.join(ctx.cwd, 'src');
28
+ const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [ctx.cwd]);
29
+ const jscpdBin = (0, utils_1.resolvePackageBin)('jscpd', 'bin/jscpd');
30
+ try {
31
+ const result = await (0, execa_1.execa)(process.execPath, [
32
+ jscpdBin,
33
+ '--min-lines',
34
+ String(minLines),
35
+ '--min-tokens',
36
+ String(minTokens),
37
+ '--reporters',
38
+ 'console',
39
+ '--ignore',
40
+ 'node_modules/**,dist/**,.git/**',
41
+ '--quiet',
42
+ ...targetPaths,
43
+ ], { cwd: ctx.cwd, reject: false });
44
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
45
+ const clonesMatch = output.match(/(\d+)\s+clone/i);
46
+ const cloneCount = clonesMatch ? parseInt(clonesMatch[1], 10) : 0;
47
+ if (cloneCount > 0) {
48
+ utils_1.logger.warn(`[Duplicate Code]: ⚠️ Found ${cloneCount} duplicate block(s)`);
49
+ if (output)
50
+ utils_1.logger.warn(output);
51
+ return (0, types_1.warn)(`${cloneCount} duplicate block(s)`);
52
+ }
53
+ return (0, types_1.pass)();
54
+ }
55
+ catch (err) {
56
+ utils_1.logger.warn(`[Duplicate Code]: ⚠️ Could not run duplicate check\n${err}`);
57
+ return (0, types_1.skip)('jscpd unavailable');
58
+ }
59
+ },
60
+ };