autosnippet 3.0.10 → 3.0.13

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 (56) hide show
  1. package/bin/cli.js +64 -1
  2. package/config/default.json +9 -0
  3. package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
  4. package/dashboard/dist/index.html +1 -1
  5. package/lib/cli/SetupService.js +92 -5
  6. package/lib/cli/UpgradeService.js +14 -5
  7. package/lib/core/discovery/GenericDiscoverer.js +4 -28
  8. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +246 -0
  9. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +80 -0
  10. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
  11. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +600 -0
  12. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +125 -342
  13. package/lib/external/mcp/handlers/bootstrap/refine.js +362 -0
  14. package/lib/external/mcp/handlers/bootstrap.js +6 -590
  15. package/lib/external/mcp/handlers/browse.js +119 -9
  16. package/lib/external/mcp/handlers/guard.js +25 -6
  17. package/lib/external/mcp/handlers/search.js +56 -24
  18. package/lib/http/routes/guardRules.js +9 -17
  19. package/lib/injection/ServiceContainer.js +12 -3
  20. package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
  21. package/lib/platform/ios/xcode/XcodeIntegration.js +40 -659
  22. package/lib/platform/ios/xcode/XcodeWriteUtils.js +220 -0
  23. package/lib/service/chat/ChatAgent.js +39 -418
  24. package/lib/service/chat/ChatAgentPrompts.js +149 -0
  25. package/lib/service/chat/ChatAgentTasks.js +297 -0
  26. package/lib/service/chat/tools/_shared.js +61 -0
  27. package/lib/service/chat/tools/ai-analysis.js +284 -0
  28. package/lib/service/chat/tools/ast-graph.js +681 -0
  29. package/lib/service/chat/tools/composite.js +496 -0
  30. package/lib/service/chat/tools/guard.js +265 -0
  31. package/lib/service/chat/tools/index.js +250 -0
  32. package/lib/service/chat/tools/infrastructure.js +222 -0
  33. package/lib/service/chat/tools/knowledge-graph.js +234 -0
  34. package/lib/service/chat/tools/lifecycle.js +469 -0
  35. package/lib/service/chat/tools/project-access.js +923 -0
  36. package/lib/service/chat/tools/query.js +264 -0
  37. package/lib/service/chat/tools.js +14 -3994
  38. package/lib/service/cursor/AgentInstructionsGenerator.js +395 -0
  39. package/lib/service/cursor/CursorDeliveryPipeline.js +70 -11
  40. package/lib/service/cursor/FileProtection.js +116 -0
  41. package/lib/service/cursor/KnowledgeCompressor.js +61 -11
  42. package/lib/service/cursor/SkillsSyncer.js +5 -3
  43. package/lib/service/cursor/TopicClassifier.js +19 -3
  44. package/lib/service/guard/ExclusionManager.js +26 -2
  45. package/lib/service/guard/GuardCheckEngine.js +38 -370
  46. package/lib/service/guard/GuardCodeChecks.js +362 -0
  47. package/lib/service/guard/GuardCrossFileChecks.js +307 -0
  48. package/lib/service/guard/GuardPatternUtils.js +180 -0
  49. package/lib/service/guard/GuardService.js +80 -38
  50. package/lib/service/module/ModuleService.js +1 -0
  51. package/lib/service/search/SearchEngine.js +10 -2
  52. package/lib/service/wiki/WikiGenerator.js +226 -1532
  53. package/lib/service/wiki/WikiRenderers.js +1878 -0
  54. package/lib/service/wiki/WikiUtils.js +907 -0
  55. package/lib/shared/LanguageService.js +299 -0
  56. package/package.json +1 -1
@@ -0,0 +1,362 @@
1
+ /**
2
+ * GuardCodeChecks - Guard 代码级别检查(跨行 / 配对检查)
3
+ *
4
+ * 从 GuardCheckEngine._runCodeLevelChecks 拆分
5
+ * 按语言分发到各自的检查逻辑,不依赖正则规则
6
+ */
7
+
8
+ /**
9
+ * 代码级别检查 - 需要上下文理解的检查(跨行 / 配对检查)
10
+ * 按语言分发到各自的检查逻辑
11
+ * 支持 disabledRules 禁用特定检查、codeLevelThresholds 调整阈值
12
+ *
13
+ * @param {string} code - 源代码
14
+ * @param {string} language - 语言标识
15
+ * @param {string[]} lines - 按行拆分的源代码
16
+ * @param {object} options
17
+ * @param {string[]} [options.disabledRules] - 禁用的规则 ID 列表
18
+ * @param {Record<string, number>} [options.codeLevelThresholds] - 可配置阈值
19
+ * @returns {Array<{ruleId, message, severity, line, snippet, dimension?, fixSuggestion?}>}
20
+ */
21
+ export function runCodeLevelChecks(code, language, lines, options = {}) {
22
+ const violations = [];
23
+ const disabledSet = new Set(options.disabledRules || []);
24
+ const thresholds = options.codeLevelThresholds || {};
25
+ /** 判断 ruleId 是否被禁用 */
26
+ const isDisabled = (ruleId) => disabledSet.has(ruleId);
27
+ /** 获取可配置阈值,回退到默认值 */
28
+ const threshold = (ruleId, defaultVal) => thresholds[ruleId] ?? defaultVal;
29
+
30
+ // ── ObjC ──
31
+ if (language === 'objc') {
32
+ // KVO 观察者未移除检查
33
+ if (!isDisabled('objc-kvo-missing-remove') && code.includes('addObserver') && !code.includes('removeObserver')) {
34
+ const lineIdx = lines.findIndex((l) => /addObserver/.test(l));
35
+ violations.push({
36
+ ruleId: 'objc-kvo-missing-remove',
37
+ message: '存在 addObserver 未发现配对 removeObserver,请在 dealloc 或合适时机移除',
38
+ severity: 'warning',
39
+ line: lineIdx >= 0 ? lineIdx + 1 : 1,
40
+ snippet: lineIdx >= 0 ? lines[lineIdx].trim().slice(0, 120) : '',
41
+ dimension: 'file',
42
+ });
43
+ }
44
+
45
+ // ObjC Category 重名检查 (同文件)
46
+ if (!isDisabled('objc-duplicate-category')) {
47
+ const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g;
48
+ const categories = {};
49
+ for (let i = 0; i < lines.length; i++) {
50
+ categoryRegex.lastIndex = 0;
51
+ const m = categoryRegex.exec(lines[i]);
52
+ if (!m) {
53
+ continue;
54
+ }
55
+ const key = `${m[1]}(${m[2]})`;
56
+ if (!categories[key]) {
57
+ categories[key] = [];
58
+ }
59
+ categories[key].push({ line: i + 1, snippet: lines[i].trim().slice(0, 120) });
60
+ }
61
+ for (const [key, occs] of Object.entries(categories)) {
62
+ if (occs.length <= 1) {
63
+ continue;
64
+ }
65
+ for (let j = 1; j < occs.length; j++) {
66
+ violations.push({
67
+ ruleId: 'objc-duplicate-category',
68
+ message: `同文件内 Category 重名:${key},首次在第 ${occs[0].line} 行`,
69
+ severity: 'warning',
70
+ line: occs[j].line,
71
+ snippet: occs[j].snippet,
72
+ dimension: 'file',
73
+ });
74
+ }
75
+ }
76
+ } // end isDisabled('objc-duplicate-category')
77
+ }
78
+
79
+ // ── JavaScript / TypeScript ──
80
+ if (language === 'javascript' || language === 'typescript') {
81
+ // Promise 未处理 rejection 检查
82
+ // 文件中存在 .then() 但没有对应的 .catch() 或 try-catch
83
+ if (!isDisabled('js-unhandled-promise') && code.includes('.then(') && !code.includes('.catch(') && !code.includes('try')) {
84
+ const thenLines = [];
85
+ for (let i = 0; i < lines.length; i++) {
86
+ if (/\.then\s*\(/.test(lines[i])) {
87
+ thenLines.push(i);
88
+ }
89
+ }
90
+ if (thenLines.length > 0) {
91
+ violations.push({
92
+ ruleId: 'js-unhandled-promise',
93
+ message: 'Promise 链缺少 .catch() 错误处理,未捕获的 rejection 可能导致静默失败',
94
+ severity: 'warning',
95
+ line: thenLines[0] + 1,
96
+ snippet: lines[thenLines[0]].trim().slice(0, 120),
97
+ dimension: 'file',
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ // ── Go ──
104
+ if (language === 'go') {
105
+ // defer 在循环内检查 — defer 在函数结束时才执行,循环内 defer 可能资源泄露
106
+ if (!isDisabled('go-defer-in-loop')) {
107
+ let inLoop = false;
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const trimmed = lines[i].trim();
110
+ if (/^for\s/.test(trimmed) || /^for\s*\{/.test(trimmed)) {
111
+ inLoop = true;
112
+ }
113
+ if (inLoop && /^\s*defer\s/.test(lines[i])) {
114
+ violations.push({
115
+ ruleId: 'go-defer-in-loop',
116
+ message: 'defer 在循环内会延迟到函数返回时才执行,可能导致资源泄露或大量堆积',
117
+ severity: 'warning',
118
+ line: i + 1,
119
+ snippet: lines[i].trim().slice(0, 120),
120
+ dimension: 'file',
121
+ fixSuggestion: '将循环体提取到独立函数中,或手动调用 Close()',
122
+ });
123
+ }
124
+ // 简化: 遇到 } 且缩进回到顶层,认为循环结束
125
+ if (inLoop && trimmed === '}' && (lines[i].match(/^\t/) || lines[i].match(/^}/))) {
126
+ inLoop = false;
127
+ }
128
+ }
129
+ } // end isDisabled('go-defer-in-loop')
130
+ }
131
+
132
+ // ── Python ──
133
+ if (language === 'python') {
134
+ // 文件中同时存在 tab 和 space 缩进
135
+ if (!isDisabled('py-mixed-indentation')) {
136
+ let hasTab = false;
137
+ let hasSpace = false;
138
+ for (let i = 0; i < Math.min(lines.length, 200); i++) {
139
+ if (/^\t/.test(lines[i])) hasTab = true;
140
+ if (/^ {2,}/.test(lines[i]) && !/^\t/.test(lines[i])) hasSpace = true;
141
+ }
142
+ if (hasTab && hasSpace) {
143
+ violations.push({
144
+ ruleId: 'py-mixed-indentation',
145
+ message: '文件混用 tab 和 space 缩进,Python 对此敏感,请统一使用 space',
146
+ severity: 'warning',
147
+ line: 1,
148
+ snippet: '',
149
+ dimension: 'file',
150
+ });
151
+ }
152
+ } // end isDisabled('py-mixed-indentation')
153
+ }
154
+
155
+ // ── Swift ──
156
+ if (language === 'swift') {
157
+ // 强制解包滥用检查: 连续多行使用 ! 强制解包(单行已被正则规则覆盖,这里检查文件级滥用)
158
+ if (!isDisabled('swift-excessive-force-unwrap')) {
159
+ let forceUnwrapCount = 0;
160
+ for (let i = 0; i < lines.length; i++) {
161
+ // 排除 != 和 !== 运算符, 以及注释行
162
+ const trimmed = lines[i].trimStart();
163
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
164
+ // 匹配 variable! 或 expression!. 形式,但排除 !=
165
+ if (/\w!(?!=)[.\s,)\]]/.test(lines[i]) || /\w!$/.test(lines[i].trim())) {
166
+ forceUnwrapCount++;
167
+ }
168
+ }
169
+ if (forceUnwrapCount > threshold('swift-excessive-force-unwrap', 5)) {
170
+ violations.push({
171
+ ruleId: 'swift-excessive-force-unwrap',
172
+ message: `文件包含 ${forceUnwrapCount} 处强制解包 (!),建议使用 guard let / if let 安全解包`,
173
+ severity: 'warning',
174
+ line: 1,
175
+ snippet: `${forceUnwrapCount} force unwraps detected`,
176
+ dimension: 'file',
177
+ fixSuggestion: '使用 guard let value = optional else { return } 替代 optional!',
178
+ });
179
+ }
180
+ } // end isDisabled('swift-excessive-force-unwrap')
181
+ }
182
+
183
+ // ── Java ──
184
+ if (language === 'java') {
185
+ // 资源泄露检查: new InputStream/Connection/Reader 未在 try-with-resources 或 finally 中关闭
186
+ if (!isDisabled('java-resource-leak')) {
187
+ const resourcePatterns = /new\s+(FileInputStream|FileOutputStream|BufferedReader|BufferedWriter|Connection|Socket|FileReader|FileWriter|Scanner)\s*\(/;
188
+ const hasResourceAlloc = lines.some((l) => resourcePatterns.test(l));
189
+ const hasTryWithResource = code.includes('try (') || code.includes('try(');
190
+ const hasFinallyClose = code.includes('finally') && code.includes('.close()');
191
+ if (hasResourceAlloc && !hasTryWithResource && !hasFinallyClose) {
192
+ const lineIdx = lines.findIndex((l) => resourcePatterns.test(l));
193
+ violations.push({
194
+ ruleId: 'java-resource-leak',
195
+ message: '资源分配后未使用 try-with-resources 或 finally/close(),可能造成资源泄露',
196
+ severity: 'warning',
197
+ line: lineIdx >= 0 ? lineIdx + 1 : 1,
198
+ snippet: lineIdx >= 0 ? lines[lineIdx].trim().slice(0, 120) : '',
199
+ dimension: 'file',
200
+ fixSuggestion: '使用 try (var res = new Resource()) { ... } 自动关闭资源',
201
+ });
202
+ }
203
+ } // end isDisabled('java-resource-leak')
204
+
205
+ // synchronized 在非 final 字段上 — 可能导致锁对象被替换
206
+ if (!isDisabled('java-sync-non-final')) {
207
+ const syncRegex = /synchronized\s*\(\s*(\w+)\s*\)/;
208
+ for (let i = 0; i < lines.length; i++) {
209
+ const m = syncRegex.exec(lines[i]);
210
+ if (m && m[1] !== 'this' && !m[1].endsWith('.class')) {
211
+ // 检查该变量是否声明为 final
212
+ const varName = m[1];
213
+ const declaredFinal = lines.some((l) => new RegExp(`final\\s+\\w+.*\\b${varName}\\b`).test(l));
214
+ if (!declaredFinal) {
215
+ violations.push({
216
+ ruleId: 'java-sync-non-final',
217
+ message: `synchronized 使用了非 final 变量 "${varName}",锁对象可能被重新赋值`,
218
+ severity: 'warning',
219
+ line: i + 1,
220
+ snippet: lines[i].trim().slice(0, 120),
221
+ dimension: 'file',
222
+ fixSuggestion: `将 ${varName} 声明为 private final`,
223
+ });
224
+ }
225
+ }
226
+ }
227
+ } // end isDisabled('java-sync-non-final')
228
+ }
229
+
230
+ // ── Kotlin ──
231
+ if (language === 'kotlin') {
232
+ // GlobalScope.launch — 生命周期泄露风险
233
+ if (!isDisabled('kotlin-global-scope')) {
234
+ for (let i = 0; i < lines.length; i++) {
235
+ if (/GlobalScope\s*\.\s*(launch|async)/.test(lines[i])) {
236
+ violations.push({
237
+ ruleId: 'kotlin-global-scope',
238
+ message: 'GlobalScope.launch/async 不绑定生命周期,可能导致协程泄露',
239
+ severity: 'warning',
240
+ line: i + 1,
241
+ snippet: lines[i].trim().slice(0, 120),
242
+ dimension: 'file',
243
+ fixSuggestion: '使用 viewModelScope、lifecycleScope 或自定义 CoroutineScope 替代',
244
+ });
245
+ }
246
+ }
247
+ } // end isDisabled('kotlin-global-scope')
248
+
249
+ // runBlocking 在 main/UI 线程 — 可能冻结 UI
250
+ if (!isDisabled('kotlin-run-blocking') && code.includes('runBlocking')) {
251
+ const lineIdx = lines.findIndex((l) => /runBlocking\s*[({]/.test(l));
252
+ if (lineIdx >= 0) {
253
+ violations.push({
254
+ ruleId: 'kotlin-run-blocking',
255
+ message: 'runBlocking 会阻塞当前线程,避免在 Main/UI 线程中使用',
256
+ severity: 'warning',
257
+ line: lineIdx + 1,
258
+ snippet: lines[lineIdx].trim().slice(0, 120),
259
+ dimension: 'file',
260
+ fixSuggestion: '改用 suspend 函数 或 launch { } 非阻塞协程',
261
+ });
262
+ }
263
+ }
264
+ }
265
+
266
+ // ── Rust ──
267
+ if (language === 'rust') {
268
+ // .unwrap() 滥用检查 — 生产代码应使用 ? 或 expect()
269
+ if (!isDisabled('rust-excessive-unwrap')) {
270
+ let unwrapCount = 0;
271
+ const unwrapLines = [];
272
+ for (let i = 0; i < lines.length; i++) {
273
+ const trimmed = lines[i].trimStart();
274
+ // 跳过测试代码和注释
275
+ if (trimmed.startsWith('//') || trimmed.startsWith('#[test]')) continue;
276
+ if (/\.unwrap\(\)/.test(lines[i])) {
277
+ unwrapCount++;
278
+ if (unwrapLines.length < 3) unwrapLines.push(i);
279
+ }
280
+ }
281
+ if (unwrapCount > threshold('rust-excessive-unwrap', 3)) {
282
+ violations.push({
283
+ ruleId: 'rust-excessive-unwrap',
284
+ message: `文件包含 ${unwrapCount} 处 .unwrap(),生产代码建议使用 ? 操作符或 .expect("reason")`,
285
+ severity: 'warning',
286
+ line: unwrapLines[0] + 1,
287
+ snippet: lines[unwrapLines[0]].trim().slice(0, 120),
288
+ dimension: 'file',
289
+ fixSuggestion: '使用 ? 操作符向上传播错误,或 .expect("具体原因") 提供崩溃上下文',
290
+ });
291
+ }
292
+ } // end isDisabled('rust-excessive-unwrap')
293
+
294
+ // unsafe 块数量检查
295
+ if (!isDisabled('rust-excessive-unsafe')) {
296
+ let unsafeCount = 0;
297
+ for (let i = 0; i < lines.length; i++) {
298
+ if (/\bunsafe\s*\{/.test(lines[i]) || /\bunsafe\s+fn\b/.test(lines[i])) {
299
+ unsafeCount++;
300
+ }
301
+ }
302
+ if (unsafeCount > threshold('rust-excessive-unsafe', 3)) {
303
+ violations.push({
304
+ ruleId: 'rust-excessive-unsafe',
305
+ message: `文件包含 ${unsafeCount} 处 unsafe 块/函数,请审查是否都必要`,
306
+ severity: 'warning',
307
+ line: 1,
308
+ snippet: `${unsafeCount} unsafe blocks detected`,
309
+ dimension: 'file',
310
+ fixSuggestion: '尽量使用 safe abstraction 封装 unsafe 代码,减少 unsafe 暴露面',
311
+ });
312
+ }
313
+ } // end isDisabled('rust-excessive-unsafe')
314
+ }
315
+
316
+ // ── Dart ──
317
+ if (language === 'dart') {
318
+ // setState after dispose — Flutter 常见内存泄露
319
+ if (!isDisabled('dart-setstate-after-dispose') && code.includes('setState') && code.includes('dispose')) {
320
+ // 检查 dispose 方法后是否还有 async 回调中的 setState
321
+ const disposeIdx = lines.findIndex((l) => /void\s+dispose\s*\(/.test(l) || /\bsuper\.dispose\(\)/.test(l));
322
+ if (disposeIdx >= 0) {
323
+ // 检查是否有 mounted 检查保护
324
+ const hasMountedCheck = code.includes('if (mounted)') || code.includes('if (!mounted)');
325
+ if (!hasMountedCheck) {
326
+ violations.push({
327
+ ruleId: 'dart-setstate-after-dispose',
328
+ message: '存在 setState 调用但未检查 mounted 状态,异步回调可能在 dispose 后触发 setState',
329
+ severity: 'warning',
330
+ line: disposeIdx + 1,
331
+ snippet: lines[disposeIdx].trim().slice(0, 120),
332
+ dimension: 'file',
333
+ fixSuggestion: '在 setState 前添加 if (!mounted) return; 检查',
334
+ });
335
+ }
336
+ }
337
+ }
338
+
339
+ // late 变量未初始化风险
340
+ if (!isDisabled('dart-excessive-late')) {
341
+ let lateCount = 0;
342
+ for (let i = 0; i < lines.length; i++) {
343
+ if (/\blate\s+(?!final\b)\w+/.test(lines[i]) && !lines[i].includes('=')) {
344
+ lateCount++;
345
+ }
346
+ }
347
+ if (lateCount > threshold('dart-excessive-late', 3)) {
348
+ violations.push({
349
+ ruleId: 'dart-excessive-late',
350
+ message: `文件有 ${lateCount} 个 late 非 final 变量且无初始值,访问未初始化变量会抛出 LateInitializationError`,
351
+ severity: 'warning',
352
+ line: 1,
353
+ snippet: `${lateCount} late variables without initializer`,
354
+ dimension: 'file',
355
+ fixSuggestion: '考虑使用可空类型 + null 检查,或 late final + 初始化赋值',
356
+ });
357
+ }
358
+ } // end isDisabled('dart-excessive-late')
359
+ }
360
+
361
+ return violations;
362
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * GuardCrossFileChecks - Guard 跨文件检查
3
+ *
4
+ * 从 GuardCheckEngine._runCrossFileChecks 拆分
5
+ * 包含: 跨文件规则检查 + 路径归一化工具
6
+ */
7
+
8
+ /**
9
+ * 解析相对 import 路径为归一化路径(去掉扩展名)
10
+ * @param {string} fromDir - 当前文件目录
11
+ * @param {string} importPath - 相对路径如 './foo' 或 '../bar/baz'
12
+ * @returns {string|null}
13
+ */
14
+ export function resolveImportPath(fromDir, importPath) {
15
+ try {
16
+ const parts = `${fromDir}/${importPath}`.split('/');
17
+ const resolved = [];
18
+ for (const p of parts) {
19
+ if (p === '.' || p === '') continue;
20
+ if (p === '..') { resolved.pop(); continue; }
21
+ resolved.push(p);
22
+ }
23
+ // 去掉扩展名归一化
24
+ let result = resolved.join('/');
25
+ result = result.replace(/\.(js|ts|jsx|tsx|mjs|mts)$/, '');
26
+ // 移除 /index 后缀(index barrel 导入)
27
+ result = result.replace(/\/index$/, '');
28
+ return result;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 归一化文件路径(去扩展名,用于 import 比较)
36
+ * @param {string} filePath
37
+ * @returns {string}
38
+ */
39
+ export function normalizeFilePath(filePath) {
40
+ return filePath.replace(/\.(js|ts|jsx|tsx|mjs|mts)$/, '').replace(/\/index$/, '');
41
+ }
42
+
43
+ /**
44
+ * 跨文件检查 — 需要多文件上下文才能发现的问题
45
+ * @param {Array<{path: string, content: string}>} files
46
+ * @param {object} options
47
+ * @param {string[]} [options.disabledRules] - 禁用的规则 ID 列表
48
+ * @returns {Array<{ruleId, message, severity, locations}>}
49
+ */
50
+ export function runCrossFileChecks(files, options = {}) {
51
+ const violations = [];
52
+ const disabledSet = new Set(options.disabledRules || []);
53
+ const isDisabled = (ruleId) => disabledSet.has(ruleId);
54
+
55
+ // ── ObjC Category 跨文件重名检查 ──
56
+ if (!isDisabled('objc-cross-file-duplicate-category')) {
57
+ const categoryMap = new Map();
58
+ const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g;
59
+
60
+ for (const { path: filePath, content } of files) {
61
+ const ext = filePath.split('.').pop()?.toLowerCase();
62
+ if (ext !== 'm' && ext !== 'mm' && ext !== 'h') {
63
+ continue;
64
+ }
65
+
66
+ const lines = content.split(/\r?\n/);
67
+ for (let i = 0; i < lines.length; i++) {
68
+ categoryRegex.lastIndex = 0;
69
+ let m;
70
+ while ((m = categoryRegex.exec(lines[i])) !== null) {
71
+ const key = `${m[1]}(${m[2]})`;
72
+ if (!categoryMap.has(key)) {
73
+ categoryMap.set(key, []);
74
+ }
75
+ categoryMap.get(key).push({
76
+ filePath,
77
+ line: i + 1,
78
+ snippet: lines[i].trim().slice(0, 120),
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ for (const [key, locations] of categoryMap) {
85
+ if (locations.length <= 1) {
86
+ continue;
87
+ }
88
+
89
+ const hFiles = locations.filter((l) => l.filePath.endsWith('.h'));
90
+ const mFiles = locations.filter((l) => !l.filePath.endsWith('.h'));
91
+ const hasDuplicateH = hFiles.length > 1;
92
+ const hasDuplicateM = mFiles.length > 1;
93
+ const tooMany = locations.length > 2;
94
+
95
+ if (hasDuplicateH || hasDuplicateM || tooMany) {
96
+ const conflictLocations = tooMany
97
+ ? locations
98
+ : hasDuplicateH && hasDuplicateM
99
+ ? locations
100
+ : hasDuplicateH
101
+ ? hFiles
102
+ : mFiles;
103
+
104
+ violations.push({
105
+ ruleId: 'objc-cross-file-duplicate-category',
106
+ message: `Category ${key} 在 ${conflictLocations.length} 个文件中重复声明,可能导致方法覆盖或未定义行为`,
107
+ severity: 'warning',
108
+ locations: conflictLocations,
109
+ });
110
+ }
111
+ }
112
+ } // end isDisabled('objc-cross-file-duplicate-category')
113
+
114
+ // ── JS/TS 循环依赖检查 ──
115
+ // 检测 A imports B 且 B imports A 的直接循环
116
+ if (!isDisabled('js-circular-import')) {
117
+ const jsImportMap = new Map(); // filePath → Set<importedPath>
118
+ const jsExts = new Set(['js', 'ts', 'jsx', 'tsx', 'mjs', 'mts']);
119
+ const importRegex = /(?:import\s+.+?\s+from\s+['"](.+?)['"]|require\s*\(\s*['"](.+?)['"]\s*\))/g;
120
+
121
+ for (const { path: filePath, content } of files) {
122
+ const ext = filePath.split('.').pop()?.toLowerCase();
123
+ if (!jsExts.has(ext)) continue;
124
+
125
+ const imports = new Set();
126
+ const lines = content.split(/\r?\n/);
127
+ for (const line of lines) {
128
+ importRegex.lastIndex = 0;
129
+ let m;
130
+ while ((m = importRegex.exec(line)) !== null) {
131
+ const importPath = m[1] || m[2];
132
+ if (importPath.startsWith('.')) {
133
+ // 解析相对路径为归一化 key
134
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'));
135
+ const resolved = resolveImportPath(dir, importPath);
136
+ if (resolved) imports.add(resolved);
137
+ }
138
+ }
139
+ }
140
+ if (imports.size > 0) {
141
+ jsImportMap.set(normalizeFilePath(filePath), imports);
142
+ }
143
+ }
144
+
145
+ // 检测直接双向循环: A→B 且 B→A
146
+ const reportedCycles = new Set();
147
+ for (const [fileA, importsA] of jsImportMap) {
148
+ for (const depB of importsA) {
149
+ const importsB = jsImportMap.get(depB);
150
+ if (importsB?.has(fileA)) {
151
+ const cycleKey = [fileA, depB].sort().join(' <-> ');
152
+ if (!reportedCycles.has(cycleKey)) {
153
+ reportedCycles.add(cycleKey);
154
+ violations.push({
155
+ ruleId: 'js-circular-import',
156
+ message: `检测到循环依赖,两个模块互相导入可能导致运行时 undefined`,
157
+ severity: 'warning',
158
+ locations: [
159
+ { filePath: fileA, line: 1, snippet: `imports ${depB.split('/').pop()}` },
160
+ { filePath: depB, line: 1, snippet: `imports ${fileA.split('/').pop()}` },
161
+ ],
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ } // end isDisabled('js-circular-import')
168
+
169
+ // ── Java/Kotlin 同名类跨文件检查 ──
170
+ if (!isDisabled('java-duplicate-class-name')) {
171
+ const classMap = new Map(); // className → [{filePath, line, snippet}]
172
+ const javaClassRegex = /(?:public\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)/;
173
+ const jkExts = new Set(['java', 'kt']);
174
+
175
+ for (const { path: filePath, content } of files) {
176
+ const ext = filePath.split('.').pop()?.toLowerCase();
177
+ if (!jkExts.has(ext)) continue;
178
+
179
+ const lines = content.split(/\r?\n/);
180
+ for (let i = 0; i < lines.length; i++) {
181
+ const m = javaClassRegex.exec(lines[i]);
182
+ if (m) {
183
+ const className = m[1];
184
+ if (!classMap.has(className)) {
185
+ classMap.set(className, []);
186
+ }
187
+ classMap.get(className).push({
188
+ filePath,
189
+ line: i + 1,
190
+ snippet: lines[i].trim().slice(0, 120),
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ for (const [className, locations] of classMap) {
197
+ if (locations.length > 1) {
198
+ violations.push({
199
+ ruleId: 'java-duplicate-class-name',
200
+ message: `类名 "${className}" 在 ${locations.length} 个文件中定义,可能导致导入歧义`,
201
+ severity: 'info',
202
+ locations,
203
+ });
204
+ }
205
+ }
206
+ } // end isDisabled('java-duplicate-class-name')
207
+
208
+ // ── Go 多文件 init() 函数检查 ──
209
+ // 同一 package 下多个文件都有 init(),执行顺序依赖文件名排序,容易出错
210
+ if (!isDisabled('go-multiple-init')) {
211
+ const goInitMap = new Map(); // dirPath → [{filePath, line}]
212
+
213
+ for (const { path: filePath, content } of files) {
214
+ if (!filePath.endsWith('.go')) continue;
215
+
216
+ const lines = content.split(/\r?\n/);
217
+ for (let i = 0; i < lines.length; i++) {
218
+ if (/^func\s+init\s*\(\s*\)/.test(lines[i].trim())) {
219
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'));
220
+ if (!goInitMap.has(dir)) {
221
+ goInitMap.set(dir, []);
222
+ }
223
+ goInitMap.get(dir).push({
224
+ filePath,
225
+ line: i + 1,
226
+ snippet: lines[i].trim().slice(0, 120),
227
+ });
228
+ break; // 每个文件只记录一次
229
+ }
230
+ }
231
+ }
232
+
233
+ for (const [dir, locations] of goInitMap) {
234
+ if (locations.length > 2) {
235
+ violations.push({
236
+ ruleId: 'go-multiple-init',
237
+ message: `同一 package (${dir.split('/').pop()}) 中 ${locations.length} 个文件都定义了 init(),执行顺序依赖文件名排序`,
238
+ severity: 'info',
239
+ locations,
240
+ });
241
+ }
242
+ }
243
+ } // end isDisabled('go-multiple-init')
244
+
245
+ // ── Swift Extension 方法跨文件冲突检查 ──
246
+ if (!isDisabled('swift-cross-file-extension-conflict')) {
247
+ const swiftExtMethodMap = new Map(); // "TypeName.methodName" → [{filePath, line}]
248
+ const swiftExtRegex = /extension\s+(\w+)/;
249
+ const swiftFuncRegex = /func\s+(\w+)\s*\(/;
250
+
251
+ for (const { path: filePath, content } of files) {
252
+ if (!filePath.endsWith('.swift')) continue;
253
+
254
+ const lines = content.split(/\r?\n/);
255
+ let currentExt = null;
256
+ let braceDepth = 0;
257
+
258
+ for (let i = 0; i < lines.length; i++) {
259
+ const extMatch = swiftExtRegex.exec(lines[i]);
260
+ if (extMatch && !currentExt) {
261
+ currentExt = extMatch[1];
262
+ braceDepth = 0;
263
+ }
264
+
265
+ if (currentExt) {
266
+ for (const ch of lines[i]) {
267
+ if (ch === '{') braceDepth++;
268
+ else if (ch === '}') braceDepth--;
269
+ }
270
+
271
+ const funcMatch = swiftFuncRegex.exec(lines[i]);
272
+ if (funcMatch && braceDepth >= 1) {
273
+ const key = `${currentExt}.${funcMatch[1]}`;
274
+ if (!swiftExtMethodMap.has(key)) {
275
+ swiftExtMethodMap.set(key, []);
276
+ }
277
+ swiftExtMethodMap.get(key).push({
278
+ filePath,
279
+ line: i + 1,
280
+ snippet: lines[i].trim().slice(0, 120),
281
+ });
282
+ }
283
+
284
+ if (braceDepth <= 0) {
285
+ currentExt = null;
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ for (const [key, locations] of swiftExtMethodMap) {
292
+ if (locations.length > 1) {
293
+ const uniqueFiles = new Set(locations.map((l) => l.filePath));
294
+ if (uniqueFiles.size > 1) {
295
+ violations.push({
296
+ ruleId: 'swift-cross-file-extension-conflict',
297
+ message: `Extension 方法 ${key} 在 ${uniqueFiles.size} 个文件中定义,可能导致方法冲突`,
298
+ severity: 'warning',
299
+ locations,
300
+ });
301
+ }
302
+ }
303
+ }
304
+ } // end isDisabled('swift-cross-file-extension-conflict')
305
+
306
+ return violations;
307
+ }