Files
wurangyu 38e99638c5 chore: sync skill to v2.2.1
- NW 7.40 E2E-verified compatibility matrix in SKILL.md
- DDIC fixes: include namespace, tabletype exists check, DDIC delete lock Accept
- New commands: clone, enhancement, history, unit-test
- 665 tests, 96% coverage
2026-07-10 19:13:46 +08:00

156 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""依赖排序模块。
使用 Kahn 算法对清单对象进行拓扑排序,
按 depends_on 显式依赖和 TYPE_PRIORITY 类型优先级决定执行顺序。
"""
from __future__ import annotations
import logging
from collections import deque
from sapcli.exceptions import CyclicDependencyError
from sapcli.manifest import ManifestEntry
logger = logging.getLogger("sapcli.sorter")
# 类型默认优先级(数值越小越先执行)
TYPE_PRIORITY: dict[str, int] = {
"domain": 10,
"dataelement": 20,
"structure": 25,
"table": 30,
"tabletype": 40,
"view": 42,
"lockobject": 44,
"searchhelp": 46,
"messageclass": 48,
"interface": 50,
"class": 60,
"functiongroup": 70,
"function": 75,
"include": 78,
"cdsview": 79,
"report": 80,
# DCL/DDLX 依赖 CDS View,排在 cdsview(79)/report(80) 之后
"dcl": 81,
"ddlX": 82,
}
# 未注册类型的默认优先级
_DEFAULT_PRIORITY = 90
def topological_sort(objects: list[ManifestEntry]) -> list[ManifestEntry]:
"""对清单对象进行拓扑排序。
1. 按 depends_on 建有向边(被依赖 → 当前对象)
2. 无入边的节点按 TYPE_PRIORITY 排序作为 tie-breaker
3. 检测循环依赖 → 抛出 CyclicDependencyError
Returns:
排序后的对象列表(依赖在前,被依赖在后...不,应该是依赖在前,被依赖的对象先执行)。
"""
if not objects:
return []
name_map: dict[str, ManifestEntry] = {obj.name: obj for obj in objects}
names = set(name_map.keys())
# ── 建图:计算入度 ──
in_degree: dict[str, int] = {name: 0 for name in names}
# adj[A] = [B, C, ...] 表示 A 完成后可以释放 B、C 的入度
adj: dict[str, list[str]] = {name: [] for name in names}
for obj in objects:
for dep in obj.depends_on:
if dep not in names:
# 依赖不在当前列表中,忽略(外部依赖)
logger.debug("忽略外部依赖: %s%s", obj.name, dep)
continue
# dep → objdep 先执行,obj 后执行)
adj[dep].append(obj.name)
in_degree[obj.name] += 1
# ── Kahn 算法 ──
# 初始化:入度为 0 的节点
ready: list[str] = [
name for name, deg in in_degree.items() if deg == 0
]
# 按 TYPE_PRIORITY 排序
ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
queue: deque[str] = deque(ready)
sorted_names: list[str] = []
while queue:
# 取出优先级最高(数值最小)的节点
# 由于 deque 不支持按优先级取,先排序再取
# 实际实现:每次从 ready 列表中取第一个
current = queue.popleft()
sorted_names.append(current)
# 释放后续节点
newly_ready: list[str] = []
for neighbor in adj[current]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
newly_ready.append(neighbor)
# 新就绪的节点按优先级排序后加入队列
newly_ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
queue.extend(newly_ready)
# ── 循环检测 ──
if len(sorted_names) != len(names):
remaining = names - set(sorted_names)
# 尝试找出循环链
cycle = _detect_cycle(remaining, adj, in_degree)
raise CyclicDependencyError(cycle)
result = [name_map[n] for n in sorted_names]
logger.info("拓扑排序完成: %d 个对象", len(result))
return result
def _priority(*, n_map: dict[str, ManifestEntry], n: str) -> int:
"""获取节点优先级数值。"""
obj_type = n_map[n].type if n in n_map else ""
return TYPE_PRIORITY.get(obj_type, _DEFAULT_PRIORITY)
def _detect_cycle(
remaining: set[str],
adj: dict[str, list[str]],
in_degree: dict[str, int],
) -> list[str]:
"""尝试在剩余节点中检测循环,返回循环中的节点名列表。"""
# 沿着入度 > 0 的边走,迟早会回到已访问的节点
if not remaining:
return []
visited: set[str] = set()
path: list[str] = []
start = next(iter(remaining))
current = start
for _ in range(len(remaining) + 1):
if current in visited:
# 找到循环起点
idx = path.index(current)
return path[idx:]
visited.add(current)
path.append(current)
# 找下一个仍在 remaining 中的后继
found_next = False
for neighbor in adj.get(current, []):
if neighbor in remaining:
current = neighbor
found_next = True
break
if not found_next:
break
# 无法确定精确循环链,返回所有剩余节点
return list(remaining)