| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- #!/usr/bin/env python3
- """
- IRIS代码规范检查脚本
- 用于检查InterSystems IRIS/Cache ObjectScript代码是否符合普瑞云HIS开发规范
- """
- import os
- import re
- import sys
- from pathlib import Path
- from typing import List, Dict, Tuple, Optional
- class IRISCodeChecker:
- """IRIS代码规范检查器"""
- def __init__(self, base_dir: str):
- self.base_dir = Path(base_dir)
- self.errors = []
- self.warnings = []
- # 业务包列表
- self.business_packages = [
- 'Decoct', 'Pharmacy', 'Inventory', 'Inpatient',
- 'Outpatient', 'Emergency', 'MedicalRecord', 'Registration',
- 'Billing', 'Insurance', 'Laboratory', 'Radiology',
- 'BloodTransfusion', 'Nutrition', 'Nursing', 'Surgery', 'Admin'
- ]
- # CB表必填字段
- self.cb_required_fields = [
- 'Code', 'Descripts', 'ENDescripts',
- 'StartDate', 'StopDate',
- 'CreateDate', 'CreateUserDr'
- ]
- def check_file(self, file_path: Path) -> None:
- """检查单个文件"""
- print(f"正在检查文件: {file_path}")
- try:
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
- content = f.read()
- # 判断文件类型
- class_name, package = self.extract_class_info(content)
- print(f" 类名: {class_name}, 包: {package}")
- if not class_name:
- print(f" 跳过: 未找到类定义")
- return
- # 检查规范
- if package and package == 'User':
- print(f" 检查表类规范...")
- self.check_table_class(content, file_path)
- elif package and any(bp in package for bp in self.business_packages):
- print(f" 检查业务类规范...")
- self.check_business_class(content, file_path)
- else:
- print(f" 跳过: 未识别的包类型")
- except Exception as e:
- print(f" 错误: {e}")
- self.warnings.append(f"{file_path}: 无法读取文件 - {e}")
- def extract_class_info(self, content: str) -> Tuple[Optional[str], Optional[str]]:
- """提取类名和包名"""
- match = re.search(r'Class\s+(\w+)\.(\w+)', content)
- if match:
- return match.group(2), match.group(1)
- return None, None
- def check_table_class(self, content: str, file_path: Path) -> None:
- """检查表类规范"""
- # 1. 检查是否在User包下
- if not re.search(r'Class\s+User\.\w+', content):
- self.errors.append(f"{file_path}: 表类必须保存到User包下")
- # 2. 检查表名前缀
- class_name_match = re.search(r'Class\s+User\.(CB\w+|HB\w+|BS\w+)', content)
- if not class_name_match:
- self.warnings.append(f"{file_path}: 表名应使用CB/HB/BS前缀")
- else:
- table_prefix = class_name_match.group(1)[:2]
- # 3. CB表必填字段检查
- if table_prefix == 'CB':
- missing_fields = []
- for field in self.cb_required_fields:
- if not re.search(rf'Property\s+{field}', content):
- missing_fields.append(field)
- if missing_fields:
- self.errors.append(
- f"{file_path}: CB表缺少必填字段: {', '.join(missing_fields)}"
- )
- def check_business_class(self, content: str, file_path: Path) -> None:
- """检查业务类规范"""
- # 1. 检查是否使用正确的包名
- has_valid_package = False
- for bp in self.business_packages:
- if f'src.{bp}.' in content or f'class src.{bp}.' in content.lower():
- has_valid_package = True
- break
- if not has_valid_package:
- self.warnings.append(f"{file_path}: 业务类应使用标准业务包")
- # 2. 检查方法注释
- methods = re.findall(r'ClassMethod\s+(\w+)\s*\(', content)
- for method in methods:
- # 检查方法上方是否有注释
- method_pattern = rf'ClassMethod\s+{method}\s*\('
- match = re.search(method_pattern, content)
- if match:
- # 获取方法位置前的内容
- pre_content = content[:match.start()]
- # 检查最后3行是否有注释
- lines_before = pre_content.split('\n')[-3:]
- has_comment = any('///' in line for line in lines_before)
- if not has_comment:
- self.warnings.append(f"{file_path}: 方法 '{method}' 缺少注释")
- # 3. 检查try-catch使用
- for method in methods:
- method_pattern = rf'ClassMethod\s+{method}\s*\([^)]*\)\s+As\s+\w+.*?{{(.*?)}}'
- method_match = re.search(method_pattern, content, re.DOTALL)
- if method_match:
- method_body = method_match.group(1)
- # 检查是否包含数据操作
- if re.search(r'operatetable\.(Insert|Update|Delete|GetRow)', method_body):
- if 'Try' not in method_body or 'Catch' not in method_body:
- self.errors.append(
- f"{file_path}: 方法 '{method}' 包含数据操作但未使用try-catch"
- )
- # 4. 检查调试语句
- if re.search(r'^\s*["\']\s*w\s+', content, re.MULTILINE):
- self.errors.append(f"{file_path}: 代码中包含调试语句(w \"xxx\"),请删除")
- if re.search(r'\bWRITE\s+', content, re.IGNORECASE):
- self.errors.append(f"{file_path}: 代码中包含调试语句(WRITE),请删除")
- # 5. 检查事务处理
- for method in methods:
- method_pattern = rf'ClassMethod\s+{method}\s*\([^)]*\)\s+As\s+\w+.*?{{(.*?)}}'
- method_match = re.search(method_pattern, content, re.DOTALL)
- if method_match:
- method_body = method_match.group(1)
- # 检查TSTART和TCOMMIT
- has_tstart = re.search(r'\bTSTART\b', method_body)
- has_tcommit = re.search(r'\bTCOMMIT\b', method_body)
- has_trollback = re.search(r'\bTROLLBACK\b', method_body)
- if has_tstart:
- if not has_tcommit:
- self.errors.append(
- f"{file_path}: 方法 '{method}' 使用了TSTART但缺少TCOMMIT"
- )
- if not has_trollback:
- self.warnings.append(
- f"{file_path}: 方法 '{method}' 使用了TSTART但缺少TROLLBACK"
- )
- if has_tcommit and not has_tstart:
- self.errors.append(
- f"{file_path}: 方法 '{method}' 使用了TCOMMIT但缺少TSTART"
- )
- if has_trollback and not has_tstart:
- self.errors.append(
- f"{file_path}: 方法 '{method}' 使用了TROLLBACK但缺少TSTART"
- )
- # 6. 检查Query定义
- queries = re.findall(r'Query\s+(\w+)\s*\([^)]*\)\s+As\s+%Query\(ROWSPEC\s*=\s*"[^"]+"\)', content)
- for query in queries:
- # 检查Execute方法
- if not re.search(rf'ClassMethod\s+{query}Execute\(', content):
- self.errors.append(f"{file_path}: Query '{query}' 缺少Execute方法")
- # 检查Fetch方法
- if not re.search(rf'ClassMethod\s+{query}Fetch\(', content):
- self.errors.append(f"{file_path}: Query '{query}' 缺少Fetch方法")
- # 检查Close方法
- if not re.search(rf'ClassMethod\s+{query}Close\(', content):
- self.errors.append(f"{file_path}: Query '{query}' 缺少Close方法")
- # 7. 检查禁止使用SQL语句提取数据
- sql_patterns = [
- r'&sql\s*\(', # &sql() 嵌入式SQL
- r'##class\s*\(\s*%SQL\.Statement\s*\)', # %SQL.Statement
- r'##class\s*\(\s*%ResultSet\s*\)', # %ResultSet
- r'\.%Prepare\s*\(', # %Prepare 方法调用
- r'\.%Execute\s*\(', # %Execute 方法调用(SQL上下文)
- ]
- for pattern in sql_patterns:
- if re.search(pattern, content, re.IGNORECASE):
- self.errors.append(
- f"{file_path}: 禁止使用SQL语句提取数据,只能通过Global提取数据"
- )
- break
- # 8. 检查SQL注入风险
- # 检查字符串拼接SQL
- sql_concat_patterns = [
- r'Set\s+tSQL\s*=\s*[^;]*?\.\s*\w+', # 字符串拼接
- r'Prepare\([^)]*\+\s*\w+', # Prepare中使用拼接
- ]
- for pattern in sql_concat_patterns:
- if re.search(pattern, content):
- self.errors.append(
- f"{file_path}: 检测到可能的SQL注入风险(字符串拼接SQL)"
- )
- break
- # 9. 检查外键命名
- # 检查是否以Dr结尾的外键字段
- property_matches = re.findall(r'Property\s+(\w+)\s+As\s+([A-Z]\w+\.[A-Z]\w+)', content)
- for prop_name, prop_type in property_matches:
- # 如果是外键(引用其他类),检查是否以Dr结尾
- if '.' in prop_type:
- if not prop_name.endswith('Dr'):
- self.warnings.append(
- f"{file_path}: 外键字段 '{prop_name}' 应以Dr结尾"
- )
- def check_directory(self, directory: Path) -> None:
- """递归检查目录"""
- if not directory.exists():
- print(f"目录不存在: {directory}")
- return
- for file_path in directory.rglob('*.cls'):
- if file_path.is_file():
- self.check_file(file_path)
- def print_results(self) -> None:
- """打印检查结果"""
- print("=" * 80)
- print("IRIS代码规范检查结果")
- print("=" * 80)
- if not self.errors and not self.warnings:
- print("\n✅ 所有检查通过!代码符合规范。\n")
- return
- # 打印错误
- if self.errors:
- print(f"\n❌ 发现 {len(self.errors)} 个错误:\n")
- for i, error in enumerate(self.errors, 1):
- print(f" [{i}] {error}")
- # 打印警告
- if self.warnings:
- print(f"\n⚠️ 发现 {len(self.warnings)} 个警告:\n")
- for i, warning in enumerate(self.warnings, 1):
- print(f" [{i}] {warning}")
- # 打印统计
- print("\n" + "=" * 80)
- print(f"统计: {len(self.errors)} 个错误, {len(self.warnings)} 个警告")
- print("=" * 80)
- def has_errors(self) -> bool:
- """是否有错误"""
- return len(self.errors) > 0
- def main():
- """主函数"""
- print("脚本启动...")
- # 获取项目根目录
- script_dir = Path(__file__).parent
- print(f"脚本目录: {script_dir}")
- skill_dir = script_dir.parent
- project_dir = skill_dir.parent
- default_src_dir = project_dir / "src"
- # 解析命令行参数
- import argparse
- parser = argparse.ArgumentParser(description='IRIS代码规范检查脚本')
- parser.add_argument('path', nargs='?', default=str(default_src_dir),
- help='要检查的文件或目录路径(默认:src目录)')
- args = parser.parse_args()
- target_path = Path(args.path)
- print(f"目标路径: {target_path}")
- print(f"路径存在: {target_path.exists()}")
- print(f"是文件: {target_path.is_file()}")
- print(f"是目录: {target_path.is_dir()}")
- if not target_path.exists():
- print(f"错误: 路径不存在: {target_path}")
- sys.exit(1)
- print()
- print("=" * 80)
- print("IRIS代码规范检查")
- print("=" * 80)
- print()
- # 创建检查器
- checker = IRISCodeChecker(str(target_path.parent))
- # 检查代码
- if target_path.is_file():
- checker.check_file(target_path)
- elif target_path.is_dir():
- checker.check_directory(target_path)
- # 打印结果
- print()
- checker.print_results()
- # 返回退出码
- sys.exit(1 if checker.has_errors() else 0)
- if __name__ == "__main__":
- main()
|