"""S04 只能持久化自己的结构定义,实例值由具名业务 owner 保存。""" import ast import re from importlib.util import resolve_name from pathlib import Path import pytest @pytest.mark.case_id( "NC-meta-owner-scope", environment="离线架构检查", given="当前元数据模块的导入与 SQL", when="检查实际源码访问归属", then=["仅持久化 metadata 自有定义,不导入业务内部或构造动态表名"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) def test_元数据仅访问自有定义且无动态表名__a40020() -> None: 根 = Path(__file__).resolve().parents[2] / "src/muse/元数据" 数据表 = set() for 文件 in 根.glob("*.py"): 问题, 表 = _检查元数据来源(文件.read_text(), 文件.name) assert 问题 == [], f"{文件.name}: {问题}" 数据表.update(表) assert "metadata.schema_version" in 数据表 assert "metadata.schema_binding" in 数据表 def _检查元数据来源(代码: str, 文件名: str) -> tuple[list[str], set[str]]: 树 = ast.parse(代码) 别名, 赋值 = {}, {} 问题, 数据表 = [], set() for 节点 in ast.walk(树): if isinstance(节点, ast.Import): for 项 in 节点.names: 别名[项.asname or 项.name.split(".")[0]] = ( 项.name if 项.asname else 项.name.split(".")[0] ) if 项.name.startswith("muse.") and not 项.name.startswith( ("muse.共享.", "muse.元数据.") ): 问题.append("越界导入") elif isinstance(节点, ast.ImportFrom): 来源 = ( resolve_name("." * 节点.level + (节点.module or ""), "muse.元数据") if 节点.level else (节点.module or "") ) for 项 in 节点.names: 全名 = 来源 + "." + 项.name 别名[项.asname or 项.name] = 全名 if 全名.startswith("muse.") and not 全名.startswith(("muse.共享.", "muse.元数据.")): 问题.append("越界导入") elif isinstance(节点, ast.Assign): for 目标 in 节点.targets: if isinstance(目标, ast.Name): 赋值[目标.id] = 节点.value def 名称(节点, 已见=frozenset()): if isinstance(节点, ast.Name): if 节点.id in 赋值 and 节点.id not in 已见: return 名称(赋值[节点.id], 已见 | {节点.id}) return 别名.get(节点.id, 节点.id) if isinstance(节点, ast.Attribute): return 名称(节点.value, 已见) + "." + 节点.attr return "" def 文本(节点, 已见=frozenset()): if isinstance(节点, ast.Constant) and isinstance(节点.value, str): return 节点.value if isinstance(节点, ast.Name) and 节点.id in 赋值 and 节点.id not in 已见: return 文本(赋值[节点.id], 已见 | {节点.id}) if isinstance(节点, ast.BinOp) and isinstance(节点.op, ast.Add): 左, 右 = 文本(节点.left, 已见), 文本(节点.right, 已见) return 左 + 右 if 左 is not None and 右 is not None else None if isinstance(节点, ast.JoinedStr): return "".join( 值.value if isinstance(值, ast.Constant) else "{动态}" for 值 in 节点.values ) return None for 节点 in ast.walk(树): if isinstance(节点, ast.Attribute): 来源 = 名称(节点) if 来源.startswith("muse.") and not 来源.startswith(("muse.共享.", "muse.元数据.")): 问题.append("越界导入访问") if isinstance(节点, ast.Call) and 名称(节点.func) == "psycopg.sql.Identifier": 问题.append("不得接受动态表列名") 值 = 文本(节点) if 值 is None: continue 表 = re.findall(r"\b(?:FROM|JOIN|INSERT\s+INTO|UPDATE(?!\s+SET))\s+([^\s(,;]+)", 值, re.I) for 名 in 表: if 文件名 != "存储.py": 问题.append("绕过S04存储归属") if not re.fullmatch(r"metadata\.[a-z_][a-z0-9_]*", 名): 问题.append("越界或动态表名") 数据表.add(名) return 问题, 数据表 @pytest.mark.case_id( "NC-o04-metadata-owner-counterexamples", environment="离线,临时元数据Python源文件", given="绝对/相对导入、SQL别名、拼接和动态表名的绕过形式", when="使用与真实源码相同的AST归属检查", then=["越界写与动态表名拒绝,合法metadata静态查询不误报"], ) @pytest.mark.parametrize( "代码", [ "import muse.正文写作.存储 as business", "from ..正文写作 import 存储 as business", "from muse import 正文写作", "import muse as m; m.正文写作.存储.写()", "from psycopg.sql import Identifier as ID; ID('muse_work')", "import psycopg.sql as s; s.Identifier('muse_work')", "from psycopg import sql; ident=sql.Identifier; ident('muse_work')", "sql = 'UP' + 'DATE muse_work SET payload=%s'", "table='muse_work'; sql=f'UPDATE {table} SET payload=%s'", "sql='DELETE FROM muse_work WHERE work_id=%s'", ], ids=[ "import", "relative", "from-root", "root-alias", "identifier-import", "identifier-alias", "identifier-assignment", "concat", "fstring", "delete", ], ) def test_元数据归属门禁拒绝AST绕过(tmp_path, 代码): 文件 = tmp_path / "存储.py" 文件.write_text(代码) assert _检查元数据来源(文件.read_text(), 文件.name)[0] assert ( _检查元数据来源( "sql='SELECT definition FROM metadata.schema_version WHERE type_id=%s'", "存储.py" )[0] == [] ) assert _检查元数据来源("sql='SELECT definition FROM metadata.schema_version'", "接口.py")[0]