|
1 | | -"""静态架构门禁:内核依赖白名单与第一方 Python 导入环。 |
| 1 | +"""静态架构门禁:内核依赖白名单、顶层脚本角色与第一方 Python 导入环。 |
2 | 2 |
|
3 | 3 | 只解析 AST,不导入被检查代码。函数内、条件分支中的静态 import 同样检查; |
4 | 4 | 运行时拼接的动态加载不在静态保证范围内,本工具不是安全沙箱或 CodeGraph 替代。 |
|
11 | 11 | from pathlib import Path |
12 | 12 |
|
13 | 13 | # 明确的层依赖;stdlib 也受约束,纯模型不能悄悄导入 subprocess/宿主。 |
| 14 | +# 顶层 scripts/*.py 的显式角色登记:新脚本必须归类,不许"就是多了一个脚本"。 |
| 15 | +# entry=命令入口 / compat-shim=旧导入面兼容外观(docstring 必须含 Deprecated)/ |
| 16 | +# adapter=物化适配层 / support=共享工具层。内核包 codeguard.* 走 CORE_DEPENDENCIES, |
| 17 | +# 本表只管包外顶层脚本;两者共同构成可检查的分层边界。 |
| 18 | +SCRIPT_ROLES = { |
| 19 | + "check_architecture.py": "entry", |
| 20 | + "cve_check.py": "entry", |
| 21 | + "detect_lang.py": "entry", |
| 22 | + "dockerfile_security.py": "entry", |
| 23 | + "fix.py": "entry", |
| 24 | + "gen_language_docs.py": "entry", |
| 25 | + "java_project.py": "entry", |
| 26 | + "run_check.py": "entry", |
| 27 | + "validate_languages_json.py": "entry", |
| 28 | + "verdict.py": "compat-shim", |
| 29 | + "user_config.py": "compat-shim", |
| 30 | + "run_per_language.py": "compat-shim", |
| 31 | + "scope.py": "adapter", |
| 32 | + "git_snapshot.py": "adapter", |
| 33 | + "paths.py": "support", |
| 34 | +} |
| 35 | + |
| 36 | +# 适配层白名单:包外实现模块的用途声明(谁在依赖它由 CORE_DEPENDENCIES 约束)。 |
| 37 | +ADAPTER_LAYERS = { |
| 38 | + "scope.py": "linter 命令作用域物化 + git 改动集(delta 门禁共享层)", |
| 39 | + "git_snapshot.py": "只读 index/HEAD 物化一次性检查目录", |
| 40 | +} |
| 41 | + |
14 | 42 | CORE_DEPENDENCIES = { |
15 | 43 | "codeguard.cve_policy": {"__future__", "dataclasses"}, |
16 | 44 | "codeguard.cve_reports": {"__future__", "json", "math", "codeguard.cve_policy"}, |
@@ -192,9 +220,42 @@ def check(root: Path) -> list[str]: |
192 | 220 | graph[name].add(local) |
193 | 221 | if allowed is not None and target not in allowed and target.split(".")[0] not in allowed: |
194 | 222 | errors.append(f"{relative}:{line}: forbidden dependency: {name} -> {target}") |
| 223 | + errors.extend(_script_roles(root, modules)) |
195 | 224 | return errors + _cycles(graph) |
196 | 225 |
|
197 | 226 |
|
| 227 | +def _script_roles(root: Path, modules: dict) -> list[str]: |
| 228 | + """顶层 scripts/*.py 必须显式归类;compat-shim 必须带 Deprecated 通告; |
| 229 | + adapter 必须在 ADAPTER_LAYERS 声明用途。""" |
| 230 | + errors: list[str] = [] |
| 231 | + top_level = { |
| 232 | + path.name |
| 233 | + for path in (root / "scripts").glob("*.py") |
| 234 | + } |
| 235 | + for name in sorted(top_level - set(SCRIPT_ROLES)): |
| 236 | + errors.append(f"scripts/{name}:1: unclassified top-level script (add to SCRIPT_ROLES)") |
| 237 | + # 完备性校验只对真实插件仓布局生效:架构用例会在合成临时树上跑本检查, |
| 238 | + # 临时树不需要携带全部顶层脚本,不应误报 missing。 |
| 239 | + is_full_repo = (root / "scripts" / "check_architecture.py").is_file() |
| 240 | + if is_full_repo: |
| 241 | + for name in sorted(set(SCRIPT_ROLES) - top_level): |
| 242 | + errors.append(f"scripts/{name}:1: declared in SCRIPT_ROLES but missing on disk") |
| 243 | + for name, role in sorted(SCRIPT_ROLES.items()): |
| 244 | + path = root / "scripts" / name |
| 245 | + if not path.is_file(): |
| 246 | + continue |
| 247 | + if is_full_repo and role == "compat-shim": |
| 248 | + head = path.read_text(encoding="utf-8")[:400] |
| 249 | + if "Deprecated" not in head: |
| 250 | + errors.append(f"scripts/{name}:1: compat-shim must carry a Deprecated notice in its docstring") |
| 251 | + if role == "adapter" and is_full_repo and name not in ADAPTER_LAYERS: |
| 252 | + errors.append(f"scripts/{name}:1: adapter must declare its purpose in ADAPTER_LAYERS") |
| 253 | + if is_full_repo: |
| 254 | + for name in sorted(set(ADAPTER_LAYERS) - top_level): |
| 255 | + errors.append(f"scripts/{name}:1: declared in ADAPTER_LAYERS but missing on disk") |
| 256 | + return errors |
| 257 | + |
| 258 | + |
198 | 259 | def main() -> int: |
199 | 260 | parser = argparse.ArgumentParser(description=__doc__) |
200 | 261 | parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) |
|
0 commit comments