pr-checkmate 1.21.10 → 1.21.11
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/dist/cli.js +37 -3
- package/dist/config/loader.js +6 -0
- package/dist/core/checks/circular-deps.js +34 -0
- package/dist/core/checks/commitlint.js +83 -0
- package/dist/core/checks/cpp-format.js +120 -0
- package/dist/core/checks/dependency-check.js +54 -0
- package/dist/core/checks/diff-security.js +162 -0
- package/dist/core/checks/diff-smell.js +65 -0
- package/dist/core/checks/duplicate-check.js +60 -0
- package/dist/core/checks/license-check.js +49 -0
- package/dist/core/checks/lint.js +62 -0
- package/dist/core/checks/markdown-lint.js +115 -0
- package/dist/core/checks/npm-audit.js +50 -0
- package/dist/core/checks/outdated-deps.js +48 -0
- package/dist/core/checks/pr-body.js +49 -0
- package/dist/core/checks/pr-size-check.js +42 -0
- package/dist/core/checks/pr-title-lint.js +37 -0
- package/dist/core/checks/prettier.js +96 -0
- package/dist/core/checks/security.js +50 -0
- package/dist/core/checks/sensitive-file-guard.js +103 -0
- package/dist/core/checks/spellcheck.js +98 -0
- package/dist/core/checks/typecheck.js +48 -0
- package/dist/core/context.js +55 -0
- package/dist/core/index.js +24 -0
- package/dist/core/registry.js +55 -0
- package/dist/{scripts/pr-summary.js → core/reporters.js} +34 -31
- package/dist/core/run-checks.js +66 -0
- package/dist/core/types.js +16 -0
- package/dist/index.js +10 -98
- package/dist/init.js +7 -0
- package/dist/utils/detect-language.js +81 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/result-collector.js +2 -2
- package/package.json +2 -1
- package/dist/scripts/circular-deps.js +0 -37
- package/dist/scripts/commitlint.js +0 -114
- package/dist/scripts/dependency-check.js +0 -62
- package/dist/scripts/diff-security.js +0 -199
- package/dist/scripts/diff-smell.js +0 -100
- package/dist/scripts/duplicate-check.js +0 -68
- package/dist/scripts/index.js +0 -187
- package/dist/scripts/license-check.js +0 -61
- package/dist/scripts/lint.js +0 -61
- package/dist/scripts/markdown-lint.js +0 -128
- package/dist/scripts/npm-audit.js +0 -65
- package/dist/scripts/outdated-deps.js +0 -52
- package/dist/scripts/pr-body.js +0 -62
- package/dist/scripts/pr-size-check.js +0 -56
- package/dist/scripts/pr-title-lint.js +0 -40
- package/dist/scripts/prettier-autoformat.js +0 -116
- package/dist/scripts/security.js +0 -97
- package/dist/scripts/sensitive-file-guard.js +0 -145
- package/dist/scripts/spellcheck.js +0 -127
- package/dist/scripts/typecheck.js +0 -51
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const
|
|
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
|
+
};
|
|
7
31
|
const cmd = process.argv[2];
|
|
8
32
|
void (async () => {
|
|
9
33
|
try {
|
|
@@ -11,8 +35,18 @@ void (async () => {
|
|
|
11
35
|
await (0, init_1.init)();
|
|
12
36
|
process.exit(0);
|
|
13
37
|
}
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
// Full run: post a GitHub summary comment and allow formatters to write.
|
|
39
|
+
if (!cmd || cmd === 'all') {
|
|
40
|
+
const report = await (0, core_1.runChecks)({ reporter: 'github', write: true });
|
|
41
|
+
process.exit(report.ok ? 0 : 1);
|
|
42
|
+
}
|
|
43
|
+
const checkName = COMMAND_TO_CHECK[cmd];
|
|
44
|
+
if (!checkName) {
|
|
45
|
+
utils_1.logger.warn(`[cli]: Unknown command "${cmd}". Available: all, init, ${Object.keys(COMMAND_TO_CHECK).join(', ')}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const report = await (0, core_1.runChecks)({ only: [checkName], reporter: 'stdout', write: true });
|
|
49
|
+
process.exit(report.ok ? 0 : 1);
|
|
16
50
|
}
|
|
17
51
|
catch (err) {
|
|
18
52
|
utils_1.logger.error(`❌ CLI failed: ${err}`);
|
package/dist/config/loader.js
CHANGED
|
@@ -56,6 +56,11 @@ 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
|
+
},
|
|
59
64
|
diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
|
|
60
65
|
prBody: { ...defaults.prBody, ...userConfig.prBody },
|
|
61
66
|
duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
|
|
@@ -127,6 +132,7 @@ function getDefaultConfig() {
|
|
|
127
132
|
'npm-audit': { enabled: true, auditLevel: 'moderate' },
|
|
128
133
|
},
|
|
129
134
|
typecheck: { enabled: true },
|
|
135
|
+
cpp: { enabled: true, clangFormat: { style: 'LLVM' } },
|
|
130
136
|
diffSmell: { enabled: true },
|
|
131
137
|
prBody: { enabled: true, minLength: 20, requireTicket: true },
|
|
132
138
|
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
|
+
};
|