refactor(impact): CodeParser 多语言就绪——语言适配器注册表(仅 Java,行为不变)

- BaseLanguageParser 抽象基类 + JavaLanguageParser(迁移全部 Java 正则,输出逐字节一致)
- LANGUAGE_PARSERS 注册表 + register_language_parser 扩展点(新增语言=加适配器类并注册,下游零改动)
- CodeParser 分发器:parse(root, language=None) 自动按扩展名探测 / 显式语言覆盖 / 多语言合并(language 逗号连接)/ 无源码或语言不支持抛 CodeParseError
- source_aggregator 新增 existing_system_language 透传;config.py 探测扩展名补 .py/.ts/.go/.cs
- 删除自动探测下不可达死分支;全量 362 passed / 99.28%(基线 351/99.27%)
- 门禁复跑 PASS:summary 与 MVP 基线一致(16/5/8/3/50/0),概要设计书 13 章
This commit is contained in:
lhl
2026-08-23 18:49:34 +08:00
parent 2e11720a2d
commit 61ac47c7af
6 changed files with 324 additions and 93 deletions
+2 -1
View File
@@ -17,7 +17,8 @@ ENV_PREFIX = "GENESIS_"
class ServerConfig(BaseModel):
max_upload_mb: int = 100
allowed_extensions: list[str] = Field(
default_factory=lambda: [".xlsx", ".xls", ".docx", ".pptx", ".java", ".xml", ".yml"]
default_factory=lambda: [".xlsx", ".xls", ".docx", ".pptx", ".java", ".xml", ".yml",
".py", ".ts", ".go", ".cs"]
)
+176 -89
View File
@@ -1,7 +1,11 @@
"""CodeParserJava 项目源码解析 → CodeStructureImpact Agent MVP)。
"""CodeParser多语言源码解析 → CodeStructureImpact Agent)。
识别 @RestController/@Controller、@Service、@Entity/@Table 及方法级路由映射,
输出供 ExistingSystemExplorer 组装 ExistingSystemInfo 的结构化清单。
通过语言适配器注册表(LANGUAGE_PARSERS)分发:每种开发语言一个适配器
BaseLanguageParser 子类),定义文件扩展名、类名识别、角色(控制器/服务/实体)
与端点提取。当前内置 Java 适配器(Spring Boot / JPA / MyBatis-Plus)。
新增语言 = 实现一个适配器类并调用 register_language_parser 注册,
下游(ExistingSystemExplorer / ImpactAgent / Writer)零改动。
"""
from __future__ import annotations
@@ -18,27 +22,7 @@ from genesis.data_models import (
class CodeParseError(Exception):
"""既有系统解析失败(路径无效或非 Java 源码项目)。"""
_JAVA_EXT = ".java"
_CLASS_RE = re.compile(
r"(?:public\s+|abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)"
)
_TABLE_RE = re.compile(r"@(?:Table|TableName)\s*\(\s*(?:name\s*=\s*)?[\"']([^\"']+)[\"']")
_CLASS_MAPPING_RE = re.compile(r"@RequestMapping\s*\(\s*[\"']([^\"']+)[\"']")
_METHOD_MAPPING_RE = re.compile(
r"@(Get|Post|Put|Delete|Patch|Request)Mapping\s*(?:\(\s*[\"']([^\"']*)[\"'])?"
)
_METHOD_DECL_RE = re.compile(
r"(?:public|private|protected|)\s+(?:static\s+|final\s+|synchronized\s+)*"
r"[\w<>\[\],.?]+\s+(\w+)\s*\("
)
_FIELD_DECL_RE = re.compile(
r"(?:private|public|protected)\s+[\w<>\[\],]+\s+(\w+)\s*;"
)
_IMPORT_RE = re.compile(r"^import\s+([\w.]+);", re.MULTILINE)
"""既有系统解析失败(路径无效或无可识别源码)。"""
def _relative(path: Path, root: Path) -> str:
@@ -49,81 +33,100 @@ def _read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="ignore")
class CodeParser:
"""解析 Java 项目目录,输出 CodeStructure(控制器/服务/实体/端点/模块)。"""
class BaseLanguageParser:
"""语言适配器基类:定义扩展名与单文件解析契约。
def parse(self, root_path: str | Path) -> CodeStructure:
root = Path(root_path)
if not root.is_dir():
raise CodeParseError(f"既有系统路径无效或不存在: {root_path}")
子类需设置 language / extensions,并实现 parse_file
返回 dictkeys: imports / classes / controllers / services / entities / endpoints)。
"""
java_files = sorted(p for p in root.rglob(f"*{_JAVA_EXT}") if p.is_file())
if not java_files:
raise CodeParseError(f"未找到 Java 源码: {root_path}")
language: str = ""
extensions: tuple[str, ...] = ()
controllers: list[ControllerInfo] = []
services: list[ServiceInfo] = []
entities: list[EntityInfo] = []
endpoints: list[EndpointInfo] = []
classes: list[dict] = []
raw_imports: list[dict] = []
def source_files(self, root: Path) -> list[Path]:
"""返回本语言适配器覆盖的源文件(排序 + 去重,保证确定性)。"""
files: list[Path] = []
for ext in self.extensions:
files.extend(p for p in root.rglob(f"*{ext}") if p.is_file())
return sorted(set(files))
for path in java_files:
text = _read(path)
rel = _relative(path, root)
class_name = self._class_name(text)
def parse_file(self, text: str, rel: str) -> dict:
"""解析单个源文件,返回该文件对 CodeStructure 各分层的贡献。"""
raise NotImplementedError
imports = _IMPORT_RE.findall(text)
raw_imports.append({"path": rel, "imports": imports})
if not class_name:
# package-info.java 等无类声明文件:仅登记 imports,不参与要素提取
continue
class JavaLanguageParser(BaseLanguageParser):
"""JavaSpring Boot / JPA / MyBatis-Plus)适配器。"""
is_controller = "@RestController" in text or "@Controller" in text
is_service = "@Service" in text
# 既有系统实体可能用 JPA @Entity 或 MyBatis-Plus @TableName 标注
is_entity = "@Entity" in text or "@TableName" in text
language = "java"
extensions = (".java",)
if is_controller:
controllers.append(self._parse_controller(text, rel, class_name, endpoints))
elif is_service:
services.append(self._parse_service(text, rel, class_name))
elif is_entity:
entities.append(self._parse_entity(text, rel, class_name))
_CLASS_RE = re.compile(
r"(?:public\s+|abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)"
)
_TABLE_RE = re.compile(r"@(?:Table|TableName)\s*\(\s*(?:name\s*=\s*)?[\"']([^\"']+)[\"']")
_CLASS_MAPPING_RE = re.compile(r"@RequestMapping\s*\(\s*[\"']([^\"']+)[\"']")
_METHOD_MAPPING_RE = re.compile(
r"@(Get|Post|Put|Delete|Patch|Request)Mapping\s*(?:\(\s*[\"']([^\"']*)[\"'])?"
)
_METHOD_DECL_RE = re.compile(
r"(?:public|private|protected|)\s+(?:static\s+|final\s+|synchronized\s+)*"
r"[\w<>\[\],.?]+\s+(\w+)\s*\("
)
_FIELD_DECL_RE = re.compile(
r"(?:private|public|protected)\s+[\w<>\[\],]+\s+(\w+)\s*;"
)
_IMPORT_RE = re.compile(r"^import\s+([\w.]+);", re.MULTILINE)
classes.append({"class_name": class_name, "path": rel})
def parse_file(self, text: str, rel: str) -> dict:
class_name = self._class_name(text)
imports = self._IMPORT_RE.findall(text)
out: dict = {
"imports": imports,
"classes": [],
"controllers": [],
"services": [],
"entities": [],
"endpoints": [],
}
if not class_name:
# package-info.java 等无类声明文件:仅登记 imports,不参与要素提取
return out
modules = self._modules(root, java_files)
out["classes"].append({"class_name": class_name, "path": rel})
return CodeStructure(
root_path=str(root),
language="java",
modules=modules,
classes=classes,
controllers=controllers,
services=services,
entities=entities,
endpoints=endpoints,
raw_imports=raw_imports,
)
is_controller = "@RestController" in text or "@Controller" in text
is_service = "@Service" in text
# 既有系统实体可能用 JPA @Entity 或 MyBatis-Plus @TableName 标注
is_entity = "@Entity" in text or "@TableName" in text
if is_controller:
ctrl, endpoints = self._parse_controller(text, rel, class_name)
out["controllers"].append(ctrl)
out["endpoints"] = endpoints
elif is_service:
out["services"].append(self._parse_service(text, rel, class_name))
elif is_entity:
out["entities"].append(self._parse_entity(text, rel, class_name))
return out
@staticmethod
def _class_name(text: str) -> str | None:
m = _CLASS_RE.search(text)
m = JavaLanguageParser._CLASS_RE.search(text)
return m.group(1) if m else None
@staticmethod
def _parse_controller(
text: str, rel: str, class_name: str, endpoints: list[EndpointInfo]
) -> ControllerInfo:
text: str, rel: str, class_name: str
) -> tuple[ControllerInfo, list[EndpointInfo]]:
base_path = ""
m = _CLASS_MAPPING_RE.search(text)
m = JavaLanguageParser._CLASS_MAPPING_RE.search(text)
if m:
base_path = m.group(1)
ctrl_endpoints: list[str] = []
for m in _METHOD_MAPPING_RE.finditer(text):
endpoints: list[EndpointInfo] = []
for m in JavaLanguageParser._METHOD_MAPPING_RE.finditer(text):
verb, sub = m.group(1).upper(), m.group(2) or ""
if verb == "REQUEST":
verb = "ANY"
@@ -139,18 +142,21 @@ class CodeParser:
)
)
return ControllerInfo(
name=class_name,
class_name=class_name,
path=rel,
base_path=base_path,
endpoints=ctrl_endpoints,
source_uri=rel,
return (
ControllerInfo(
name=class_name,
class_name=class_name,
path=rel,
base_path=base_path,
endpoints=ctrl_endpoints,
source_uri=rel,
),
endpoints,
)
@staticmethod
def _parse_service(text: str, rel: str, class_name: str) -> ServiceInfo:
methods = list(dict.fromkeys(_METHOD_DECL_RE.findall(text)))
methods = list(dict.fromkeys(JavaLanguageParser._METHOD_DECL_RE.findall(text)))
return ServiceInfo(
name=class_name,
class_name=class_name,
@@ -162,10 +168,10 @@ class CodeParser:
@staticmethod
def _parse_entity(text: str, rel: str, class_name: str) -> EntityInfo:
table_name = None
m = _TABLE_RE.search(text)
m = JavaLanguageParser._TABLE_RE.search(text)
if m:
table_name = m.group(1)
fields = list(dict.fromkeys(_FIELD_DECL_RE.findall(text)))
fields = list(dict.fromkeys(JavaLanguageParser._FIELD_DECL_RE.findall(text)))
return EntityInfo(
name=class_name,
class_name=class_name,
@@ -175,12 +181,93 @@ class CodeParser:
source_uri=rel,
)
# ---------- 语言适配器注册表 ----------
LANGUAGE_PARSERS: dict[str, type[BaseLanguageParser]] = {}
def register_language_parser(name: str, parser_cls: type[BaseLanguageParser]) -> None:
"""注册语言适配器(未来扩展点:新增语言只需实现并注册,下游零改动)。"""
LANGUAGE_PARSERS[name] = parser_cls
register_language_parser(JavaLanguageParser.language, JavaLanguageParser)
class CodeParser:
"""按语言分发解析既有系统目录,输出 CodeStructure。"""
def parse(self, root_path: str | Path, language: str | None = None) -> CodeStructure:
root = Path(root_path)
if not root.is_dir():
raise CodeParseError(f"既有系统路径无效或不存在: {root_path}")
if language is not None:
cls = LANGUAGE_PARSERS.get(language)
if cls is None:
raise CodeParseError(
f"不支持的源码语言: {language}(支持: {', '.join(sorted(LANGUAGE_PARSERS))}"
)
parser_cls = [cls]
else:
# 自动探测:仅保留在根目录下确有源文件的语言
present = [
name for name in sorted(LANGUAGE_PARSERS)
if LANGUAGE_PARSERS[name]().source_files(root)
]
if not present:
raise CodeParseError(
f"未找到可识别的源码(支持: {', '.join(sorted(LANGUAGE_PARSERS))}: {root_path}"
)
parser_cls = [LANGUAGE_PARSERS[name] for name in present]
controllers: list[ControllerInfo] = []
services: list[ServiceInfo] = []
entities: list[EntityInfo] = []
endpoints: list[EndpointInfo] = []
classes: list[dict] = []
raw_imports: list[dict] = []
all_files: list[Path] = []
for cls in parser_cls:
parser = cls()
files = parser.source_files(root)
if not files:
# 自动探测的 present 过滤已保证有文件;此处仅显式 language 时可达
raise CodeParseError(f"未找到 {parser.language} 源码: {root_path}")
all_files.extend(files)
for path in files:
text = _read(path)
rel = _relative(path, root)
out = parser.parse_file(text, rel)
raw_imports.append({"path": rel, "imports": out["imports"]})
controllers.extend(out["controllers"])
services.extend(out["services"])
entities.extend(out["entities"])
endpoints.extend(out["endpoints"])
classes.extend(out["classes"])
modules = self._modules(root, all_files)
langs = [cls.language for cls in parser_cls]
return CodeStructure(
root_path=str(root),
language=",".join(langs) if len(langs) > 1 else langs[0],
modules=modules,
classes=classes,
controllers=controllers,
services=services,
entities=entities,
endpoints=endpoints,
raw_imports=raw_imports,
)
@staticmethod
def _modules(root: Path, java_files: list[Path]) -> list[str]:
"""顶层目录中凡包含 Java 源码者视为一个模块(按名排序,保证确定性)。"""
def _modules(root: Path, files: list[Path]) -> list[str]:
"""顶层目录中凡包含源码者视为一个模块(按名排序,保证确定性)。"""
mods = {
p.relative_to(root).parts[0]
for p in java_files
for p in files
if len(p.relative_to(root).parts) > 1
}
return sorted(mods)
+4 -1
View File
@@ -45,11 +45,14 @@ class SourceParser:
write_instruction_paths: list[str | Path] | None = None,
rule_paths: list[str | Path] | None = None,
existing_system_path: str | Path | None = None,
existing_system_language: str | None = None,
) -> StructuredSource:
"""扩展名校验先于存在性校验(不存在的文件若扩展名未知将抛出 ValueError 而非 FileNotFoundError)。
existing_system_path:既有系统源码目录(追加/改修场景)。提供时解析为
ExistingSystemInfo(门控通过 → 进入影响调查);未提供/解析失败 → 保持 None。
existing_system_language:既有系统源码开发语言(如 "java")。默认 None 表示
由 CodeParser 按扩展名自动探测;显式指定时按该语言解析(多语言支持扩展点)。
"""
requirement_paths = requirement_paths or []
@@ -77,7 +80,7 @@ class SourceParser:
existing_system = None
if existing_system_path is not None:
code = CodeParser().parse(existing_system_path)
code = CodeParser().parse(existing_system_path, language=existing_system_language)
existing_system = ExistingSystemExplorer().explore(code)
return StructuredSource(