export const CHECK_TYPES = [ "magic_value", "naming", "loop_db", "exception", "import", "lifecycle", "transaction", "resource", "security", "performance", "threading", ] as const; export type CheckType = (typeof CHECK_TYPES)[number]; export const CHECK_SEVERITIES = ["error", "warning", "info"] as const; export type CheckSeverity = (typeof CHECK_SEVERITIES)[number]; export interface CheckResult { line: number; column: number; type: CheckType; severity: CheckSeverity; message: string; rule: string; } export type CheckLanguage = "java" | "csharp" | "python"; const ALLOWED_NUMBERS = new Set(["0", "1", "-1", "2", "10", "100"]); const DB_KEYWORDS = [ "BusinessDataServiceHelper", "QueryServiceHelper", "SaveServiceHelper", "OperationServiceHelper", "DispatchServiceHelper", "ORM.create", "DB.", "loadSingle", "loadInBatch", "executeQuery", "executeSql", "queryDataSet", ]; export function checkCode(code: string, language: CheckLanguage = "java"): CheckResult[] { const lines = code.split("\n"); const results = [ ...checkMagicValues(lines), ...checkNaming(lines, language), ...checkLoopDb(lines), ...checkExceptionHandling(lines), ...checkCosmicReviewerRules(lines), ]; return results.sort((a, b) => a.line - b.line || a.column - b.column); } function checkCosmicReviewerRules(lines: string[]): CheckResult[] { return [ ...checkLifecycleMisuse(lines), ...checkTransactionMisuse(lines), ...checkResourceHandling(lines), ...checkSecurityPatterns(lines), ...checkThreadingPatterns(lines), ...checkUiPerformance(lines), ...checkViewInteractionOrder(lines), ...checkLoggingAndDiagnostics(lines), ]; } function checkLifecycleMisuse(lines: string[]): CheckResult[] { const results: CheckResult[] = []; const initializeRanges = findMethodRanges(lines, "initialize"); const beforeBindRanges = findMethodRanges(lines, "beforeBindData"); const afterBindRanges = findMethodRanges(lines, "afterBindData"); const afterOperationRanges = findMethodRanges(lines, "afterExecuteOperationTransaction"); for (const range of initializeRanges) { forEachLineInRange(lines, range, (line, index) => { if (/addItemClickListener\s*\(/.test(line)) { results.push(makeResult(index, line, "addItemClickListener", "lifecycle", "error", "P0:initialize 中注册监听器,可能导致监听生命周期错误;必须移到 registerListener", "p0-initialize-listener")); } if (/getView\s*\(\)\s*\.\s*(setVisible|setEnable|updateView|showConfirm|showTipNotification)\s*\(/.test(line)) { results.push(makeResult(index, line, "getView", "lifecycle", "error", "P0:initialize 中操作 UI 控件,控件状态可能不生效;必须移到 afterBindData 等合适阶段", "p0-initialize-ui")); } }); } for (const range of [...beforeBindRanges, ...afterBindRanges]) { forEachLineInRange(lines, range, (line, index) => { if (/(getModel\s*\(\)\s*\.\s*)?setValue\s*\(/.test(line)) { results.push(makeResult(index, line, "setValue", "lifecycle", "error", "P0:数据绑定阶段修改模型数据,可能破坏绑定流程;默认值必须放到 afterCreateNewData 等阶段", "p0-binddata-setvalue")); } }); } for (const range of afterOperationRanges) { forEachLineInRange(lines, range, (line, index) => { if (/getOperationResult\s*\(/.test(line)) { results.push(makeResult(index, line, "getOperationResult", "lifecycle", "error", "P0:afterExecuteOperationTransaction 的参数禁止调用 getOperationResult;必须确认事件参数类型", "p0-after-operation-result")); } }); } return results; } function checkTransactionMisuse(lines: string[]): CheckResult[] { const results: CheckResult[] = []; const ranges = [...findMethodRanges(lines, "beforeExecuteOperationTransaction"), ...findMethodRanges(lines, "afterExecuteOperationTransaction")]; for (const range of ranges) { forEachLineInRange(lines, range, (line, index) => { if (/SaveServiceHelper\s*\.\s*(save|update)\s*\(/.test(line)) { results.push(makeResult(index, line, "SaveServiceHelper", "transaction", "error", "P0:操作事务钩子中独立保存会破坏事务一致性;必须直接修改平台传入的数据实体", "p0-transaction-save")); } }); } return results; } function checkResourceHandling(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isCommentLine(line)) continue; if (!/\bDataSet\b\s+\w+\s*=/.test(line)) continue; if (/try\s*\([^)]*\bDataSet\b/.test(line)) continue; if (nearbyLineHas(lines, i, -2, /\btry\s*\(/)) continue; results.push(makeResult(i, line, "DataSet", "resource", "error", "P0:DataSet 创建后未明显使用 try-with-resources 关闭,可能导致连接泄漏", "p0-dataset-not-closed")); } for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isCommentLine(line)) continue; if (/\.\s*getDynamicObject\s*\([^)]*\)\s*\.\s*get(?:String|Long|Int|Integer|Double|Date|PkValue|DynamicObject|DynamicObjectCollection)\s*\(/.test(line)) { results.push(makeResult(i, line, "getDynamicObject", "resource", "warning", "P1:嵌套 DynamicObject 直接取值缺少空值保护,引用字段为空时可能 NPE;取对象并判空后再取值,或使用安全取值工具", "p1-dynamicobject-chain-null-risk")); } if (/row\s*\.\s*get(?:BigDecimal|String|Long|Int|Integer|Date)\s*\(/.test(line) && !nearbyLineHas(lines, i, -2, /\brow\b\s*(?:!=|==)\s*null|Optional\.ofNullable\s*\(\s*row\s*\)|Objects\.nonNull\s*\(\s*row\s*\)/)) { results.push(makeResult(i, line, "row", "resource", "warning", "P1:DataSet.Row 取值可能返回 null,参与运算或解包前必须判空或给默认值", "p1-dataset-row-null-risk")); } } return results; } function checkSecurityPatterns(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isCommentLine(line)) continue; if (/\bStatement\b/.test(line) && !/\bPreparedStatement\b/.test(line)) { results.push(makeResult(i, line, "Statement", "security", "error", "P0:使用 Statement 存在 SQL 注入风险;必须改用参数化查询或 QFilter", "p0-raw-statement")); } if (/"\s*(SELECT|UPDATE|DELETE|INSERT)\b[^"]*"\s*\+|\+\s*"\s*(WHERE|AND|OR|SET)\b/i.test(line)) { results.push(makeResult(i, line, "+", "security", "error", "P0:SQL 字符串拼接存在注入风险;必须改用参数化查询或 QFilter", "p0-sql-concat")); } if (/= 2 && !looksLikeRowIndexedViewCall(args)) { const fieldArgs = args.slice(1).join(","); if (/(entry|entries|detail|row|qty|price|amount|material|item)/i.test(fieldArgs)) { results.push(makeResult(i, line, entryUiMatch[1], "performance", "warning", "P1:疑似对分录字段使用单头 setEnable/setVisible;分录字段必须使用带 rowIndex 的重载", "p1-entry-field-without-row-index")); } } } } if (beginInitCount > endInitCount) { const index = lines.findIndex((line) => /beginInit\s*\(/.test(line)); if (index >= 0) { results.push(makeResult(index, lines[index], "beginInit", "performance", "warning", "P1:beginInit/endInit 数量不成对,异常时可能导致表单行为异常", "p1-unpaired-begin-init")); } } return results; } function checkViewInteractionOrder(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isCommentLine(line)) continue; if (/setDataChanged\s*\(\s*false\s*\)/.test(line)) { for (let j = i + 1; j < Math.min(lines.length, i + 25); j++) { if (/^\s*}\s*$/.test(lines[j])) break; if (!isCommentLine(lines[j]) && /(getModel\s*\(\)\s*\.\s*)?setValue\s*\(/.test(lines[j])) { results.push(makeResult(i, line, "setDataChanged", "lifecycle", "warning", "P1:setDataChanged(false) 后仍继续 setValue,脏标记会被重新置回;必须放到所有 setValue 之后", "p1-set-data-changed-before-setvalue")); break; } } } if (/showConfirm\s*\(/.test(line)) { const statement = collectStatement(lines, i, 8); const args = extractCallArgs(statement, /showConfirm\s*\(/); if (args.length < 3 || /\bnull\b/.test(args[2] ?? "")) { results.push(makeResult(i, line, "showConfirm", "lifecycle", "error", "P0:showConfirm 未提供有效回调监听器,用户选择不会触发业务处理", "p0-show-confirm-without-callback")); } } if (/getView\s*\(\)\s*\.\s*close\s*\(/.test(line)) { for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) { if (/^\s*}\s*$/.test(lines[j])) break; if (!isCommentLine(lines[j]) && /returnDataToParent\s*\(/.test(lines[j])) { results.push(makeResult(i, line, "close", "lifecycle", "error", "P0:close 早于 returnDataToParent 会导致返回数据不执行;必须执行 returnDataToParent 后再 close", "p0-close-before-return-data")); break; } } } } return results; } function checkLoggingAndDiagnostics(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (isCommentLine(line)) continue; if (/System\s*\.\s*out\s*\.\s*println\s*\(/.test(line)) { results.push(makeResult(i, line, "System.out.println", "exception", "warning", "P1:生产代码禁止使用 System.out.println;必须使用平台日志", "p1-system-out")); } if (/\.printStackTrace\s*\(/.test(line)) { results.push(makeResult(i, line, "printStackTrace", "exception", "warning", "P2:禁止直接 printStackTrace;必须使用 logger.error 并传入异常对象", "p2-print-stack-trace")); } } return results; } function checkMagicValues(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; if (isCommentLine(line)) continue; const numberPattern = /new\s+BigDecimal\s*\(\s*(\d+)\s*\)|(? 1 && !value.startsWith(" ") && !value.includes("{") && !value.includes("%") && !isCommonString(value) && !isLogMessage(line) && !isAnnotation(line) && isBusinessConstant(value) ) { results.push({ line: lineNum, column: match.index ?? 0, type: "magic_value", severity: "warning", message: `检测到可能的业务常量 "${value}",必须定义为常量`, rule: "magic-string", }); } } } return results; } function checkNaming(lines: string[], language: CheckLanguage): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; if (isCommentLine(line)) continue; if (language === "java") { const classMatch = line.match(/\bclass\s+([A-Za-z_]\w*)/); if (classMatch) { const className = classMatch[1]; if (!isPascalCase(className) && !className.startsWith("Abstract") && !className.startsWith("I")) { results.push({ line: lineNum, column: line.indexOf(className), type: "naming", severity: "error", message: `类名 "${className}" 必须使用 PascalCase 命名`, rule: "class-naming", }); } } const methodMatch = line.match(/\b(?:public|protected|private)\s+\w+(?:<[^>]+>)?\s+(\w+)\s*\(/); if (methodMatch) { const methodName = methodMatch[1]; if ( !isPascalCase(methodName) && !isCamelCase(methodName) && !methodName.startsWith("get") && !methodName.startsWith("set") && !methodName.startsWith("is") && !methodName.startsWith("has") && methodName !== "main" ) { results.push({ line: lineNum, column: line.indexOf(methodName), type: "naming", severity: "warning", message: `方法名 "${methodName}" 必须使用 camelCase 命名`, rule: "method-naming", }); } } const constantMatch = line.match(/\b(?:static\s+final|final\s+static)\s+\w+\s+(\w+)\s*=/); if (constantMatch) { const constantName = constantMatch[1]; if (!isUpperSnakeCase(constantName)) { results.push({ line: lineNum, column: line.indexOf(constantName), type: "naming", severity: "warning", message: `常量 "${constantName}" 必须使用 UPPER_SNAKE_CASE 命名`, rule: "constant-naming", }); } } } } return results; } function checkLoopDb(lines: string[]): CheckResult[] { const results: CheckResult[] = []; let inLoop = false; let loopBraceCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (/\b(for|while)\s*\(/.test(trimmed)) { inLoop = true; loopBraceCount = 0; } if (!inLoop) continue; for (const char of line) { if (char === "{") loopBraceCount++; if (char === "}") loopBraceCount--; } for (const keyword of DB_KEYWORDS) { if (line.includes(keyword)) { results.push({ line: i + 1, column: line.indexOf(keyword), type: "loop_db", severity: "error", message: `在循环中调用 DB 操作 "${keyword}",可能导致性能问题`, rule: "loop-db-query", }); break; } } if (loopBraceCount <= 0 && trimmed.includes("}")) { inLoop = false; } } return results; } function checkExceptionHandling(lines: string[]): CheckResult[] { const results: CheckResult[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (!/\bcatch\s*\(/.test(trimmed)) continue; let j = i + 1; let hasContent = false; let braceCount = 0; let catchStarted = false; if (trimmed.includes("{")) { catchStarted = true; braceCount += (trimmed.match(/{/g) || []).length; } while (j < lines.length) { const catchLine = lines[j].trim(); if (!catchStarted && catchLine.includes("{")) catchStarted = true; if (catchStarted) { braceCount += (catchLine.match(/{/g) || []).length; braceCount -= (catchLine.match(/}/g) || []).length; } if ( catchStarted && catchLine !== "{" && catchLine !== "}" && catchLine !== "" && !catchLine.startsWith("//") && !catchLine.startsWith("*") && !catchLine.startsWith("/*") ) { hasContent = true; break; } if (braceCount <= 0 && catchStarted) break; j++; } if (!hasContent) { results.push({ line: i + 1, column: line.indexOf("catch"), type: "exception", severity: "error", message: "空的 catch 块,必须记录日志或处理异常", rule: "empty-catch", }); } } return results; } export function formatCheckResults(results: CheckResult[]): string { if (results.length === 0) return "金蝶代码检查通过,未发现问题。"; const errors = results.filter((result) => result.severity === "error"); const warnings = results.filter((result) => result.severity === "warning"); const infos = results.filter((result) => result.severity === "info"); const lines = [ `金蝶代码检查发现 ${results.length} 个问题:`, `错误:${errors.length}`, `警告:${warnings.length}`, `信息:${infos.length}`, "", ]; for (const result of results) { lines.push(`- 第 ${result.line} 行,第 ${result.column + 1} 列:[${result.severity}] ${result.type} ${result.message} (${result.rule})`); } return lines.join("\n"); } interface LineRange { start: number; end: number; } function makeResult( index: number, line: string, token: string, type: CheckType, severity: CheckSeverity, message: string, rule: string, ): CheckResult { return { line: index + 1, column: Math.max(0, line.indexOf(token)), type, severity, message, rule, }; } function findMethodRanges(lines: string[], methodName: string): LineRange[] { const ranges: LineRange[] = []; const pattern = new RegExp(`\\b${methodName}\\s*\\(`); for (let i = 0; i < lines.length; i++) { if (!pattern.test(lines[i])) continue; const end = findBlockEnd(lines, i); ranges.push({ start: i, end }); } return ranges; } function findBlockEnd(lines: string[], start: number): number { let braceCount = 0; let seenOpen = false; for (let i = start; i < lines.length; i++) { for (const char of lines[i]) { if (char === "{") { seenOpen = true; braceCount++; } if (char === "}") braceCount--; } if (seenOpen && braceCount <= 0) return i; } return start; } function forEachLineInRange(lines: string[], range: LineRange, fn: (line: string, index: number) => void): void { for (let i = range.start; i <= range.end; i++) { if (!isCommentLine(lines[i])) fn(lines[i], i); } } function nearbyLineHas(lines: string[], index: number, offsetStart: number, pattern: RegExp): boolean { const start = Math.max(0, index + offsetStart); for (let i = start; i <= index; i++) { if (pattern.test(lines[i])) return true; } return false; } function isInsideLoop(lines: string[], index: number): boolean { for (let i = index; i >= Math.max(0, index - 20); i--) { if (/\b(for|while)\s*\(/.test(lines[i])) { const end = findBlockEnd(lines, i); return index <= end; } } return false; } function collectStatement(lines: string[], start: number, maxExtraLines: number): string { const parts: string[] = []; for (let i = start; i < Math.min(lines.length, start + maxExtraLines + 1); i++) { parts.push(lines[i]); if (/[;{}]\s*$/.test(lines[i].trim())) break; } return parts.join("\n"); } function extractCallArgs(statement: string, callPattern: RegExp): string[] { const match = callPattern.exec(statement); if (!match || match.index === undefined) return []; const open = statement.indexOf("(", match.index); if (open < 0) return []; const args: string[] = []; let depth = 0; let current = ""; let quote: "'" | '"' | undefined; for (let i = open + 1; i < statement.length; i++) { const ch = statement[i]; const prev = statement[i - 1]; if (quote) { current += ch; if (ch === quote && prev !== "\\") quote = undefined; continue; } if (ch === "'" || ch === '"') { quote = ch; current += ch; continue; } if (ch === "(" || ch === "[" || ch === "{") { depth++; current += ch; continue; } if (ch === ")" && depth === 0) { if (current.trim()) args.push(current.trim()); return args; } if (ch === ")" || ch === "]" || ch === "}") { depth--; current += ch; continue; } if (ch === "," && depth === 0) { args.push(current.trim()); current = ""; continue; } current += ch; } return []; } function looksLikeRowIndexedViewCall(args: string[]): boolean { if (args.length < 3) return false; const second = args[1].trim(); return /^(rowIndex|index|idx|i|row|entryIndex|\d+)$/.test(second) || /\bgetRowIndex\s*\(/.test(second); } function isCommentLine(line: string): boolean { const trimmed = line.trim(); return trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*"); } function isPascalCase(name: string): boolean { return /^[A-Z][a-zA-Z0-9]*$/.test(name); } function isCamelCase(name: string): boolean { return /^[a-z][a-zA-Z0-9]*$/.test(name); } function isUpperSnakeCase(name: string): boolean { return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name); } function isCommonString(value: string): boolean { return [ "UTF-8", "GBK", "ISO-8859-1", "ASCII", "true", "false", "null", "undefined", "GET", "POST", "PUT", "DELETE", "JSON", "XML", "HTML", "TEXT", "application/json", "text/html", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", ].includes(value); } function isLogMessage(line: string): boolean { return ( /\b(log|logger|LOG)\s*\.\s*(debug|info|warn|error|trace)\s*\(/.test(line) || /\b(System\s*\.\s*out\s*\.\s*println)\s*\(/.test(line) || /\b(console\s*\.\s*log)\s*\(/.test(line) ); } function isAnnotation(line: string): boolean { return line.trim().startsWith("@") || /@\w+\s*\(/.test(line); } function isBusinessConstant(value: string): boolean { return ( /^[A-Z][A-Z_]+$/.test(value) || /^[A-Z]+_[A-Z]+$/.test(value) || /^(DRAFT|SUBMIT|APPROVE|CLOSE|VOID|AUDIT)/.test(value) || /^(SUCCESS|FAIL|ERROR|PENDING)/.test(value) ); }