- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types) - references/: tool constraints + error handling (self-contained) - scripts/setup.py: one-click install/config/verify - SKILL.md: full command reference + dual-platform install guide - VERSION: 2.1.0 Built from D:/Codespace/sap-cli via scripts/pack_skill.py
127 lines
4.3 KiB
Python
127 lines
4.3 KiB
Python
"""依赖分析命令: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)
|