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:
@@ -112,3 +112,4 @@
|
|||||||
| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash || 2026-08-23 | 需求理解 | 用户指出写概要设计书需「要件定義 + 既有系统(设计/代码)做影响调查 → 确定修改范围」;确认场景=追加改修、输入=Java 项目源码;选定既有系统样本 sunOnly/stock-trade-system(股票量化交易系统,无 LICENSE,仅作测试输入保留来源标注,引入 samples/existing-system/,13 模块 278 Java 文件);确认领域不一致 → 新造股票交易域追加改修样本 | docs/superpowers/specs/2026-08-23-impact-mvp.md; samples/existing-system/README.md | deepseek-v4-flash |
|
| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash || 2026-08-23 | 需求理解 | 用户指出写概要设计书需「要件定義 + 既有系统(设计/代码)做影响调查 → 确定修改范围」;确认场景=追加改修、输入=Java 项目源码;选定既有系统样本 sunOnly/stock-trade-system(股票量化交易系统,无 LICENSE,仅作测试输入保留来源标注,引入 samples/existing-system/,13 模块 278 Java 文件);确认领域不一致 → 新造股票交易域追加改修样本 | docs/superpowers/specs/2026-08-23-impact-mvp.md; samples/existing-system/README.md | deepseek-v4-flash |
|
||||||
| 2026-08-23 | 架构设计 | Impact Agent MVP 设计(brainstorming 逐问确认):门控=用户是否提供 existing_system 路径(流水线级,非章节级);范围确认=与用户确认调查范围(模块/深度);消费模型=影响调查结果是生成概要设计书的主上下文(整本基于影响结果生成,无专用影响章/无 {{section:impact}} 锚点门控);MVP=变更点定位(确定性规则,无 LLM);spec 落盘 docs/superpowers/specs/2026-08-23-impact-mvp.md | docs/superpowers/specs/2026-08-23-impact-mvp.md | deepseek-v4-flash |
|
| 2026-08-23 | 架构设计 | Impact Agent MVP 设计(brainstorming 逐问确认):门控=用户是否提供 existing_system 路径(流水线级,非章节级);范围确认=与用户确认调查范围(模块/深度);消费模型=影响调查结果是生成概要设计书的主上下文(整本基于影响结果生成,无专用影响章/无 {{section:impact}} 锚点门控);MVP=变更点定位(确定性规则,无 LLM);spec 落盘 docs/superpowers/specs/2026-08-23-impact-mvp.md | docs/superpowers/specs/2026-08-23-impact-mvp.md | deepseek-v4-flash |
|
||||||
| 2026-08-23 | Agent 实现 | Impact Agent MVP 实现(TDD RED→GREEN→全量 351 passed / 99.27%):data_models 新增 ChangeType/ChangeElement/ImpactWarning/ChangeAnalysis/ImpactReport + StructuredSource.impact_report;新增 src/genesis/impact/(code_parser.py Java 解析 @RestController/@Service/@Entity/@TableName、existing_system_explorer.py 组装、impact_agent.py 变更点定位+impact_report_to_dict 序列化);source_aggregator 解除 existing_system=None 硬编码(新增 existing_system_path 门控);writer 集成——GenerationContext.impact_report + to_vars()[impact]、WRITER_PROMPT_TEMPLATE 加「影响调查上下文」、context_builder 透传、WriteOrchestrator.generate 门控自动跑 ImpactAgent + 回填 + 默认 meta(doc_title/version/created_at);新造样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名);门禁(fake 模式)PASS:影响调查 total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0,概要设计书 13 章产出,影响调查书 JSON 可下载 | src/genesis/data_models.py; src/genesis/impact/code_parser.py; src/genesis/impact/existing_system_explorer.py; src/genesis/impact/impact_agent.py; src/genesis/parsers/source_aggregator.py; src/genesis/writer/models.py; src/genesis/writer/writer_agent.py; src/genesis/writer/context_builder.py; src/genesis/writer/orchestrator.py; tests/test_data_models.py; tests/test_code_parser.py; tests/test_existing_system_explorer.py; tests/test_impact_agent.py; tests/test_source_aggregator.py; tests/test_phase5_models.py; tests/test_phase5_writer_agent.py; tests/test_phase5_context_builder.py; tests/test_phase5_writer_orchestrator.py; samples/要件定義_追加改修_股票.xlsx; samples/existing-system/; docs/superpowers/specs/2026-08-23-impact-mvp.md; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
| 2026-08-23 | Agent 实现 | Impact Agent MVP 实现(TDD RED→GREEN→全量 351 passed / 99.27%):data_models 新增 ChangeType/ChangeElement/ImpactWarning/ChangeAnalysis/ImpactReport + StructuredSource.impact_report;新增 src/genesis/impact/(code_parser.py Java 解析 @RestController/@Service/@Entity/@TableName、existing_system_explorer.py 组装、impact_agent.py 变更点定位+impact_report_to_dict 序列化);source_aggregator 解除 existing_system=None 硬编码(新增 existing_system_path 门控);writer 集成——GenerationContext.impact_report + to_vars()[impact]、WRITER_PROMPT_TEMPLATE 加「影响调查上下文」、context_builder 透传、WriteOrchestrator.generate 门控自动跑 ImpactAgent + 回填 + 默认 meta(doc_title/version/created_at);新造样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名);门禁(fake 模式)PASS:影响调查 total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0,概要设计书 13 章产出,影响调查书 JSON 可下载 | src/genesis/data_models.py; src/genesis/impact/code_parser.py; src/genesis/impact/existing_system_explorer.py; src/genesis/impact/impact_agent.py; src/genesis/parsers/source_aggregator.py; src/genesis/writer/models.py; src/genesis/writer/writer_agent.py; src/genesis/writer/context_builder.py; src/genesis/writer/orchestrator.py; tests/test_data_models.py; tests/test_code_parser.py; tests/test_existing_system_explorer.py; tests/test_impact_agent.py; tests/test_source_aggregator.py; tests/test_phase5_models.py; tests/test_phase5_writer_agent.py; tests/test_phase5_context_builder.py; tests/test_phase5_writer_orchestrator.py; samples/要件定義_追加改修_股票.xlsx; samples/existing-system/; docs/superpowers/specs/2026-08-23-impact-mvp.md; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
||||||
|
| 2026-08-23 | Agent 实现 | CodeParser 多语言就绪预备性重构(行为不变):code_parser.py 重构为语言适配器注册表——BaseLanguageParser 抽象基类(extensions/source_files/parse_file 契约)+ JavaLanguageParser(迁移全部 Java 正则,输出与重构前逐字节一致:20Ctrl/25Svc/22Ent/113EP/13模块/219类/278imports)+ LANGUAGE_PARSERS 注册表 + register_language_parser 扩展点 + CodeParser 分发器(parse(root, language=None) 自动按扩展名探测/显式语言覆盖/多语言合并 language 逗号连接/无源码与不支持语言抛 CodeParseError);source_aggregator 新增 existing_system_language 透传参数;config.py allowed_extensions 补 .py/.ts/.go/.cs;删除自动探测下不可达 continue 死分支;TDD RED(ImportError)→ GREEN(test_code_parser 17 passed + test_source_aggregator 19 passed)→ 全量 362 passed / 99.28%(基线 351/99.27%,+11 新测试);门禁(fake 模式)复跑 PASS:summary 与 MVP 基线一致(total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0),概要设计书 13 章;未实现任何新语言(仅 Java,后续加语言=新增适配器类+register_language_parser,下游零改动) | src/genesis/impact/code_parser.py; src/genesis/parsers/source_aggregator.py; src/genesis/config.py; tests/test_code_parser.py; tests/test_source_aggregator.py; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ ENV_PREFIX = "GENESIS_"
|
|||||||
class ServerConfig(BaseModel):
|
class ServerConfig(BaseModel):
|
||||||
max_upload_mb: int = 100
|
max_upload_mb: int = 100
|
||||||
allowed_extensions: list[str] = Field(
|
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"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
"""CodeParser:Java 项目源码解析 → CodeStructure(Impact Agent MVP)。
|
"""CodeParser:多语言源码解析 → CodeStructure(Impact Agent)。
|
||||||
|
|
||||||
识别 @RestController/@Controller、@Service、@Entity/@Table 及方法级路由映射,
|
通过语言适配器注册表(LANGUAGE_PARSERS)分发:每种开发语言一个适配器
|
||||||
输出供 ExistingSystemExplorer 组装 ExistingSystemInfo 的结构化清单。
|
(BaseLanguageParser 子类),定义文件扩展名、类名识别、角色(控制器/服务/实体)
|
||||||
|
与端点提取。当前内置 Java 适配器(Spring Boot / JPA / MyBatis-Plus)。
|
||||||
|
|
||||||
|
新增语言 = 实现一个适配器类并调用 register_language_parser 注册,
|
||||||
|
下游(ExistingSystemExplorer / ImpactAgent / Writer)零改动。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,27 +22,7 @@ from genesis.data_models import (
|
|||||||
|
|
||||||
|
|
||||||
class CodeParseError(Exception):
|
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:
|
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")
|
return path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
class CodeParser:
|
class BaseLanguageParser:
|
||||||
"""解析 Java 项目目录,输出 CodeStructure(控制器/服务/实体/端点/模块)。"""
|
"""语言适配器基类:定义扩展名与单文件解析契约。
|
||||||
|
|
||||||
def parse(self, root_path: str | Path) -> CodeStructure:
|
子类需设置 language / extensions,并实现 parse_file:
|
||||||
root = Path(root_path)
|
返回 dict(keys: imports / classes / controllers / services / entities / endpoints)。
|
||||||
if not root.is_dir():
|
"""
|
||||||
raise CodeParseError(f"既有系统路径无效或不存在: {root_path}")
|
|
||||||
|
|
||||||
java_files = sorted(p for p in root.rglob(f"*{_JAVA_EXT}") if p.is_file())
|
language: str = ""
|
||||||
if not java_files:
|
extensions: tuple[str, ...] = ()
|
||||||
raise CodeParseError(f"未找到 Java 源码: {root_path}")
|
|
||||||
|
|
||||||
controllers: list[ControllerInfo] = []
|
def source_files(self, root: Path) -> list[Path]:
|
||||||
services: list[ServiceInfo] = []
|
"""返回本语言适配器覆盖的源文件(排序 + 去重,保证确定性)。"""
|
||||||
entities: list[EntityInfo] = []
|
files: list[Path] = []
|
||||||
endpoints: list[EndpointInfo] = []
|
for ext in self.extensions:
|
||||||
classes: list[dict] = []
|
files.extend(p for p in root.rglob(f"*{ext}") if p.is_file())
|
||||||
raw_imports: list[dict] = []
|
return sorted(set(files))
|
||||||
|
|
||||||
for path in java_files:
|
def parse_file(self, text: str, rel: str) -> dict:
|
||||||
text = _read(path)
|
"""解析单个源文件,返回该文件对 CodeStructure 各分层的贡献。"""
|
||||||
rel = _relative(path, root)
|
raise NotImplementedError
|
||||||
class_name = self._class_name(text)
|
|
||||||
|
|
||||||
imports = _IMPORT_RE.findall(text)
|
|
||||||
raw_imports.append({"path": rel, "imports": imports})
|
|
||||||
|
|
||||||
if not class_name:
|
class JavaLanguageParser(BaseLanguageParser):
|
||||||
# package-info.java 等无类声明文件:仅登记 imports,不参与要素提取
|
"""Java(Spring Boot / JPA / MyBatis-Plus)适配器。"""
|
||||||
continue
|
|
||||||
|
|
||||||
is_controller = "@RestController" in text or "@Controller" in text
|
language = "java"
|
||||||
is_service = "@Service" in text
|
extensions = (".java",)
|
||||||
# 既有系统实体可能用 JPA @Entity 或 MyBatis-Plus @TableName 标注
|
|
||||||
is_entity = "@Entity" in text or "@TableName" in text
|
|
||||||
|
|
||||||
if is_controller:
|
_CLASS_RE = re.compile(
|
||||||
controllers.append(self._parse_controller(text, rel, class_name, endpoints))
|
r"(?:public\s+|abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)"
|
||||||
elif is_service:
|
)
|
||||||
services.append(self._parse_service(text, rel, class_name))
|
_TABLE_RE = re.compile(r"@(?:Table|TableName)\s*\(\s*(?:name\s*=\s*)?[\"']([^\"']+)[\"']")
|
||||||
elif is_entity:
|
_CLASS_MAPPING_RE = re.compile(r"@RequestMapping\s*\(\s*[\"']([^\"']+)[\"']")
|
||||||
entities.append(self._parse_entity(text, rel, class_name))
|
_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(
|
is_controller = "@RestController" in text or "@Controller" in text
|
||||||
root_path=str(root),
|
is_service = "@Service" in text
|
||||||
language="java",
|
# 既有系统实体可能用 JPA @Entity 或 MyBatis-Plus @TableName 标注
|
||||||
modules=modules,
|
is_entity = "@Entity" in text or "@TableName" in text
|
||||||
classes=classes,
|
|
||||||
controllers=controllers,
|
if is_controller:
|
||||||
services=services,
|
ctrl, endpoints = self._parse_controller(text, rel, class_name)
|
||||||
entities=entities,
|
out["controllers"].append(ctrl)
|
||||||
endpoints=endpoints,
|
out["endpoints"] = endpoints
|
||||||
raw_imports=raw_imports,
|
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
|
@staticmethod
|
||||||
def _class_name(text: str) -> str | None:
|
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
|
return m.group(1) if m else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_controller(
|
def _parse_controller(
|
||||||
text: str, rel: str, class_name: str, endpoints: list[EndpointInfo]
|
text: str, rel: str, class_name: str
|
||||||
) -> ControllerInfo:
|
) -> tuple[ControllerInfo, list[EndpointInfo]]:
|
||||||
base_path = ""
|
base_path = ""
|
||||||
m = _CLASS_MAPPING_RE.search(text)
|
m = JavaLanguageParser._CLASS_MAPPING_RE.search(text)
|
||||||
if m:
|
if m:
|
||||||
base_path = m.group(1)
|
base_path = m.group(1)
|
||||||
|
|
||||||
ctrl_endpoints: list[str] = []
|
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 ""
|
verb, sub = m.group(1).upper(), m.group(2) or ""
|
||||||
if verb == "REQUEST":
|
if verb == "REQUEST":
|
||||||
verb = "ANY"
|
verb = "ANY"
|
||||||
@@ -139,18 +142,21 @@ class CodeParser:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ControllerInfo(
|
return (
|
||||||
name=class_name,
|
ControllerInfo(
|
||||||
class_name=class_name,
|
name=class_name,
|
||||||
path=rel,
|
class_name=class_name,
|
||||||
base_path=base_path,
|
path=rel,
|
||||||
endpoints=ctrl_endpoints,
|
base_path=base_path,
|
||||||
source_uri=rel,
|
endpoints=ctrl_endpoints,
|
||||||
|
source_uri=rel,
|
||||||
|
),
|
||||||
|
endpoints,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_service(text: str, rel: str, class_name: str) -> ServiceInfo:
|
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(
|
return ServiceInfo(
|
||||||
name=class_name,
|
name=class_name,
|
||||||
class_name=class_name,
|
class_name=class_name,
|
||||||
@@ -162,10 +168,10 @@ class CodeParser:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_entity(text: str, rel: str, class_name: str) -> EntityInfo:
|
def _parse_entity(text: str, rel: str, class_name: str) -> EntityInfo:
|
||||||
table_name = None
|
table_name = None
|
||||||
m = _TABLE_RE.search(text)
|
m = JavaLanguageParser._TABLE_RE.search(text)
|
||||||
if m:
|
if m:
|
||||||
table_name = m.group(1)
|
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(
|
return EntityInfo(
|
||||||
name=class_name,
|
name=class_name,
|
||||||
class_name=class_name,
|
class_name=class_name,
|
||||||
@@ -175,12 +181,93 @@ class CodeParser:
|
|||||||
source_uri=rel,
|
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
|
@staticmethod
|
||||||
def _modules(root: Path, java_files: list[Path]) -> list[str]:
|
def _modules(root: Path, files: list[Path]) -> list[str]:
|
||||||
"""顶层目录中凡包含 Java 源码者视为一个模块(按名排序,保证确定性)。"""
|
"""顶层目录中凡包含源码者视为一个模块(按名排序,保证确定性)。"""
|
||||||
mods = {
|
mods = {
|
||||||
p.relative_to(root).parts[0]
|
p.relative_to(root).parts[0]
|
||||||
for p in java_files
|
for p in files
|
||||||
if len(p.relative_to(root).parts) > 1
|
if len(p.relative_to(root).parts) > 1
|
||||||
}
|
}
|
||||||
return sorted(mods)
|
return sorted(mods)
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ class SourceParser:
|
|||||||
write_instruction_paths: list[str | Path] | None = None,
|
write_instruction_paths: list[str | Path] | None = None,
|
||||||
rule_paths: list[str | Path] | None = None,
|
rule_paths: list[str | Path] | None = None,
|
||||||
existing_system_path: str | Path | None = None,
|
existing_system_path: str | Path | None = None,
|
||||||
|
existing_system_language: str | None = None,
|
||||||
) -> StructuredSource:
|
) -> StructuredSource:
|
||||||
"""扩展名校验先于存在性校验(不存在的文件若扩展名未知将抛出 ValueError 而非 FileNotFoundError)。
|
"""扩展名校验先于存在性校验(不存在的文件若扩展名未知将抛出 ValueError 而非 FileNotFoundError)。
|
||||||
|
|
||||||
existing_system_path:既有系统源码目录(追加/改修场景)。提供时解析为
|
existing_system_path:既有系统源码目录(追加/改修场景)。提供时解析为
|
||||||
ExistingSystemInfo(门控通过 → 进入影响调查);未提供/解析失败 → 保持 None。
|
ExistingSystemInfo(门控通过 → 进入影响调查);未提供/解析失败 → 保持 None。
|
||||||
|
existing_system_language:既有系统源码开发语言(如 "java")。默认 None 表示
|
||||||
|
由 CodeParser 按扩展名自动探测;显式指定时按该语言解析(多语言支持扩展点)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
requirement_paths = requirement_paths or []
|
requirement_paths = requirement_paths or []
|
||||||
@@ -77,7 +80,7 @@ class SourceParser:
|
|||||||
|
|
||||||
existing_system = None
|
existing_system = None
|
||||||
if existing_system_path is not 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)
|
existing_system = ExistingSystemExplorer().explore(code)
|
||||||
|
|
||||||
return StructuredSource(
|
return StructuredSource(
|
||||||
|
|||||||
+113
-2
@@ -1,9 +1,17 @@
|
|||||||
"""CodeParser 测试(Impact Agent MVP):Java 项目解析 → CodeStructure。"""
|
"""CodeParser 测试(Impact Agent MVP):多语言适配器注册表 + Java 项目解析。"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from genesis.impact.code_parser import CodeParseError, CodeParser
|
from genesis.data_models import ControllerInfo, EndpointInfo
|
||||||
|
from genesis.impact.code_parser import (
|
||||||
|
LANGUAGE_PARSERS,
|
||||||
|
BaseLanguageParser,
|
||||||
|
CodeParseError,
|
||||||
|
CodeParser,
|
||||||
|
JavaLanguageParser,
|
||||||
|
register_language_parser,
|
||||||
|
)
|
||||||
|
|
||||||
FIXTURE_JAVA = {
|
FIXTURE_JAVA = {
|
||||||
"trade-order/src/main/java/com/trade/order/controller/OrderController.java": (
|
"trade-order/src/main/java/com/trade/order/controller/OrderController.java": (
|
||||||
@@ -124,3 +132,106 @@ def test_parse_real_sample_has_entities_and_endpoints():
|
|||||||
assert result.entities, "既有系统样本应至少有一个实体类"
|
assert result.entities, "既有系统样本应至少有一个实体类"
|
||||||
assert result.endpoints, "既有系统样本应至少有一个端点"
|
assert result.endpoints, "既有系统样本应至少有一个端点"
|
||||||
assert result.services, "既有系统样本应至少有一个 @Service 类"
|
assert result.services, "既有系统样本应至少有一个 @Service 类"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 多语言适配器注册表(预备性重构) ----------
|
||||||
|
|
||||||
|
class _FakeParser(BaseLanguageParser):
|
||||||
|
"""测试用假语言适配器:验证注册表/分发/多语言合并。"""
|
||||||
|
|
||||||
|
language = "fake"
|
||||||
|
extensions = (".fx",)
|
||||||
|
|
||||||
|
def parse_file(self, text: str, rel: str) -> dict:
|
||||||
|
return {
|
||||||
|
"imports": ["fake.import"],
|
||||||
|
"classes": [{"class_name": "FakeThing", "path": rel}],
|
||||||
|
"controllers": [
|
||||||
|
ControllerInfo(
|
||||||
|
name="FakeThing", class_name="FakeThing", path=rel,
|
||||||
|
base_path="/fake", endpoints=["/fake"], source_uri=rel,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"services": [],
|
||||||
|
"entities": [],
|
||||||
|
"endpoints": [
|
||||||
|
EndpointInfo(
|
||||||
|
method="GET", path="/fake", controller="FakeThing",
|
||||||
|
description="", source_uri=rel,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_language_registry_contains_java():
|
||||||
|
assert "java" in LANGUAGE_PARSERS
|
||||||
|
assert issubclass(LANGUAGE_PARSERS["java"], BaseLanguageParser)
|
||||||
|
|
||||||
|
|
||||||
|
def test_java_parser_adapter_metadata():
|
||||||
|
jp = JavaLanguageParser()
|
||||||
|
assert jp.language == "java"
|
||||||
|
assert ".java" in jp.extensions
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_parser_parse_file_contract():
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseLanguageParser().parse_file("text", "rel")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_explicit_language(tmp_path):
|
||||||
|
root = _make_project(tmp_path, FIXTURE_JAVA)
|
||||||
|
result = CodeParser().parse(root, language="java")
|
||||||
|
assert {c.class_name for c in result.controllers} == {"OrderController"}
|
||||||
|
assert result.language == "java"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_unsupported_language_raises(tmp_path):
|
||||||
|
root = _make_project(tmp_path, FIXTURE_JAVA)
|
||||||
|
with pytest.raises(CodeParseError):
|
||||||
|
CodeParser().parse(root, language="kotlin")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_explicit_language_no_files_raises(tmp_path):
|
||||||
|
(tmp_path / "readme.txt").write_text("hello", encoding="utf-8")
|
||||||
|
with pytest.raises(CodeParseError):
|
||||||
|
CodeParser().parse(tmp_path, language="java")
|
||||||
|
|
||||||
|
|
||||||
|
def test_java_parse_file_interface(tmp_path):
|
||||||
|
jp = JavaLanguageParser()
|
||||||
|
text = FIXTURE_JAVA["trade-order/src/main/java/com/trade/order/controller/OrderController.java"]
|
||||||
|
out = jp.parse_file(text, "OrderController.java")
|
||||||
|
assert len(out["controllers"]) == 1
|
||||||
|
assert out["controllers"][0].class_name == "OrderController"
|
||||||
|
assert {"/api/order/{id}", "/api/order/create"} <= {e.path for e in out["endpoints"]}
|
||||||
|
assert out["classes"] == [{"class_name": "OrderController", "path": "OrderController.java"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_and_dispatch_new_language(tmp_path):
|
||||||
|
register_language_parser("fake", _FakeParser)
|
||||||
|
try:
|
||||||
|
root = _make_project(tmp_path, {
|
||||||
|
"app/main.fx": "FAKE",
|
||||||
|
"trade-order/src/main/java/com/trade/order/controller/OrderController.java": (
|
||||||
|
FIXTURE_JAVA["trade-order/src/main/java/com/trade/order/controller/OrderController.java"]
|
||||||
|
),
|
||||||
|
})
|
||||||
|
result = CodeParser().parse(root)
|
||||||
|
names = {c.class_name for c in result.controllers}
|
||||||
|
assert names == {"OrderController", "FakeThing"}
|
||||||
|
assert result.language == "fake,java" # 多语言按名排序逗号连接
|
||||||
|
assert any(e.path == "/fake" for e in result.endpoints)
|
||||||
|
finally:
|
||||||
|
LANGUAGE_PARSERS.pop("fake", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_language_explicit_only(tmp_path):
|
||||||
|
register_language_parser("fake", _FakeParser)
|
||||||
|
try:
|
||||||
|
root = _make_project(tmp_path, {"a/main.fx": "FAKE"})
|
||||||
|
result = CodeParser().parse(root, language="fake")
|
||||||
|
assert result.language == "fake"
|
||||||
|
assert [c.class_name for c in result.controllers] == ["FakeThing"]
|
||||||
|
finally:
|
||||||
|
LANGUAGE_PARSERS.pop("fake", None)
|
||||||
|
|||||||
@@ -199,3 +199,31 @@ def test_parse_existing_system_path_without_java_raises(tmp_path):
|
|||||||
empty.mkdir()
|
empty.mkdir()
|
||||||
with pytest.raises(CodeParseError):
|
with pytest.raises(CodeParseError):
|
||||||
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=empty)
|
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=empty)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 多语言预备重构:existing_system_language 透传 ----------
|
||||||
|
|
||||||
|
def test_parse_existing_system_language_explicit(tmp_path):
|
||||||
|
xlsx = _xlsx(tmp_path)
|
||||||
|
existing = _java_project(tmp_path)
|
||||||
|
|
||||||
|
result = SourceParser().parse(
|
||||||
|
requirement_paths=[xlsx],
|
||||||
|
existing_system_path=existing,
|
||||||
|
existing_system_language="java",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.existing_system is not None
|
||||||
|
assert {c.class_name for c in result.existing_system.controller_layer} == {"OrderController"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_existing_system_language_unsupported_raises(tmp_path):
|
||||||
|
xlsx = _xlsx(tmp_path)
|
||||||
|
existing = _java_project(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(CodeParseError):
|
||||||
|
SourceParser().parse(
|
||||||
|
requirement_paths=[xlsx],
|
||||||
|
existing_system_path=existing,
|
||||||
|
existing_system_language="kotlin",
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user