ccbot-cli 2.0.1 → 2.1.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/LICENSE +21 -0
- package/bin/adapters/claude.js +150 -0
- package/bin/adapters/codex.js +439 -0
- package/bin/install.js +509 -349
- package/bin/lib/ccline.js +82 -0
- package/bin/lib/utils.js +87 -34
- package/bin/uninstall.js +48 -0
- package/config/AGENTS.md +630 -0
- package/config/CLAUDE.md +229 -20
- package/config/ccline/config.toml +161 -0
- package/config/codex-config.example.toml +22 -0
- package/config/settings.example.json +32 -0
- package/output-styles/abyss-cultivator.md +399 -0
- package/package.json +14 -5
- package/skills/SKILL.md +159 -0
- package/skills/domains/ai/SKILL.md +34 -0
- package/skills/domains/ai/agent-dev.md +242 -0
- package/skills/domains/ai/llm-security.md +288 -0
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/ai/rag-system.md +542 -0
- package/skills/domains/architecture/SKILL.md +42 -0
- package/skills/domains/architecture/api-design.md +225 -0
- package/skills/domains/architecture/caching.md +299 -0
- package/skills/domains/architecture/cloud-native.md +285 -0
- package/skills/domains/architecture/message-queue.md +329 -0
- package/skills/domains/architecture/security-arch.md +297 -0
- package/skills/domains/data-engineering/SKILL.md +207 -0
- package/skills/domains/development/SKILL.md +46 -0
- package/skills/domains/development/cpp.md +246 -0
- package/skills/domains/development/go.md +323 -0
- package/skills/domains/development/java.md +277 -0
- package/skills/domains/development/python.md +288 -0
- package/skills/domains/development/rust.md +313 -0
- package/skills/domains/development/shell.md +313 -0
- package/skills/domains/development/typescript.md +277 -0
- package/skills/domains/devops/SKILL.md +39 -0
- package/skills/domains/devops/cost-optimization.md +272 -0
- package/skills/domains/devops/database.md +217 -0
- package/skills/domains/devops/devsecops.md +198 -0
- package/skills/domains/devops/git-workflow.md +181 -0
- package/skills/domains/devops/observability.md +280 -0
- package/skills/domains/devops/performance.md +336 -0
- package/skills/domains/devops/testing.md +283 -0
- package/skills/domains/frontend-design/SKILL.md +38 -0
- package/skills/domains/frontend-design/claymorphism/SKILL.md +119 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/component-patterns.md +202 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +140 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +137 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +143 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/frontend-design/state-management.md +680 -0
- package/skills/domains/frontend-design/ui-aesthetics.md +110 -0
- package/skills/domains/frontend-design/ux-principles.md +156 -0
- package/skills/domains/infrastructure/SKILL.md +200 -0
- package/skills/domains/mobile/SKILL.md +224 -0
- package/skills/domains/orchestration/SKILL.md +29 -0
- package/skills/domains/orchestration/multi-agent.md +263 -0
- package/skills/domains/security/SKILL.md +54 -0
- package/skills/domains/security/blue-team.md +436 -0
- package/skills/domains/security/code-audit.md +265 -0
- package/skills/domains/security/pentest.md +226 -0
- package/skills/domains/security/red-team.md +375 -0
- package/skills/domains/security/threat-intel.md +372 -0
- package/skills/domains/security/vuln-research.md +369 -0
- package/skills/orchestration/multi-agent/SKILL.md +493 -0
- package/skills/run_skill.js +129 -0
- package/skills/tools/gen-docs/SKILL.md +116 -0
- package/skills/tools/gen-docs/scripts/doc_generator.js +435 -0
- package/skills/tools/lib/shared.js +98 -0
- package/skills/tools/verify-change/SKILL.md +140 -0
- package/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
- package/skills/tools/verify-module/SKILL.md +127 -0
- package/skills/tools/verify-module/scripts/module_scanner.js +171 -0
- package/skills/tools/verify-quality/SKILL.md +160 -0
- package/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
- package/skills/tools/verify-security/SKILL.md +143 -0
- package/skills/tools/verify-security/scripts/security_scanner.js +283 -0
- package/bin/lib/registry.js +0 -61
- package/config/.claudeignore +0 -11
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
7
|
+
|
|
8
|
+
const REQUIRED_FILES = { 'README.md': '模块说明文档', 'DESIGN.md': '设计决策文档' };
|
|
9
|
+
const ALT_SRC_DIRS = ['src', 'lib', 'pkg', 'internal', 'cmd', 'app'];
|
|
10
|
+
const ALT_TEST_DIRS = ['tests', 'test', '__tests__', 'spec'];
|
|
11
|
+
const ROOT_SCRIPT_FILES = new Set([
|
|
12
|
+
'install.sh', 'uninstall.sh', 'install.ps1',
|
|
13
|
+
'uninstall.ps1', 'Dockerfile', 'Makefile'
|
|
14
|
+
]);
|
|
15
|
+
const CODE_EXTS = new Set(['.py', '.go', '.rs', '.ts', '.js', '.java', '.sh', '.ps1']);
|
|
16
|
+
const TEST_PATTERNS = ['test_', '_test.', '.test.', 'spec_', '_spec.'];
|
|
17
|
+
|
|
18
|
+
function scanStructure(p, depth = 3) {
|
|
19
|
+
const s = { name: path.basename(p), type: 'dir', children: [] };
|
|
20
|
+
if (depth <= 0) return s;
|
|
21
|
+
try {
|
|
22
|
+
for (const name of fs.readdirSync(p).sort()) {
|
|
23
|
+
if (name.startsWith('.')) continue;
|
|
24
|
+
const full = path.join(p, name);
|
|
25
|
+
const stat = fs.statSync(full);
|
|
26
|
+
if (stat.isFile()) s.children.push({ name, type: 'file', size: stat.size });
|
|
27
|
+
else if (stat.isDirectory()) s.children.push(scanStructure(full, depth - 1));
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rglob(dir, test) {
|
|
34
|
+
try {
|
|
35
|
+
for (const name of fs.readdirSync(dir)) {
|
|
36
|
+
const full = path.join(dir, name);
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(full);
|
|
39
|
+
if (stat.isFile() && test(name)) return true;
|
|
40
|
+
if (stat.isDirectory()) { if (rglob(full, test)) return true; }
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scanModule(target) {
|
|
48
|
+
const modulePath = path.resolve(target);
|
|
49
|
+
const issues = [];
|
|
50
|
+
const add = (severity, message, p) => issues.push({ severity, message, path: p || null });
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(modulePath)) {
|
|
53
|
+
add('error', `路径不存在: ${modulePath}`);
|
|
54
|
+
return { modulePath, issues, structure: {} };
|
|
55
|
+
}
|
|
56
|
+
if (!fs.statSync(modulePath).isDirectory()) {
|
|
57
|
+
add('error', `不是目录: ${modulePath}`);
|
|
58
|
+
return { modulePath, issues, structure: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const structure = scanStructure(modulePath);
|
|
62
|
+
|
|
63
|
+
// required files
|
|
64
|
+
for (const [file, desc] of Object.entries(REQUIRED_FILES)) {
|
|
65
|
+
const fp = path.join(modulePath, file);
|
|
66
|
+
if (!fs.existsSync(fp)) add('error', `缺少必需文档: ${file} (${desc})`, fp);
|
|
67
|
+
else if (fs.statSync(fp).size < 50) add('warning', `文档内容过少: ${file} (< 50 bytes)`, fp);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// source dirs
|
|
71
|
+
let srcFound = ALT_SRC_DIRS.some(d => {
|
|
72
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
73
|
+
catch { return false; }
|
|
74
|
+
});
|
|
75
|
+
const entries = fs.readdirSync(modulePath);
|
|
76
|
+
const rootCode = entries.filter(n => {
|
|
77
|
+
try {
|
|
78
|
+
const s = fs.statSync(path.join(modulePath, n));
|
|
79
|
+
return s.isFile() && CODE_EXTS.has(path.extname(n));
|
|
80
|
+
} catch { return false; }
|
|
81
|
+
});
|
|
82
|
+
const rootScript = entries.filter(n => {
|
|
83
|
+
try {
|
|
84
|
+
return fs.statSync(path.join(modulePath, n)).isFile()
|
|
85
|
+
&& ROOT_SCRIPT_FILES.has(n);
|
|
86
|
+
} catch { return false; }
|
|
87
|
+
});
|
|
88
|
+
if (rootCode.length || rootScript.length) {
|
|
89
|
+
srcFound = true;
|
|
90
|
+
if (rootCode.length > 5) {
|
|
91
|
+
add('warning', `根目录代码文件过多 (${rootCode.length}个),建议整理到 src/ 目录`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!srcFound) add('warning', '未找到源码目录或代码文件');
|
|
95
|
+
|
|
96
|
+
// test dirs
|
|
97
|
+
let testFound = ALT_TEST_DIRS.some(d => {
|
|
98
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
99
|
+
catch { return false; }
|
|
100
|
+
});
|
|
101
|
+
if (!testFound) testFound = rglob(modulePath, n => TEST_PATTERNS.some(p => n.includes(p)));
|
|
102
|
+
if (!testFound) add('warning', '未找到测试目录或测试文件');
|
|
103
|
+
|
|
104
|
+
// doc quality
|
|
105
|
+
const readme = path.join(modulePath, 'README.md');
|
|
106
|
+
if (fs.existsSync(readme)) {
|
|
107
|
+
const c = fs.readFileSync(readme, 'utf-8');
|
|
108
|
+
if (!c.includes('#')) add('warning', 'README.md 缺少标题', readme);
|
|
109
|
+
const docKeys = ['usage', 'install', '使用', '安装', 'example', '示例'];
|
|
110
|
+
if (!docKeys.some(k => c.toLowerCase().includes(k)))
|
|
111
|
+
add('info', 'README.md 建议添加使用说明或示例', readme);
|
|
112
|
+
}
|
|
113
|
+
const design = path.join(modulePath, 'DESIGN.md');
|
|
114
|
+
if (fs.existsSync(design)) {
|
|
115
|
+
const c = fs.readFileSync(design, 'utf-8');
|
|
116
|
+
const designKeys = ['决策', 'decision', '选择', 'choice', '权衡', 'trade'];
|
|
117
|
+
if (!designKeys.some(k => c.toLowerCase().includes(k)))
|
|
118
|
+
add('info', 'DESIGN.md 建议记录设计决策和权衡', design);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { modulePath, issues, structure };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatStructure(s, indent = 0) {
|
|
125
|
+
const pre = ' '.repeat(indent);
|
|
126
|
+
if (s.type === 'dir') {
|
|
127
|
+
const lines = [`${pre}\u{1F4C1} ${s.name}/`];
|
|
128
|
+
for (const ch of (s.children || [])) lines.push(formatStructure(ch, indent + 1));
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
const sz = (s.size || 0) < 1024 ? `(${s.size} B)` : `(${Math.floor(s.size / 1024)} KB)`;
|
|
132
|
+
return `${pre}\u{1F4C4} ${s.name} ${sz}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatReport(r, verbose) {
|
|
136
|
+
const errs = r.issues.filter(i => i.severity === 'error').length;
|
|
137
|
+
const warns = r.issues.filter(i => i.severity === 'warning').length;
|
|
138
|
+
const passed = !hasFatal(r.issues);
|
|
139
|
+
const fields = {
|
|
140
|
+
'模块路径': r.modulePath,
|
|
141
|
+
'扫描结果': passed ? '\u2713 通过' : '\u2717 未通过',
|
|
142
|
+
'统计': `错误: ${errs} | 警告: ${warns}`,
|
|
143
|
+
};
|
|
144
|
+
const issues = r.issues.map(i => ({
|
|
145
|
+
severity: i.severity, message: i.message, path: i.path,
|
|
146
|
+
file_path: i.path || '', line_number: null,
|
|
147
|
+
}));
|
|
148
|
+
let report = buildReport('模块完整性扫描报告', fields, issues, verbose);
|
|
149
|
+
if (verbose && r.structure.name) {
|
|
150
|
+
report += '\n' + '-'.repeat(40) + '\n目录结构:\n' + '-'.repeat(40) + '\n' + formatStructure(r.structure);
|
|
151
|
+
}
|
|
152
|
+
return report;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// CLI
|
|
156
|
+
const opts = parseCliArgs(process.argv);
|
|
157
|
+
const result = scanModule(opts.target);
|
|
158
|
+
const passed = !hasFatal(result.issues);
|
|
159
|
+
|
|
160
|
+
if (opts.json) {
|
|
161
|
+
console.log(JSON.stringify({
|
|
162
|
+
module_path: result.modulePath, passed,
|
|
163
|
+
error_count: result.issues.filter(i => i.severity === 'error').length,
|
|
164
|
+
warning_count: result.issues.filter(i => i.severity === 'warning').length,
|
|
165
|
+
issues: result.issues
|
|
166
|
+
}, null, 2));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(formatReport(result, opts.verbose));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
process.exit(passed ? 0 : 1);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-quality
|
|
3
|
+
description: 代码质量校验关卡。检测复杂度、重复代码、命名规范、函数长度等质量指标。当魔尊提到代码质量、复杂度检查、代码异味、重构建议、lint检查、代码规范时使用。在复杂模块、重构完成时自动触发。
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: node>=18
|
|
6
|
+
user-invocable: true
|
|
7
|
+
disable-model-invocation: false
|
|
8
|
+
allowed-tools: Bash, Read, Glob
|
|
9
|
+
argument-hint: <扫描路径>
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# ⚖ 校验关卡 · 代码质量
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## 核心原则
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
代码质量 = 可读性 + 可维护性 + 可测试性
|
|
19
|
+
劣质代码是技术债,技术债是道基裂痕
|
|
20
|
+
复杂度是 bug 的温床
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 自动检查
|
|
24
|
+
|
|
25
|
+
运行质量检查脚本(跨平台):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 在 skill 目录下运行
|
|
29
|
+
node scripts/quality_checker.js <扫描路径>
|
|
30
|
+
node scripts/quality_checker.js <扫描路径> -v # 详细模式
|
|
31
|
+
node scripts/quality_checker.js <扫描路径> --json # JSON 输出
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 检测指标
|
|
35
|
+
|
|
36
|
+
### 复杂度指标
|
|
37
|
+
|
|
38
|
+
| 指标 | 阈值 | 超标后果 |
|
|
39
|
+
|------|------|----------|
|
|
40
|
+
| **圈复杂度** | ≤ 10 | 🟠 警告,建议拆分 |
|
|
41
|
+
| **函数长度** | ≤ 50 行 | 🟠 警告,建议拆分 |
|
|
42
|
+
| **文件长度** | ≤ 500 行 | 🟡 提示,考虑拆分 |
|
|
43
|
+
| **参数数量** | ≤ 5 | 🟠 警告,考虑封装 |
|
|
44
|
+
| **嵌套深度** | ≤ 4 | 🟠 警告,建议重构 |
|
|
45
|
+
| **行长度** | ≤ 120 | 🔵 提示 |
|
|
46
|
+
|
|
47
|
+
### 命名规范
|
|
48
|
+
|
|
49
|
+
| 类型 | 规范 | 示例 |
|
|
50
|
+
|------|------|------|
|
|
51
|
+
| **类名** | PascalCase | `UserService`, `HttpClient` |
|
|
52
|
+
| **函数名** | snake_case | `get_user`, `process_data` |
|
|
53
|
+
| **常量** | UPPER_SNAKE | `MAX_RETRY`, `DEFAULT_TIMEOUT` |
|
|
54
|
+
| **变量** | snake_case | `user_id`, `total_count` |
|
|
55
|
+
|
|
56
|
+
### 代码异味
|
|
57
|
+
|
|
58
|
+
| 异味 | 说明 | 严重度 |
|
|
59
|
+
|------|------|--------|
|
|
60
|
+
| 重复代码 | 相似代码块 > 10 行 | 🟠 High |
|
|
61
|
+
| 过长参数列表 | 参数 > 5 个 | 🟡 Medium |
|
|
62
|
+
| 魔法数字 | 未命名的常量 | 🟡 Medium |
|
|
63
|
+
| 死代码 | 未使用的函数/变量 | 🔵 Low |
|
|
64
|
+
| 注释代码 | 被注释的代码块 | 🔵 Low |
|
|
65
|
+
|
|
66
|
+
## 自动触发时机
|
|
67
|
+
|
|
68
|
+
| 场景 | 触发条件 |
|
|
69
|
+
|------|----------|
|
|
70
|
+
| 复杂模块 | 代码行数 > 200 |
|
|
71
|
+
| 重构完成 | 重构任务完成时 |
|
|
72
|
+
| 代码审查 | PR/MR 审查时 |
|
|
73
|
+
| 提交前 | 代码提交前检查 |
|
|
74
|
+
|
|
75
|
+
## 校验流程
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
1. 扫描代码文件
|
|
79
|
+
2. 计算复杂度指标
|
|
80
|
+
3. 检测代码异味
|
|
81
|
+
4. 验证命名规范
|
|
82
|
+
5. 输出质量校验报告
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 校验报告格式
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
## 代码质量校验报告
|
|
89
|
+
|
|
90
|
+
✓ 通过 | ✗ 未通过
|
|
91
|
+
|
|
92
|
+
### 复杂度指标
|
|
93
|
+
- 平均函数复杂度: N
|
|
94
|
+
- 超标函数数: N
|
|
95
|
+
- 最大文件行数: N
|
|
96
|
+
|
|
97
|
+
### 代码异味
|
|
98
|
+
- 🟠 High: N
|
|
99
|
+
- 🟡 Medium: N
|
|
100
|
+
- 🔵 Low: N
|
|
101
|
+
|
|
102
|
+
### 问题清单
|
|
103
|
+
|
|
104
|
+
| 文件 | 行号 | 类型 | 严重度 | 描述 |
|
|
105
|
+
|------|------|------|--------|------|
|
|
106
|
+
| ... | ... | ... | ... | ... |
|
|
107
|
+
|
|
108
|
+
### 结论
|
|
109
|
+
可交付 / 需重构后交付
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 重构建议
|
|
113
|
+
|
|
114
|
+
### 降低复杂度
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# 🔴 高复杂度 - 道基不稳
|
|
118
|
+
def process(data):
|
|
119
|
+
if condition1:
|
|
120
|
+
if condition2:
|
|
121
|
+
if condition3:
|
|
122
|
+
# 深层嵌套
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
# ✅ 低复杂度 - 道基稳固
|
|
126
|
+
def process(data):
|
|
127
|
+
if not condition1:
|
|
128
|
+
return
|
|
129
|
+
if not condition2:
|
|
130
|
+
return
|
|
131
|
+
if not condition3:
|
|
132
|
+
return
|
|
133
|
+
# 主逻辑
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 消除重复
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# 🔴 重复代码 - 异端
|
|
140
|
+
def func1():
|
|
141
|
+
# 10行相同逻辑
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def func2():
|
|
145
|
+
# 10行相同逻辑
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# ✅ 提取公共函数 - 正道
|
|
149
|
+
def common_logic():
|
|
150
|
+
# 公共逻辑
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
def func1():
|
|
154
|
+
common_logic()
|
|
155
|
+
|
|
156
|
+
def func2():
|
|
157
|
+
common_logic()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
7
|
+
|
|
8
|
+
// 质量规则配置
|
|
9
|
+
const MAX_LINE_LENGTH = 120;
|
|
10
|
+
const MAX_FUNCTION_LENGTH = 50;
|
|
11
|
+
const MAX_FILE_LENGTH = 500;
|
|
12
|
+
const MAX_COMPLEXITY = 10;
|
|
13
|
+
const MAX_PARAMETERS = 5;
|
|
14
|
+
const MIN_FUNCTION_NAME_LENGTH = 2;
|
|
15
|
+
|
|
16
|
+
const EXCLUDE_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build', '.tox']);
|
|
17
|
+
const CODE_EXTENSIONS = new Set(['.py', '.js', '.ts', '.go', '.java', '.rs', '.c', '.cpp']);
|
|
18
|
+
|
|
19
|
+
const COMMENT_PREFIXES = {
|
|
20
|
+
'.js': '//', '.ts': '//', '.go': '//', '.java': '//',
|
|
21
|
+
'.c': '//', '.cpp': '//', '.rs': '//',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// --- Analysis ---
|
|
25
|
+
|
|
26
|
+
function analyzeGenericFile(filePath) {
|
|
27
|
+
const metrics = {
|
|
28
|
+
path: filePath, lines: 0, code_lines: 0, comment_lines: 0,
|
|
29
|
+
blank_lines: 0, functions: 0, classes: 0,
|
|
30
|
+
max_complexity: 0, avg_function_length: 0,
|
|
31
|
+
};
|
|
32
|
+
const issues = [];
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
} catch { return { metrics, issues }; }
|
|
37
|
+
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
metrics.lines = lines.length;
|
|
40
|
+
const prefix = COMMENT_PREFIXES[
|
|
41
|
+
path.extname(filePath).toLowerCase()
|
|
42
|
+
] || '//';
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const stripped = lines[i].trim();
|
|
46
|
+
if (!stripped) metrics.blank_lines++;
|
|
47
|
+
else if (
|
|
48
|
+
stripped.startsWith(prefix) ||
|
|
49
|
+
stripped.startsWith('/*') ||
|
|
50
|
+
stripped.startsWith('*')
|
|
51
|
+
) metrics.comment_lines++;
|
|
52
|
+
else metrics.code_lines++;
|
|
53
|
+
|
|
54
|
+
if (lines[i].length > MAX_LINE_LENGTH) {
|
|
55
|
+
issues.push({
|
|
56
|
+
severity: 'info', category: '格式',
|
|
57
|
+
message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`,
|
|
58
|
+
file_path: filePath, line_number: i + 1,
|
|
59
|
+
suggestion: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (metrics.code_lines > MAX_FILE_LENGTH) {
|
|
65
|
+
issues.push({
|
|
66
|
+
severity: 'warning', category: '复杂度',
|
|
67
|
+
message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`,
|
|
68
|
+
file_path: filePath, suggestion: '考虑拆分为多个模块',
|
|
69
|
+
line_number: null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { metrics, issues };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function analyzePythonFile(filePath) {
|
|
77
|
+
const metrics = {
|
|
78
|
+
path: filePath, lines: 0, code_lines: 0, comment_lines: 0,
|
|
79
|
+
blank_lines: 0, functions: 0, classes: 0,
|
|
80
|
+
max_complexity: 0, avg_function_length: 0,
|
|
81
|
+
};
|
|
82
|
+
const issues = [];
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
86
|
+
} catch (e) {
|
|
87
|
+
issues.push({
|
|
88
|
+
severity: 'error', category: '文件',
|
|
89
|
+
message: `无法读取文件: ${e.message}`,
|
|
90
|
+
file_path: filePath, line_number: null, suggestion: null,
|
|
91
|
+
});
|
|
92
|
+
return { metrics, issues };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
metrics.lines = lines.length;
|
|
97
|
+
let inMultiline = false;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const stripped = lines[i].trim();
|
|
101
|
+
if (!stripped) { metrics.blank_lines++; }
|
|
102
|
+
else if (stripped.startsWith('#')) { metrics.comment_lines++; }
|
|
103
|
+
else if (stripped.includes('"""') || stripped.includes("'''")) {
|
|
104
|
+
const dq = (stripped.match(/"""/g) || []).length;
|
|
105
|
+
const sq = (stripped.match(/'''/g) || []).length;
|
|
106
|
+
if (dq === 2 || sq === 2) { metrics.comment_lines++; }
|
|
107
|
+
else { inMultiline = !inMultiline; metrics.comment_lines++; }
|
|
108
|
+
} else if (inMultiline) { metrics.comment_lines++; }
|
|
109
|
+
else { metrics.code_lines++; }
|
|
110
|
+
|
|
111
|
+
if (lines[i].length > MAX_LINE_LENGTH) {
|
|
112
|
+
issues.push({
|
|
113
|
+
severity: 'info', category: '格式',
|
|
114
|
+
message: `行过长 (${lines[i].length} > ${MAX_LINE_LENGTH})`,
|
|
115
|
+
file_path: filePath, line_number: i + 1,
|
|
116
|
+
suggestion: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (metrics.code_lines > MAX_FILE_LENGTH) {
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: 'warning', category: '复杂度',
|
|
124
|
+
message: `文件过长 (${metrics.code_lines} 行代码 > ${MAX_FILE_LENGTH})`,
|
|
125
|
+
file_path: filePath, suggestion: '考虑拆分为多个模块',
|
|
126
|
+
line_number: null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Regex-based Python analysis (no AST available in Node)
|
|
131
|
+
const funcRegex = /^( *)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gm;
|
|
132
|
+
const classRegex = /^( *)class\s+(\w+)/gm;
|
|
133
|
+
const functions = [];
|
|
134
|
+
let match;
|
|
135
|
+
|
|
136
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
137
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
138
|
+
const name = match[2];
|
|
139
|
+
const indent = match[1].length;
|
|
140
|
+
const params = match[3].trim()
|
|
141
|
+
? match[3].split(',').map(p => p.trim())
|
|
142
|
+
.filter(p => p && p !== 'self' && p !== 'cls')
|
|
143
|
+
: [];
|
|
144
|
+
|
|
145
|
+
// Calculate function length by finding next line at same or lesser indent
|
|
146
|
+
const funcLines = lines.slice(lineNum); // lines after def
|
|
147
|
+
let length = 1;
|
|
148
|
+
for (let j = 1; j < funcLines.length; j++) {
|
|
149
|
+
const l = funcLines[j];
|
|
150
|
+
if (l.trim() === '') { length++; continue; }
|
|
151
|
+
const curIndent = l.match(/^(\s*)/)[1].length;
|
|
152
|
+
if (curIndent <= indent && l.trim() !== '') break;
|
|
153
|
+
length++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Estimate complexity from function body
|
|
157
|
+
const bodyLines = lines.slice(lineNum, lineNum + length - 1);
|
|
158
|
+
let complexity = 1;
|
|
159
|
+
for (const bl of bodyLines) {
|
|
160
|
+
const s = bl.trim();
|
|
161
|
+
if (/^(if|elif|while|for)\s/.test(s) || /^(if|elif|while|for)\(/.test(s)) complexity++;
|
|
162
|
+
if (/^except(\s|:)/.test(s)) complexity++;
|
|
163
|
+
if (/\s(and|or)\s/.test(s)) complexity++;
|
|
164
|
+
if (/\sfor\s/.test(s) && /\sin\s/.test(s) && (s.includes('[') || s.includes('('))) complexity++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
functions.push({ name, line: lineNum, length, complexity, parameters: params.length });
|
|
168
|
+
metrics.max_complexity = Math.max(metrics.max_complexity, complexity);
|
|
169
|
+
|
|
170
|
+
// Check function length
|
|
171
|
+
if (length > MAX_FUNCTION_LENGTH) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: 'warning', category: '复杂度',
|
|
174
|
+
message: `函数 '${name}' 过长 (${length} 行 > ${MAX_FUNCTION_LENGTH})`,
|
|
175
|
+
file_path: filePath, line_number: lineNum,
|
|
176
|
+
suggestion: '考虑拆分为多个小函数',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// Check complexity
|
|
180
|
+
if (complexity > MAX_COMPLEXITY) {
|
|
181
|
+
issues.push({
|
|
182
|
+
severity: 'warning', category: '复杂度',
|
|
183
|
+
message: `函数 '${name}' 圈复杂度过高 (${complexity} > ${MAX_COMPLEXITY})`,
|
|
184
|
+
file_path: filePath, line_number: lineNum,
|
|
185
|
+
suggestion: '减少嵌套层级,提取子函数',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Check parameter count
|
|
189
|
+
if (params.length > MAX_PARAMETERS) {
|
|
190
|
+
issues.push({
|
|
191
|
+
severity: 'warning', category: '设计',
|
|
192
|
+
message: `函数 '${name}' 参数过多 (${params.length} > ${MAX_PARAMETERS})`,
|
|
193
|
+
file_path: filePath, line_number: lineNum,
|
|
194
|
+
suggestion: '考虑使用配置对象或数据类封装参数',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Check naming
|
|
198
|
+
const SPECIAL = new Set([
|
|
199
|
+
'setUp', 'tearDown', 'setUpClass',
|
|
200
|
+
'tearDownClass', 'setUpModule', 'tearDownModule',
|
|
201
|
+
]);
|
|
202
|
+
if (!name.startsWith('_') && !SPECIAL.has(name) && !name.startsWith('visit_')) {
|
|
203
|
+
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
|
|
204
|
+
issues.push({
|
|
205
|
+
severity: 'info', category: '命名',
|
|
206
|
+
message: `函数名 '${name}' 不符合 snake_case 规范`,
|
|
207
|
+
file_path: filePath, line_number: lineNum,
|
|
208
|
+
suggestion: '函数名应使用 snake_case',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (name.length < MIN_FUNCTION_NAME_LENGTH) {
|
|
213
|
+
issues.push({
|
|
214
|
+
severity: 'warning', category: '命名',
|
|
215
|
+
message: `函数名 '${name}' 过短`,
|
|
216
|
+
file_path: filePath, line_number: lineNum,
|
|
217
|
+
suggestion: '使用更具描述性的函数名',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
223
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
224
|
+
const name = match[2];
|
|
225
|
+
metrics.classes++;
|
|
226
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: 'warning', category: '命名',
|
|
229
|
+
message: `类名 '${name}' 不符合 PascalCase 规范`,
|
|
230
|
+
file_path: filePath, line_number: lineNum,
|
|
231
|
+
suggestion: '类名应使用 PascalCase,如 MyClassName',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
metrics.functions = functions.length;
|
|
237
|
+
if (functions.length > 0) {
|
|
238
|
+
metrics.avg_function_length = functions.reduce((s, f) => s + f.length, 0) / functions.length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { metrics, issues };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Directory scan ---
|
|
245
|
+
|
|
246
|
+
function scanDirectory(scanPath, excludeDirs) {
|
|
247
|
+
const resolved = path.resolve(scanPath);
|
|
248
|
+
const exclude = excludeDirs || EXCLUDE_DIRS;
|
|
249
|
+
const result = {
|
|
250
|
+
scan_path: resolved, files_scanned: 0,
|
|
251
|
+
total_lines: 0, total_code_lines: 0,
|
|
252
|
+
issues: [], file_metrics: [],
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
function walk(dir) {
|
|
256
|
+
let entries;
|
|
257
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
if (exclude.has(entry.name)) continue;
|
|
260
|
+
const full = path.join(dir, entry.name);
|
|
261
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
262
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
263
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
264
|
+
|
|
265
|
+
result.files_scanned++;
|
|
266
|
+
const { metrics, issues } = ext === '.py' ? analyzePythonFile(full) : analyzeGenericFile(full);
|
|
267
|
+
result.file_metrics.push(metrics);
|
|
268
|
+
result.issues.push(...issues);
|
|
269
|
+
result.total_lines += metrics.lines;
|
|
270
|
+
result.total_code_lines += metrics.code_lines;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
walk(resolved);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Reporting ---
|
|
279
|
+
|
|
280
|
+
function passed(result) { return !hasFatal(result.issues); }
|
|
281
|
+
|
|
282
|
+
function formatReport(result, verbose) {
|
|
283
|
+
const errs = result.issues.filter(i => i.severity === 'error').length;
|
|
284
|
+
const warns = result.issues.filter(i => i.severity === 'warning').length;
|
|
285
|
+
const fields = {
|
|
286
|
+
'扫描路径': result.scan_path,
|
|
287
|
+
'扫描文件': result.files_scanned,
|
|
288
|
+
'总行数': result.total_lines,
|
|
289
|
+
'代码行数': result.total_code_lines,
|
|
290
|
+
'检查结果': passed(result) ? '✓ 通过' : '✗ 需要关注',
|
|
291
|
+
'统计': `错误: ${errs} | 警告: ${warns}`,
|
|
292
|
+
};
|
|
293
|
+
let report = buildReport(
|
|
294
|
+
'代码质量检查报告', fields, result.issues, verbose, 'category'
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (verbose && result.file_metrics.length) {
|
|
298
|
+
const complex = result.file_metrics
|
|
299
|
+
.filter(m => m.max_complexity > 0)
|
|
300
|
+
.sort((a, b) => b.max_complexity - a.max_complexity)
|
|
301
|
+
.slice(0, 5);
|
|
302
|
+
if (complex.length) {
|
|
303
|
+
const lines = ['\n' + '-'.repeat(40), '复杂度最高的文件:', '-'.repeat(40)];
|
|
304
|
+
for (const m of complex) lines.push(` ${m.path}: 复杂度 ${m.max_complexity}, ${m.functions} 个函数`);
|
|
305
|
+
report += '\n' + lines.join('\n');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return report;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- CLI ---
|
|
312
|
+
|
|
313
|
+
function main() {
|
|
314
|
+
const opts = parseCliArgs(process.argv);
|
|
315
|
+
|
|
316
|
+
const result = scanDirectory(opts.target);
|
|
317
|
+
|
|
318
|
+
if (opts.json) {
|
|
319
|
+
const output = {
|
|
320
|
+
scan_path: result.scan_path,
|
|
321
|
+
files_scanned: result.files_scanned,
|
|
322
|
+
total_lines: result.total_lines,
|
|
323
|
+
total_code_lines: result.total_code_lines,
|
|
324
|
+
passed: passed(result),
|
|
325
|
+
error_count: result.issues.filter(i => i.severity === 'error').length,
|
|
326
|
+
warning_count: result.issues.filter(i => i.severity === 'warning').length,
|
|
327
|
+
issues: result.issues
|
|
328
|
+
};
|
|
329
|
+
console.log(JSON.stringify(output, null, 2));
|
|
330
|
+
} else {
|
|
331
|
+
console.log(formatReport(result, opts.verbose));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
process.exit(passed(result) ? 0 : 1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
main();
|