UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

369 lines (368 loc) 18.1 kB
/** * GuardCodeChecks - Guard 代码级别检查(跨行 / 配对检查) * * 从 GuardCheckEngine._runCodeLevelChecks 拆分 * 按语言分发到各自的检查逻辑,不依赖正则规则 */ export function runCodeLevelChecks(code, language, lines, options = {}) { const violations = []; const disabledSet = new Set(options.disabledRules || []); const thresholds = options.codeLevelThresholds || {}; /** 判断 ruleId 是否被禁用 */ const isDisabled = (ruleId) => disabledSet.has(ruleId); /** 获取可配置阈值,回退到默认值 */ const threshold = (ruleId, defaultVal) => thresholds[ruleId] ?? defaultVal; // ── ObjC ── if (language === 'objc') { // KVO 观察者未移除检查 if (!isDisabled('objc-kvo-missing-remove') && code.includes('addObserver') && !code.includes('removeObserver')) { const lineIdx = lines.findIndex((l) => /addObserver/.test(l)); violations.push({ ruleId: 'objc-kvo-missing-remove', message: '存在 addObserver 未发现配对 removeObserver,请在 dealloc 或合适时机移除', severity: 'warning', line: lineIdx >= 0 ? lineIdx + 1 : 1, snippet: lineIdx >= 0 ? lines[lineIdx].trim().slice(0, 120) : '', dimension: 'file', }); } // ObjC Category 重名检查 (同文件) if (!isDisabled('objc-duplicate-category')) { const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g; const categories = {}; for (let i = 0; i < lines.length; i++) { categoryRegex.lastIndex = 0; const m = categoryRegex.exec(lines[i]); if (!m) { continue; } const key = `${m[1]}(${m[2]})`; if (!categories[key]) { categories[key] = []; } categories[key].push({ line: i + 1, snippet: lines[i].trim().slice(0, 120) }); } for (const [key, occs] of Object.entries(categories)) { if (occs.length <= 1) { continue; } for (let j = 1; j < occs.length; j++) { violations.push({ ruleId: 'objc-duplicate-category', message: `同文件内 Category 重名:${key},首次在第 ${occs[0].line} 行`, severity: 'warning', line: occs[j].line, snippet: occs[j].snippet, dimension: 'file', }); } } } // end isDisabled('objc-duplicate-category') } // ── JavaScript / TypeScript ── if (language === 'javascript' || language === 'typescript') { // Promise 未处理 rejection 检查 // 文件中存在 .then() 但没有对应的 .catch() 或 try-catch if (!isDisabled('js-unhandled-promise') && code.includes('.then(') && !code.includes('.catch(') && !code.includes('try')) { const thenLines = []; for (let i = 0; i < lines.length; i++) { if (/\.then\s*\(/.test(lines[i])) { thenLines.push(i); } } if (thenLines.length > 0) { violations.push({ ruleId: 'js-unhandled-promise', message: 'Promise 链缺少 .catch() 错误处理,未捕获的 rejection 可能导致静默失败', severity: 'warning', line: thenLines[0] + 1, snippet: lines[thenLines[0]].trim().slice(0, 120), dimension: 'file', }); } } } // ── Go ── if (language === 'go') { // defer 在循环内检查 — defer 在函数结束时才执行,循环内 defer 可能资源泄露 // 排除 go func() { defer ... } 模式(goroutine 内的 defer 是安全的) if (!isDisabled('go-defer-in-loop')) { let inLoop = false; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (/^for\s/.test(trimmed) || /^for\s*\{/.test(trimmed)) { inLoop = true; } if (inLoop && /^\s*defer\s/.test(lines[i])) { // 回溯找最近的作用域开场 { — 判断 defer 是否在匿名函数/goroutine 内 let insideAnonymousFunc = false; let braceBalance = 0; for (let j = i - 1; j >= 0; j--) { const prev = lines[j]; braceBalance += (prev.match(/\}/g) || []).length; braceBalance -= (prev.match(/\{/g) || []).length; if (braceBalance < 0) { // 找到包裹 defer 的最近 { — 检查该行是否包含 func 关键字 if (/\bfunc\b/.test(prev)) { insideAnonymousFunc = true; } break; } } if (!insideAnonymousFunc) { violations.push({ ruleId: 'go-defer-in-loop', message: 'defer 在循环内会延迟到函数返回时才执行,可能导致资源泄露或大量堆积', severity: 'warning', line: i + 1, snippet: lines[i].trim().slice(0, 120), dimension: 'file', fixSuggestion: '将循环体提取到独立函数中,或手动调用 Close()', }); } } // 简化: 遇到 } 且缩进回到顶层,认为循环结束 if (inLoop && trimmed === '}' && (lines[i].match(/^\t/) || lines[i].match(/^}/))) { inLoop = false; } } } // end isDisabled('go-defer-in-loop') } // ── Python ── if (language === 'python') { // 文件中同时存在 tab 和 space 缩进 if (!isDisabled('py-mixed-indentation')) { let hasTab = false; let hasSpace = false; for (let i = 0; i < Math.min(lines.length, 200); i++) { if (/^\t/.test(lines[i])) { hasTab = true; } if (/^ {2,}/.test(lines[i]) && !/^\t/.test(lines[i])) { hasSpace = true; } } if (hasTab && hasSpace) { violations.push({ ruleId: 'py-mixed-indentation', message: '文件混用 tab 和 space 缩进,Python 对此敏感,请统一使用 space', severity: 'warning', line: 1, snippet: '', dimension: 'file', }); } } // end isDisabled('py-mixed-indentation') } // ── Swift ── if (language === 'swift') { // 强制解包滥用检查: 连续多行使用 ! 强制解包(单行已被正则规则覆盖,这里检查文件级滥用) if (!isDisabled('swift-excessive-force-unwrap')) { let forceUnwrapCount = 0; for (let i = 0; i < lines.length; i++) { // 排除 != 和 !== 运算符, 以及注释行 const trimmed = lines[i].trimStart(); if (trimmed.startsWith('//') || trimmed.startsWith('/*')) { continue; } // 匹配 variable! 或 expression!. 形式,但排除 != if (/\w!(?!=)[.\s,)\]]/.test(lines[i]) || /\w!$/.test(lines[i].trim())) { forceUnwrapCount++; } } if (forceUnwrapCount > threshold('swift-excessive-force-unwrap', 5)) { violations.push({ ruleId: 'swift-excessive-force-unwrap', message: `文件包含 ${forceUnwrapCount} 处强制解包 (!),建议使用 guard let / if let 安全解包`, severity: 'warning', line: 1, snippet: `${forceUnwrapCount} force unwraps detected`, dimension: 'file', fixSuggestion: '使用 guard let value = optional else { return } 替代 optional!', }); } } // end isDisabled('swift-excessive-force-unwrap') } // ── Java ── if (language === 'java') { // 资源泄露检查: new InputStream/Connection/Reader 未在 try-with-resources 或 finally 中关闭 if (!isDisabled('java-resource-leak')) { const resourcePatterns = /new\s+(FileInputStream|FileOutputStream|BufferedReader|BufferedWriter|Connection|Socket|FileReader|FileWriter|Scanner)\s*\(/; const hasResourceAlloc = lines.some((l) => resourcePatterns.test(l)); const hasTryWithResource = code.includes('try (') || code.includes('try('); const hasFinallyClose = code.includes('finally') && code.includes('.close()'); if (hasResourceAlloc && !hasTryWithResource && !hasFinallyClose) { const lineIdx = lines.findIndex((l) => resourcePatterns.test(l)); violations.push({ ruleId: 'java-resource-leak', message: '资源分配后未使用 try-with-resources 或 finally/close(),可能造成资源泄露', severity: 'warning', line: lineIdx >= 0 ? lineIdx + 1 : 1, snippet: lineIdx >= 0 ? lines[lineIdx].trim().slice(0, 120) : '', dimension: 'file', fixSuggestion: '使用 try (var res = new Resource()) { ... } 自动关闭资源', }); } } // end isDisabled('java-resource-leak') // synchronized 在非 final 字段上 — 可能导致锁对象被替换 if (!isDisabled('java-sync-non-final')) { const syncRegex = /synchronized\s*\(\s*(\w+)\s*\)/; for (let i = 0; i < lines.length; i++) { const m = syncRegex.exec(lines[i]); if (m && m[1] !== 'this' && !m[1].endsWith('.class')) { // 检查该变量是否声明为 final const varName = m[1]; const declaredFinal = lines.some((l) => new RegExp(`final\\s+\\w+.*\\b${varName}\\b`).test(l)); if (!declaredFinal) { violations.push({ ruleId: 'java-sync-non-final', message: `synchronized 使用了非 final 变量 "${varName}",锁对象可能被重新赋值`, severity: 'warning', line: i + 1, snippet: lines[i].trim().slice(0, 120), dimension: 'file', fixSuggestion: `将 ${varName} 声明为 private final`, }); } } } } // end isDisabled('java-sync-non-final') } // ── Kotlin ── if (language === 'kotlin') { // GlobalScope.launch — 生命周期泄露风险 if (!isDisabled('kotlin-global-scope')) { for (let i = 0; i < lines.length; i++) { if (/GlobalScope\s*\.\s*(launch|async)/.test(lines[i])) { violations.push({ ruleId: 'kotlin-global-scope', message: 'GlobalScope.launch/async 不绑定生命周期,可能导致协程泄露', severity: 'warning', line: i + 1, snippet: lines[i].trim().slice(0, 120), dimension: 'file', fixSuggestion: '使用 viewModelScope、lifecycleScope 或自定义 CoroutineScope 替代', }); } } } // end isDisabled('kotlin-global-scope') // runBlocking 在 main/UI 线程 — 可能冻结 UI if (!isDisabled('kotlin-run-blocking') && code.includes('runBlocking')) { const lineIdx = lines.findIndex((l) => /runBlocking\s*[({]/.test(l)); if (lineIdx >= 0) { violations.push({ ruleId: 'kotlin-run-blocking', message: 'runBlocking 会阻塞当前线程,避免在 Main/UI 线程中使用', severity: 'warning', line: lineIdx + 1, snippet: lines[lineIdx].trim().slice(0, 120), dimension: 'file', fixSuggestion: '改用 suspend 函数 或 launch { } 非阻塞协程', }); } } } // ── Rust ── if (language === 'rust') { // .unwrap() 滥用检查 — 生产代码应使用 ? 或 expect() if (!isDisabled('rust-excessive-unwrap')) { let unwrapCount = 0; const unwrapLines = []; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trimStart(); // 跳过测试代码和注释 if (trimmed.startsWith('//') || trimmed.startsWith('#[test]')) { continue; } if (/\.unwrap\(\)/.test(lines[i])) { unwrapCount++; if (unwrapLines.length < 3) { unwrapLines.push(i); } } } if (unwrapCount > threshold('rust-excessive-unwrap', 3)) { violations.push({ ruleId: 'rust-excessive-unwrap', message: `文件包含 ${unwrapCount} 处 .unwrap(),生产代码建议使用 ? 操作符或 .expect("reason")`, severity: 'warning', line: unwrapLines[0] + 1, snippet: lines[unwrapLines[0]].trim().slice(0, 120), dimension: 'file', fixSuggestion: '使用 ? 操作符向上传播错误,或 .expect("具体原因") 提供崩溃上下文', }); } } // end isDisabled('rust-excessive-unwrap') // unsafe 块数量检查 if (!isDisabled('rust-excessive-unsafe')) { let unsafeCount = 0; for (let i = 0; i < lines.length; i++) { if (/\bunsafe\s*\{/.test(lines[i]) || /\bunsafe\s+fn\b/.test(lines[i])) { unsafeCount++; } } if (unsafeCount > threshold('rust-excessive-unsafe', 3)) { violations.push({ ruleId: 'rust-excessive-unsafe', message: `文件包含 ${unsafeCount} 处 unsafe 块/函数,请审查是否都必要`, severity: 'warning', line: 1, snippet: `${unsafeCount} unsafe blocks detected`, dimension: 'file', fixSuggestion: '尽量使用 safe abstraction 封装 unsafe 代码,减少 unsafe 暴露面', }); } } // end isDisabled('rust-excessive-unsafe') } // ── Dart ── if (language === 'dart') { // setState after dispose — Flutter 常见内存泄露 if (!isDisabled('dart-setstate-after-dispose') && code.includes('setState') && code.includes('dispose')) { // 检查 dispose 方法后是否还有 async 回调中的 setState const disposeIdx = lines.findIndex((l) => /void\s+dispose\s*\(/.test(l) || /\bsuper\.dispose\(\)/.test(l)); if (disposeIdx >= 0) { // 检查是否有 mounted 检查保护 const hasMountedCheck = code.includes('if (mounted)') || code.includes('if (!mounted)'); if (!hasMountedCheck) { violations.push({ ruleId: 'dart-setstate-after-dispose', message: '存在 setState 调用但未检查 mounted 状态,异步回调可能在 dispose 后触发 setState', severity: 'warning', line: disposeIdx + 1, snippet: lines[disposeIdx].trim().slice(0, 120), dimension: 'file', fixSuggestion: '在 setState 前添加 if (!mounted) return; 检查', }); } } } // late 变量未初始化风险 if (!isDisabled('dart-excessive-late')) { let lateCount = 0; for (let i = 0; i < lines.length; i++) { if (/\blate\s+(?!final\b)\w+/.test(lines[i]) && !lines[i].includes('=')) { lateCount++; } } if (lateCount > threshold('dart-excessive-late', 3)) { violations.push({ ruleId: 'dart-excessive-late', message: `文件有 ${lateCount} 个 late 非 final 变量且无初始值,访问未初始化变量会抛出 LateInitializationError`, severity: 'warning', line: 1, snippet: `${lateCount} late variables without initializer`, dimension: 'file', fixSuggestion: '考虑使用可空类型 + null 检查,或 late final + 初始化赋值', }); } } // end isDisabled('dart-excessive-late') } return violations; }