check-iris-code.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python3
  2. """
  3. IRIS代码规范检查脚本
  4. 用于检查InterSystems IRIS/Cache ObjectScript代码是否符合普瑞云HIS开发规范
  5. """
  6. import os
  7. import re
  8. import sys
  9. from pathlib import Path
  10. from typing import List, Dict, Tuple, Optional
  11. class IRISCodeChecker:
  12. """IRIS代码规范检查器"""
  13. def __init__(self, base_dir: str):
  14. self.base_dir = Path(base_dir)
  15. self.errors = []
  16. self.warnings = []
  17. # 业务包列表
  18. self.business_packages = [
  19. 'Decoct', 'Pharmacy', 'Inventory', 'Inpatient',
  20. 'Outpatient', 'Emergency', 'MedicalRecord', 'Registration',
  21. 'Billing', 'Insurance', 'Laboratory', 'Radiology',
  22. 'BloodTransfusion', 'Nutrition', 'Nursing', 'Surgery', 'Admin'
  23. ]
  24. # CB表必填字段
  25. self.cb_required_fields = [
  26. 'Code', 'Descripts', 'ENDescripts',
  27. 'StartDate', 'StopDate',
  28. 'CreateDate', 'CreateUserDr'
  29. ]
  30. def check_file(self, file_path: Path) -> None:
  31. """检查单个文件"""
  32. print(f"正在检查文件: {file_path}")
  33. try:
  34. with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
  35. content = f.read()
  36. # 判断文件类型
  37. class_name, package = self.extract_class_info(content)
  38. print(f" 类名: {class_name}, 包: {package}")
  39. if not class_name:
  40. print(f" 跳过: 未找到类定义")
  41. return
  42. # 检查规范
  43. if package and package == 'User':
  44. print(f" 检查表类规范...")
  45. self.check_table_class(content, file_path)
  46. elif package and any(bp in package for bp in self.business_packages):
  47. print(f" 检查业务类规范...")
  48. self.check_business_class(content, file_path)
  49. else:
  50. print(f" 跳过: 未识别的包类型")
  51. except Exception as e:
  52. print(f" 错误: {e}")
  53. self.warnings.append(f"{file_path}: 无法读取文件 - {e}")
  54. def extract_class_info(self, content: str) -> Tuple[Optional[str], Optional[str]]:
  55. """提取类名和包名"""
  56. match = re.search(r'Class\s+(\w+)\.(\w+)', content)
  57. if match:
  58. return match.group(2), match.group(1)
  59. return None, None
  60. def check_table_class(self, content: str, file_path: Path) -> None:
  61. """检查表类规范"""
  62. # 1. 检查是否在User包下
  63. if not re.search(r'Class\s+User\.\w+', content):
  64. self.errors.append(f"{file_path}: 表类必须保存到User包下")
  65. # 2. 检查表名前缀
  66. class_name_match = re.search(r'Class\s+User\.(CB\w+|HB\w+|BS\w+)', content)
  67. if not class_name_match:
  68. self.warnings.append(f"{file_path}: 表名应使用CB/HB/BS前缀")
  69. else:
  70. table_prefix = class_name_match.group(1)[:2]
  71. # 3. CB表必填字段检查
  72. if table_prefix == 'CB':
  73. missing_fields = []
  74. for field in self.cb_required_fields:
  75. if not re.search(rf'Property\s+{field}', content):
  76. missing_fields.append(field)
  77. if missing_fields:
  78. self.errors.append(
  79. f"{file_path}: CB表缺少必填字段: {', '.join(missing_fields)}"
  80. )
  81. def check_business_class(self, content: str, file_path: Path) -> None:
  82. """检查业务类规范"""
  83. # 1. 检查是否使用正确的包名
  84. has_valid_package = False
  85. for bp in self.business_packages:
  86. if f'src.{bp}.' in content or f'class src.{bp}.' in content.lower():
  87. has_valid_package = True
  88. break
  89. if not has_valid_package:
  90. self.warnings.append(f"{file_path}: 业务类应使用标准业务包")
  91. # 2. 检查方法注释
  92. methods = re.findall(r'ClassMethod\s+(\w+)\s*\(', content)
  93. for method in methods:
  94. # 检查方法上方是否有注释
  95. method_pattern = rf'ClassMethod\s+{method}\s*\('
  96. match = re.search(method_pattern, content)
  97. if match:
  98. # 获取方法位置前的内容
  99. pre_content = content[:match.start()]
  100. # 检查最后3行是否有注释
  101. lines_before = pre_content.split('\n')[-3:]
  102. has_comment = any('///' in line for line in lines_before)
  103. if not has_comment:
  104. self.warnings.append(f"{file_path}: 方法 '{method}' 缺少注释")
  105. # 3. 检查try-catch使用
  106. for method in methods:
  107. method_pattern = rf'ClassMethod\s+{method}\s*\([^)]*\)\s+As\s+\w+.*?{{(.*?)}}'
  108. method_match = re.search(method_pattern, content, re.DOTALL)
  109. if method_match:
  110. method_body = method_match.group(1)
  111. # 检查是否包含数据操作
  112. if re.search(r'operatetable\.(Insert|Update|Delete|GetRow)', method_body):
  113. if 'Try' not in method_body or 'Catch' not in method_body:
  114. self.errors.append(
  115. f"{file_path}: 方法 '{method}' 包含数据操作但未使用try-catch"
  116. )
  117. # 4. 检查调试语句
  118. if re.search(r'^\s*["\']\s*w\s+', content, re.MULTILINE):
  119. self.errors.append(f"{file_path}: 代码中包含调试语句(w \"xxx\"),请删除")
  120. if re.search(r'\bWRITE\s+', content, re.IGNORECASE):
  121. self.errors.append(f"{file_path}: 代码中包含调试语句(WRITE),请删除")
  122. # 5. 检查事务处理
  123. for method in methods:
  124. method_pattern = rf'ClassMethod\s+{method}\s*\([^)]*\)\s+As\s+\w+.*?{{(.*?)}}'
  125. method_match = re.search(method_pattern, content, re.DOTALL)
  126. if method_match:
  127. method_body = method_match.group(1)
  128. # 检查TSTART和TCOMMIT
  129. has_tstart = re.search(r'\bTSTART\b', method_body)
  130. has_tcommit = re.search(r'\bTCOMMIT\b', method_body)
  131. has_trollback = re.search(r'\bTROLLBACK\b', method_body)
  132. if has_tstart:
  133. if not has_tcommit:
  134. self.errors.append(
  135. f"{file_path}: 方法 '{method}' 使用了TSTART但缺少TCOMMIT"
  136. )
  137. if not has_trollback:
  138. self.warnings.append(
  139. f"{file_path}: 方法 '{method}' 使用了TSTART但缺少TROLLBACK"
  140. )
  141. if has_tcommit and not has_tstart:
  142. self.errors.append(
  143. f"{file_path}: 方法 '{method}' 使用了TCOMMIT但缺少TSTART"
  144. )
  145. if has_trollback and not has_tstart:
  146. self.errors.append(
  147. f"{file_path}: 方法 '{method}' 使用了TROLLBACK但缺少TSTART"
  148. )
  149. # 6. 检查Query定义
  150. queries = re.findall(r'Query\s+(\w+)\s*\([^)]*\)\s+As\s+%Query\(ROWSPEC\s*=\s*"[^"]+"\)', content)
  151. for query in queries:
  152. # 检查Execute方法
  153. if not re.search(rf'ClassMethod\s+{query}Execute\(', content):
  154. self.errors.append(f"{file_path}: Query '{query}' 缺少Execute方法")
  155. # 检查Fetch方法
  156. if not re.search(rf'ClassMethod\s+{query}Fetch\(', content):
  157. self.errors.append(f"{file_path}: Query '{query}' 缺少Fetch方法")
  158. # 检查Close方法
  159. if not re.search(rf'ClassMethod\s+{query}Close\(', content):
  160. self.errors.append(f"{file_path}: Query '{query}' 缺少Close方法")
  161. # 7. 检查禁止使用SQL语句提取数据
  162. sql_patterns = [
  163. r'&sql\s*\(', # &sql() 嵌入式SQL
  164. r'##class\s*\(\s*%SQL\.Statement\s*\)', # %SQL.Statement
  165. r'##class\s*\(\s*%ResultSet\s*\)', # %ResultSet
  166. r'\.%Prepare\s*\(', # %Prepare 方法调用
  167. r'\.%Execute\s*\(', # %Execute 方法调用(SQL上下文)
  168. ]
  169. for pattern in sql_patterns:
  170. if re.search(pattern, content, re.IGNORECASE):
  171. self.errors.append(
  172. f"{file_path}: 禁止使用SQL语句提取数据,只能通过Global提取数据"
  173. )
  174. break
  175. # 8. 检查SQL注入风险
  176. # 检查字符串拼接SQL
  177. sql_concat_patterns = [
  178. r'Set\s+tSQL\s*=\s*[^;]*?\.\s*\w+', # 字符串拼接
  179. r'Prepare\([^)]*\+\s*\w+', # Prepare中使用拼接
  180. ]
  181. for pattern in sql_concat_patterns:
  182. if re.search(pattern, content):
  183. self.errors.append(
  184. f"{file_path}: 检测到可能的SQL注入风险(字符串拼接SQL)"
  185. )
  186. break
  187. # 9. 检查外键命名
  188. # 检查是否以Dr结尾的外键字段
  189. property_matches = re.findall(r'Property\s+(\w+)\s+As\s+([A-Z]\w+\.[A-Z]\w+)', content)
  190. for prop_name, prop_type in property_matches:
  191. # 如果是外键(引用其他类),检查是否以Dr结尾
  192. if '.' in prop_type:
  193. if not prop_name.endswith('Dr'):
  194. self.warnings.append(
  195. f"{file_path}: 外键字段 '{prop_name}' 应以Dr结尾"
  196. )
  197. def check_directory(self, directory: Path) -> None:
  198. """递归检查目录"""
  199. if not directory.exists():
  200. print(f"目录不存在: {directory}")
  201. return
  202. for file_path in directory.rglob('*.cls'):
  203. if file_path.is_file():
  204. self.check_file(file_path)
  205. def print_results(self) -> None:
  206. """打印检查结果"""
  207. print("=" * 80)
  208. print("IRIS代码规范检查结果")
  209. print("=" * 80)
  210. if not self.errors and not self.warnings:
  211. print("\n✅ 所有检查通过!代码符合规范。\n")
  212. return
  213. # 打印错误
  214. if self.errors:
  215. print(f"\n❌ 发现 {len(self.errors)} 个错误:\n")
  216. for i, error in enumerate(self.errors, 1):
  217. print(f" [{i}] {error}")
  218. # 打印警告
  219. if self.warnings:
  220. print(f"\n⚠️ 发现 {len(self.warnings)} 个警告:\n")
  221. for i, warning in enumerate(self.warnings, 1):
  222. print(f" [{i}] {warning}")
  223. # 打印统计
  224. print("\n" + "=" * 80)
  225. print(f"统计: {len(self.errors)} 个错误, {len(self.warnings)} 个警告")
  226. print("=" * 80)
  227. def has_errors(self) -> bool:
  228. """是否有错误"""
  229. return len(self.errors) > 0
  230. def main():
  231. """主函数"""
  232. print("脚本启动...")
  233. # 获取项目根目录
  234. script_dir = Path(__file__).parent
  235. print(f"脚本目录: {script_dir}")
  236. skill_dir = script_dir.parent
  237. project_dir = skill_dir.parent
  238. default_src_dir = project_dir / "src"
  239. # 解析命令行参数
  240. import argparse
  241. parser = argparse.ArgumentParser(description='IRIS代码规范检查脚本')
  242. parser.add_argument('path', nargs='?', default=str(default_src_dir),
  243. help='要检查的文件或目录路径(默认:src目录)')
  244. args = parser.parse_args()
  245. target_path = Path(args.path)
  246. print(f"目标路径: {target_path}")
  247. print(f"路径存在: {target_path.exists()}")
  248. print(f"是文件: {target_path.is_file()}")
  249. print(f"是目录: {target_path.is_dir()}")
  250. if not target_path.exists():
  251. print(f"错误: 路径不存在: {target_path}")
  252. sys.exit(1)
  253. print()
  254. print("=" * 80)
  255. print("IRIS代码规范检查")
  256. print("=" * 80)
  257. print()
  258. # 创建检查器
  259. checker = IRISCodeChecker(str(target_path.parent))
  260. # 检查代码
  261. if target_path.is_file():
  262. checker.check_file(target_path)
  263. elif target_path.is_dir():
  264. checker.check_directory(target_path)
  265. # 打印结果
  266. print()
  267. checker.print_results()
  268. # 返回退出码
  269. sys.exit(1 if checker.has_errors() else 0)
  270. if __name__ == "__main__":
  271. main()