"""依赖分析命令:analyze。""" from __future__ import annotations import argparse import logging import os import re from sapcli.client import ADTClient logger = logging.getLogger("sapcli.commands.analyze") # ABAP 依赖模式 _DEPENDENCY_PATTERNS: list[tuple[str, re.Pattern[str]]] = [ ("TYPE REF TO", re.compile(r"TYPE\s+REF\s+TO\s+(\w+)", re.IGNORECASE)), ("CALL METHOD", re.compile(r"CALL\s+METHOD\s+(\w+)", re.IGNORECASE)), ("CALL METHOD (static)", re.compile(r"(\w+)=>(\w+)", re.IGNORECASE)), ("CREATE OBJECT", re.compile(r"CREATE\s+OBJECT\s+\w+\s+TYPE\s+(\w+)", re.IGNORECASE)), ("PERFORM", re.compile(r"PERFORM\s+(\w+)", re.IGNORECASE)), ("CALL FUNCTION", re.compile(r"CALL\s+FUNCTION\s+\'(\w+)\'", re.IGNORECASE)), ("SELECT ... FROM", re.compile(r"FROM\s+(\w+)", re.IGNORECASE)), ("LIKE", re.compile(r"LIKE\s+(\w+)", re.IGNORECASE)), ("TYPE", re.compile(r"TYPE\s+(\w+)", re.IGNORECASE)), ] # 已知的 ABAP 内置类型(不需要追踪) _BUILTIN_TYPES = frozenset({ "STRING", "CHAR", "NUMC", "INT1", "INT2", "INT4", "INT8", "FLOAT", "DECFLOAT16", "DECFLOAT34", "DEC", "CURR", "QUAN", "RAW", "LRAW", "RAWSTRING", "DATS", "TIMS", "SSTRING", "XSTRING", "X", "C", "N", "D", "T", "I", "F", "P", "ANY", "DATA", "REF", "OBJECT", "STRUCTURE", "TABLE", "STANDARD", "SORTED", "HASHED", "INDEX", "SY", "ABAP_BOOL", "ABAP_TRUE", "ABAP_FALSE", "BOOLEAN", "XSDBOOLEAN", "FLAG", "CHAR1", "CHAR10", }) def cmd_analyze(args: argparse.Namespace, client: ADTClient) -> None: """依赖自动分析。 解析本地 .abap 文件中的 TYPE REF TO / CALL METHOD / PERFORM 等, 自动生成 depends_on 列表。 """ project_path: str = getattr(args, "path", ".") print("=" * 60) print(" sap-cli 依赖分析") print("=" * 60) print(f" 项目路径: {project_path}") if not os.path.isdir(project_path): print(f"\n ✗ 目录不存在: {project_path}") return # 扫描所有 .abap 文件 abap_files: list[str] = [] for root, dirs, files in os.walk(project_path): for fname in files: if fname.endswith(".abap"): abap_files.append(os.path.join(root, fname)) if not abap_files: print("\n ℹ 未找到 .abap 文件") return print(f"\n → 扫描到 {len(abap_files)} 个 .abap 文件\n") # 分析每个文件 all_deps: dict[str, dict[str, list[str]]] = {} for filepath in abap_files: rel_path = os.path.relpath(filepath, project_path) file_base = os.path.splitext(os.path.basename(filepath))[0].upper() with open(filepath, "r", encoding="utf-8") as f: source = f.read() deps = _analyze_dependencies(source) all_deps[file_base] = {"file": rel_path, "deps": deps} if deps: print(f" 📄 {rel_path}") print(f" → {', '.join(deps)}") else: print(f" 📄 {rel_path} (无外部依赖)") # 汇总 print(f"\n {'─' * 50}") total_deps = sum(len(v["deps"]) for v in all_deps.values()) files_with_deps = sum(1 for v in all_deps.values() if v["deps"]) print(f" 总计: {len(abap_files)} 个文件, {files_with_deps} 个有外部依赖, {total_deps} 个依赖关系") # 输出 depends_on 配置建议 if total_deps > 0: print(f"\n 建议的 manifest.json depends_on 配置:") print(f" {'─' * 50}") for name, info in sorted(all_deps.items()): if info["deps"]: deps_str = ", ".join(f'"{d}"' for d in sorted(info["deps"])) print(f' "{name}": [{deps_str}]') def _analyze_dependencies(source: str) -> list[str]: """分析 ABAP 源码中的依赖关系。 Args: source: ABAP 源码字符串。 Returns: 依赖对象名称列表(去重)。 """ deps: set[str] = set() for pattern_name, pattern in _DEPENDENCY_PATTERNS: for match in pattern.finditer(source): name = match.group(1).upper() # 过滤内置类型和短名称 if name in _BUILTIN_TYPES: continue if len(name) < 3: continue # 只追踪 Z/Y 开头的自定义对象(更精确) if name.startswith("Z") or name.startswith("Y"): deps.add(name) return sorted(deps)