jarvis-ai-assistant 0.7.8__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +567 -222
- jarvis/jarvis_agent/agent_manager.py +19 -12
- jarvis/jarvis_agent/builtin_input_handler.py +79 -11
- jarvis/jarvis_agent/config_editor.py +7 -2
- jarvis/jarvis_agent/event_bus.py +24 -13
- jarvis/jarvis_agent/events.py +19 -1
- jarvis/jarvis_agent/file_context_handler.py +67 -64
- jarvis/jarvis_agent/file_methodology_manager.py +38 -24
- jarvis/jarvis_agent/jarvis.py +186 -114
- jarvis/jarvis_agent/language_extractors/__init__.py +8 -1
- jarvis/jarvis_agent/language_extractors/c_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/cpp_extractor.py +9 -4
- jarvis/jarvis_agent/language_extractors/go_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/java_extractor.py +27 -20
- jarvis/jarvis_agent/language_extractors/javascript_extractor.py +22 -17
- jarvis/jarvis_agent/language_extractors/python_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/rust_extractor.py +7 -4
- jarvis/jarvis_agent/language_extractors/typescript_extractor.py +22 -17
- jarvis/jarvis_agent/language_support_info.py +250 -219
- jarvis/jarvis_agent/main.py +19 -23
- jarvis/jarvis_agent/memory_manager.py +9 -6
- jarvis/jarvis_agent/methodology_share_manager.py +21 -15
- jarvis/jarvis_agent/output_handler.py +4 -2
- jarvis/jarvis_agent/prompt_builder.py +7 -6
- jarvis/jarvis_agent/prompt_manager.py +113 -8
- jarvis/jarvis_agent/prompts.py +317 -85
- jarvis/jarvis_agent/protocols.py +5 -2
- jarvis/jarvis_agent/run_loop.py +192 -32
- jarvis/jarvis_agent/session_manager.py +7 -3
- jarvis/jarvis_agent/share_manager.py +23 -13
- jarvis/jarvis_agent/shell_input_handler.py +12 -8
- jarvis/jarvis_agent/stdio_redirect.py +25 -26
- jarvis/jarvis_agent/task_analyzer.py +29 -23
- jarvis/jarvis_agent/task_list.py +869 -0
- jarvis/jarvis_agent/task_manager.py +26 -23
- jarvis/jarvis_agent/tool_executor.py +6 -5
- jarvis/jarvis_agent/tool_share_manager.py +24 -14
- jarvis/jarvis_agent/user_interaction.py +3 -3
- jarvis/jarvis_agent/utils.py +9 -1
- jarvis/jarvis_agent/web_bridge.py +37 -17
- jarvis/jarvis_agent/web_output_sink.py +5 -2
- jarvis/jarvis_agent/web_server.py +165 -36
- jarvis/jarvis_c2rust/__init__.py +1 -1
- jarvis/jarvis_c2rust/cli.py +260 -141
- jarvis/jarvis_c2rust/collector.py +37 -18
- jarvis/jarvis_c2rust/constants.py +60 -0
- jarvis/jarvis_c2rust/library_replacer.py +242 -1010
- jarvis/jarvis_c2rust/library_replacer_checkpoint.py +133 -0
- jarvis/jarvis_c2rust/library_replacer_llm.py +287 -0
- jarvis/jarvis_c2rust/library_replacer_loader.py +191 -0
- jarvis/jarvis_c2rust/library_replacer_output.py +134 -0
- jarvis/jarvis_c2rust/library_replacer_prompts.py +124 -0
- jarvis/jarvis_c2rust/library_replacer_utils.py +188 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +98 -1044
- jarvis/jarvis_c2rust/llm_module_agent_apply.py +170 -0
- jarvis/jarvis_c2rust/llm_module_agent_executor.py +288 -0
- jarvis/jarvis_c2rust/llm_module_agent_loader.py +170 -0
- jarvis/jarvis_c2rust/llm_module_agent_prompts.py +268 -0
- jarvis/jarvis_c2rust/llm_module_agent_types.py +57 -0
- jarvis/jarvis_c2rust/llm_module_agent_utils.py +150 -0
- jarvis/jarvis_c2rust/llm_module_agent_validator.py +119 -0
- jarvis/jarvis_c2rust/loaders.py +28 -10
- jarvis/jarvis_c2rust/models.py +5 -2
- jarvis/jarvis_c2rust/optimizer.py +192 -1974
- jarvis/jarvis_c2rust/optimizer_build_fix.py +286 -0
- jarvis/jarvis_c2rust/optimizer_clippy.py +766 -0
- jarvis/jarvis_c2rust/optimizer_config.py +49 -0
- jarvis/jarvis_c2rust/optimizer_docs.py +183 -0
- jarvis/jarvis_c2rust/optimizer_options.py +48 -0
- jarvis/jarvis_c2rust/optimizer_progress.py +469 -0
- jarvis/jarvis_c2rust/optimizer_report.py +52 -0
- jarvis/jarvis_c2rust/optimizer_unsafe.py +309 -0
- jarvis/jarvis_c2rust/optimizer_utils.py +469 -0
- jarvis/jarvis_c2rust/optimizer_visibility.py +185 -0
- jarvis/jarvis_c2rust/scanner.py +229 -166
- jarvis/jarvis_c2rust/transpiler.py +531 -2732
- jarvis/jarvis_c2rust/transpiler_agents.py +503 -0
- jarvis/jarvis_c2rust/transpiler_build.py +1294 -0
- jarvis/jarvis_c2rust/transpiler_codegen.py +204 -0
- jarvis/jarvis_c2rust/transpiler_compile.py +146 -0
- jarvis/jarvis_c2rust/transpiler_config.py +178 -0
- jarvis/jarvis_c2rust/transpiler_context.py +122 -0
- jarvis/jarvis_c2rust/transpiler_executor.py +516 -0
- jarvis/jarvis_c2rust/transpiler_generation.py +278 -0
- jarvis/jarvis_c2rust/transpiler_git.py +163 -0
- jarvis/jarvis_c2rust/transpiler_mod_utils.py +225 -0
- jarvis/jarvis_c2rust/transpiler_modules.py +336 -0
- jarvis/jarvis_c2rust/transpiler_planning.py +394 -0
- jarvis/jarvis_c2rust/transpiler_review.py +1196 -0
- jarvis/jarvis_c2rust/transpiler_symbols.py +176 -0
- jarvis/jarvis_c2rust/utils.py +269 -79
- jarvis/jarvis_code_agent/after_change.py +233 -0
- jarvis/jarvis_code_agent/build_validation_config.py +37 -30
- jarvis/jarvis_code_agent/builtin_rules.py +68 -0
- jarvis/jarvis_code_agent/code_agent.py +976 -1517
- jarvis/jarvis_code_agent/code_agent_build.py +227 -0
- jarvis/jarvis_code_agent/code_agent_diff.py +246 -0
- jarvis/jarvis_code_agent/code_agent_git.py +525 -0
- jarvis/jarvis_code_agent/code_agent_impact.py +177 -0
- jarvis/jarvis_code_agent/code_agent_lint.py +283 -0
- jarvis/jarvis_code_agent/code_agent_llm.py +159 -0
- jarvis/jarvis_code_agent/code_agent_postprocess.py +105 -0
- jarvis/jarvis_code_agent/code_agent_prompts.py +46 -0
- jarvis/jarvis_code_agent/code_agent_rules.py +305 -0
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +52 -48
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +12 -10
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +12 -11
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +16 -12
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +26 -17
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +558 -104
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +27 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +22 -18
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +21 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +20 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +27 -16
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +47 -23
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +71 -37
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +162 -35
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +111 -57
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +18 -12
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +185 -183
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +2 -1
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +24 -15
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +227 -141
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +321 -247
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +37 -29
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -13
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +15 -9
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +75 -45
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +87 -52
- jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +84 -51
- jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +94 -64
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +109 -71
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +97 -63
- jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +103 -69
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +271 -268
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +76 -64
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +92 -19
- jarvis/jarvis_code_agent/diff_visualizer.py +998 -0
- jarvis/jarvis_code_agent/lint.py +223 -524
- jarvis/jarvis_code_agent/rule_share_manager.py +158 -0
- jarvis/jarvis_code_agent/rules/clean_code.md +144 -0
- jarvis/jarvis_code_agent/rules/code_review.md +115 -0
- jarvis/jarvis_code_agent/rules/documentation.md +165 -0
- jarvis/jarvis_code_agent/rules/generate_rules.md +52 -0
- jarvis/jarvis_code_agent/rules/performance.md +158 -0
- jarvis/jarvis_code_agent/rules/refactoring.md +139 -0
- jarvis/jarvis_code_agent/rules/security.md +160 -0
- jarvis/jarvis_code_agent/rules/tdd.md +78 -0
- jarvis/jarvis_code_agent/test_rules/cpp_test.md +118 -0
- jarvis/jarvis_code_agent/test_rules/go_test.md +98 -0
- jarvis/jarvis_code_agent/test_rules/java_test.md +99 -0
- jarvis/jarvis_code_agent/test_rules/javascript_test.md +113 -0
- jarvis/jarvis_code_agent/test_rules/php_test.md +117 -0
- jarvis/jarvis_code_agent/test_rules/python_test.md +91 -0
- jarvis/jarvis_code_agent/test_rules/ruby_test.md +102 -0
- jarvis/jarvis_code_agent/test_rules/rust_test.md +86 -0
- jarvis/jarvis_code_agent/utils.py +36 -26
- jarvis/jarvis_code_analysis/checklists/loader.py +21 -21
- jarvis/jarvis_code_analysis/code_review.py +64 -33
- jarvis/jarvis_data/config_schema.json +285 -192
- jarvis/jarvis_git_squash/main.py +8 -6
- jarvis/jarvis_git_utils/git_commiter.py +53 -76
- jarvis/jarvis_mcp/__init__.py +5 -2
- jarvis/jarvis_mcp/sse_mcp_client.py +40 -30
- jarvis/jarvis_mcp/stdio_mcp_client.py +27 -19
- jarvis/jarvis_mcp/streamable_mcp_client.py +35 -26
- jarvis/jarvis_memory_organizer/memory_organizer.py +78 -55
- jarvis/jarvis_methodology/main.py +48 -39
- jarvis/jarvis_multi_agent/__init__.py +56 -23
- jarvis/jarvis_multi_agent/main.py +15 -18
- jarvis/jarvis_platform/base.py +179 -111
- jarvis/jarvis_platform/human.py +27 -16
- jarvis/jarvis_platform/kimi.py +52 -45
- jarvis/jarvis_platform/openai.py +101 -40
- jarvis/jarvis_platform/registry.py +51 -33
- jarvis/jarvis_platform/tongyi.py +68 -38
- jarvis/jarvis_platform/yuanbao.py +59 -43
- jarvis/jarvis_platform_manager/main.py +68 -76
- jarvis/jarvis_platform_manager/service.py +24 -14
- jarvis/jarvis_rag/README_CONFIG.md +314 -0
- jarvis/jarvis_rag/README_DYNAMIC_LOADING.md +311 -0
- jarvis/jarvis_rag/README_ONLINE_MODELS.md +230 -0
- jarvis/jarvis_rag/__init__.py +57 -4
- jarvis/jarvis_rag/cache.py +3 -1
- jarvis/jarvis_rag/cli.py +48 -68
- jarvis/jarvis_rag/embedding_interface.py +39 -0
- jarvis/jarvis_rag/embedding_manager.py +7 -230
- jarvis/jarvis_rag/embeddings/__init__.py +41 -0
- jarvis/jarvis_rag/embeddings/base.py +114 -0
- jarvis/jarvis_rag/embeddings/cohere.py +66 -0
- jarvis/jarvis_rag/embeddings/edgefn.py +117 -0
- jarvis/jarvis_rag/embeddings/local.py +260 -0
- jarvis/jarvis_rag/embeddings/openai.py +62 -0
- jarvis/jarvis_rag/embeddings/registry.py +293 -0
- jarvis/jarvis_rag/llm_interface.py +8 -6
- jarvis/jarvis_rag/query_rewriter.py +8 -9
- jarvis/jarvis_rag/rag_pipeline.py +61 -52
- jarvis/jarvis_rag/reranker.py +7 -75
- jarvis/jarvis_rag/reranker_interface.py +32 -0
- jarvis/jarvis_rag/rerankers/__init__.py +41 -0
- jarvis/jarvis_rag/rerankers/base.py +109 -0
- jarvis/jarvis_rag/rerankers/cohere.py +67 -0
- jarvis/jarvis_rag/rerankers/edgefn.py +140 -0
- jarvis/jarvis_rag/rerankers/jina.py +79 -0
- jarvis/jarvis_rag/rerankers/local.py +89 -0
- jarvis/jarvis_rag/rerankers/registry.py +293 -0
- jarvis/jarvis_rag/retriever.py +58 -43
- jarvis/jarvis_sec/__init__.py +66 -141
- jarvis/jarvis_sec/agents.py +21 -17
- jarvis/jarvis_sec/analysis.py +80 -33
- jarvis/jarvis_sec/checkers/__init__.py +7 -13
- jarvis/jarvis_sec/checkers/c_checker.py +356 -164
- jarvis/jarvis_sec/checkers/rust_checker.py +47 -29
- jarvis/jarvis_sec/cli.py +43 -21
- jarvis/jarvis_sec/clustering.py +430 -272
- jarvis/jarvis_sec/file_manager.py +99 -55
- jarvis/jarvis_sec/parsers.py +9 -6
- jarvis/jarvis_sec/prompts.py +4 -3
- jarvis/jarvis_sec/report.py +44 -22
- jarvis/jarvis_sec/review.py +180 -107
- jarvis/jarvis_sec/status.py +50 -41
- jarvis/jarvis_sec/types.py +3 -0
- jarvis/jarvis_sec/utils.py +160 -83
- jarvis/jarvis_sec/verification.py +411 -181
- jarvis/jarvis_sec/workflow.py +132 -21
- jarvis/jarvis_smart_shell/main.py +28 -41
- jarvis/jarvis_stats/cli.py +14 -12
- jarvis/jarvis_stats/stats.py +28 -19
- jarvis/jarvis_stats/storage.py +14 -8
- jarvis/jarvis_stats/visualizer.py +12 -7
- jarvis/jarvis_tools/base.py +5 -2
- jarvis/jarvis_tools/clear_memory.py +13 -9
- jarvis/jarvis_tools/cli/main.py +23 -18
- jarvis/jarvis_tools/edit_file.py +572 -873
- jarvis/jarvis_tools/execute_script.py +10 -7
- jarvis/jarvis_tools/file_analyzer.py +7 -8
- jarvis/jarvis_tools/meta_agent.py +287 -0
- jarvis/jarvis_tools/methodology.py +5 -3
- jarvis/jarvis_tools/read_code.py +305 -1438
- jarvis/jarvis_tools/read_symbols.py +50 -17
- jarvis/jarvis_tools/read_webpage.py +19 -18
- jarvis/jarvis_tools/registry.py +435 -156
- jarvis/jarvis_tools/retrieve_memory.py +16 -11
- jarvis/jarvis_tools/save_memory.py +8 -6
- jarvis/jarvis_tools/search_web.py +31 -31
- jarvis/jarvis_tools/sub_agent.py +32 -28
- jarvis/jarvis_tools/sub_code_agent.py +44 -60
- jarvis/jarvis_tools/task_list_manager.py +1811 -0
- jarvis/jarvis_tools/virtual_tty.py +29 -19
- jarvis/jarvis_utils/__init__.py +4 -0
- jarvis/jarvis_utils/builtin_replace_map.py +2 -1
- jarvis/jarvis_utils/clipboard.py +9 -8
- jarvis/jarvis_utils/collections.py +331 -0
- jarvis/jarvis_utils/config.py +699 -194
- jarvis/jarvis_utils/dialogue_recorder.py +294 -0
- jarvis/jarvis_utils/embedding.py +6 -3
- jarvis/jarvis_utils/file_processors.py +7 -1
- jarvis/jarvis_utils/fzf.py +9 -3
- jarvis/jarvis_utils/git_utils.py +71 -42
- jarvis/jarvis_utils/globals.py +116 -32
- jarvis/jarvis_utils/http.py +6 -2
- jarvis/jarvis_utils/input.py +318 -83
- jarvis/jarvis_utils/jsonnet_compat.py +119 -104
- jarvis/jarvis_utils/methodology.py +37 -28
- jarvis/jarvis_utils/output.py +201 -44
- jarvis/jarvis_utils/utils.py +986 -628
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/METADATA +49 -33
- jarvis_ai_assistant-1.0.2.dist-info/RECORD +304 -0
- jarvis/jarvis_code_agent/code_analyzer/structured_code.py +0 -556
- jarvis/jarvis_tools/generate_new_tool.py +0 -205
- jarvis/jarvis_tools/lsp_client.py +0 -1552
- jarvis/jarvis_tools/rewrite_file.py +0 -105
- jarvis_ai_assistant-0.7.8.dist-info/RECORD +0 -218
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -3,22 +3,26 @@
|
|
|
3
3
|
提供代码编辑影响范围分析功能,识别可能受影响的文件、函数、测试等。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import ast
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
|
-
import ast
|
|
9
9
|
import subprocess
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import field
|
|
12
12
|
from enum import Enum
|
|
13
|
+
from typing import Dict
|
|
14
|
+
from typing import List
|
|
15
|
+
from typing import Optional
|
|
16
|
+
from typing import Set
|
|
13
17
|
|
|
14
18
|
from .context_manager import ContextManager
|
|
15
19
|
from .file_ignore import filter_walk_dirs
|
|
16
20
|
from .symbol_extractor import Symbol
|
|
17
21
|
|
|
18
22
|
|
|
19
|
-
|
|
20
23
|
class ImpactType(Enum):
|
|
21
24
|
"""影响类型枚举"""
|
|
25
|
+
|
|
22
26
|
REFERENCE = "reference" # 符号引用
|
|
23
27
|
DEPENDENT = "dependent" # 依赖的符号
|
|
24
28
|
TEST = "test" # 测试文件
|
|
@@ -28,6 +32,7 @@ class ImpactType(Enum):
|
|
|
28
32
|
|
|
29
33
|
class RiskLevel(Enum):
|
|
30
34
|
"""风险等级枚举"""
|
|
35
|
+
|
|
31
36
|
LOW = "low"
|
|
32
37
|
MEDIUM = "medium"
|
|
33
38
|
HIGH = "high"
|
|
@@ -36,6 +41,7 @@ class RiskLevel(Enum):
|
|
|
36
41
|
@dataclass
|
|
37
42
|
class Impact:
|
|
38
43
|
"""表示一个影响项"""
|
|
44
|
+
|
|
39
45
|
impact_type: ImpactType
|
|
40
46
|
target: str # 受影响的目标(文件路径、符号名等)
|
|
41
47
|
description: str = ""
|
|
@@ -46,6 +52,7 @@ class Impact:
|
|
|
46
52
|
@dataclass
|
|
47
53
|
class InterfaceChange:
|
|
48
54
|
"""表示接口变更"""
|
|
55
|
+
|
|
49
56
|
symbol_name: str
|
|
50
57
|
change_type: str # 'signature', 'return_type', 'parameter', 'removed', 'added'
|
|
51
58
|
file_path: str
|
|
@@ -58,6 +65,7 @@ class InterfaceChange:
|
|
|
58
65
|
@dataclass
|
|
59
66
|
class ImpactReport:
|
|
60
67
|
"""影响分析报告"""
|
|
68
|
+
|
|
61
69
|
affected_files: List[str] = field(default_factory=list)
|
|
62
70
|
affected_symbols: List[Symbol] = field(default_factory=list)
|
|
63
71
|
affected_tests: List[str] = field(default_factory=list)
|
|
@@ -65,48 +73,56 @@ class ImpactReport:
|
|
|
65
73
|
impacts: List[Impact] = field(default_factory=list)
|
|
66
74
|
risk_level: RiskLevel = RiskLevel.LOW
|
|
67
75
|
recommendations: List[str] = field(default_factory=list)
|
|
68
|
-
|
|
76
|
+
|
|
69
77
|
def to_string(self, project_root: str = "") -> str:
|
|
70
78
|
"""生成可读的影响报告字符串"""
|
|
71
79
|
lines = []
|
|
72
80
|
lines.append("=" * 60)
|
|
73
81
|
lines.append("编辑影响范围分析报告")
|
|
74
82
|
lines.append("=" * 60)
|
|
75
|
-
|
|
83
|
+
|
|
76
84
|
# 风险等级
|
|
77
|
-
risk_emoji = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
lines.append(f"\n风险等级: {risk_emoji.get(self.risk_level, '⚪')} {self.risk_level.value.upper()}")
|
|
83
|
-
|
|
85
|
+
risk_emoji = {RiskLevel.LOW: "🟢", RiskLevel.MEDIUM: "🟡", RiskLevel.HIGH: "🔴"}
|
|
86
|
+
lines.append(
|
|
87
|
+
f"\n风险等级: {risk_emoji.get(self.risk_level, '⚪')} {self.risk_level.value.upper()}"
|
|
88
|
+
)
|
|
89
|
+
|
|
84
90
|
# 受影响文件
|
|
85
91
|
if self.affected_files:
|
|
86
92
|
lines.append(f"\n受影响文件 ({len(self.affected_files)}):")
|
|
87
93
|
for file_path in self.affected_files[:10]:
|
|
88
|
-
rel_path =
|
|
94
|
+
rel_path = (
|
|
95
|
+
os.path.relpath(file_path, project_root)
|
|
96
|
+
if project_root
|
|
97
|
+
else file_path
|
|
98
|
+
)
|
|
89
99
|
lines.append(f" - {rel_path}")
|
|
90
100
|
if len(self.affected_files) > 10:
|
|
91
101
|
lines.append(f" ... 还有 {len(self.affected_files) - 10} 个文件")
|
|
92
|
-
|
|
102
|
+
|
|
93
103
|
# 受影响符号
|
|
94
104
|
if self.affected_symbols:
|
|
95
105
|
lines.append(f"\n受影响符号 ({len(self.affected_symbols)}):")
|
|
96
106
|
for symbol in self.affected_symbols[:10]:
|
|
97
|
-
lines.append(
|
|
107
|
+
lines.append(
|
|
108
|
+
f" - {symbol.kind} {symbol.name} ({os.path.basename(symbol.file_path)}:{symbol.line_start})"
|
|
109
|
+
)
|
|
98
110
|
if len(self.affected_symbols) > 10:
|
|
99
111
|
lines.append(f" ... 还有 {len(self.affected_symbols) - 10} 个符号")
|
|
100
|
-
|
|
112
|
+
|
|
101
113
|
# 受影响测试
|
|
102
114
|
if self.affected_tests:
|
|
103
115
|
lines.append(f"\n受影响测试 ({len(self.affected_tests)}):")
|
|
104
116
|
for test_file in self.affected_tests[:10]:
|
|
105
|
-
rel_path =
|
|
117
|
+
rel_path = (
|
|
118
|
+
os.path.relpath(test_file, project_root)
|
|
119
|
+
if project_root
|
|
120
|
+
else test_file
|
|
121
|
+
)
|
|
106
122
|
lines.append(f" - {rel_path}")
|
|
107
123
|
if len(self.affected_tests) > 10:
|
|
108
124
|
lines.append(f" ... 还有 {len(self.affected_tests) - 10} 个测试文件")
|
|
109
|
-
|
|
125
|
+
|
|
110
126
|
# 接口变更
|
|
111
127
|
if self.interface_changes:
|
|
112
128
|
lines.append(f"\n接口变更 ({len(self.interface_changes)}):")
|
|
@@ -115,14 +131,16 @@ class ImpactReport:
|
|
|
115
131
|
if change.description:
|
|
116
132
|
lines.append(f" {change.description}")
|
|
117
133
|
if len(self.interface_changes) > 10:
|
|
118
|
-
lines.append(
|
|
119
|
-
|
|
134
|
+
lines.append(
|
|
135
|
+
f" ... 还有 {len(self.interface_changes) - 10} 个接口变更"
|
|
136
|
+
)
|
|
137
|
+
|
|
120
138
|
# 建议
|
|
121
139
|
if self.recommendations:
|
|
122
140
|
lines.append("\n建议:")
|
|
123
141
|
for i, rec in enumerate(self.recommendations, 1):
|
|
124
142
|
lines.append(f" {i}. {rec}")
|
|
125
|
-
|
|
143
|
+
|
|
126
144
|
lines.append("\n" + "=" * 60)
|
|
127
145
|
return "\n".join(lines)
|
|
128
146
|
|
|
@@ -130,6 +148,7 @@ class ImpactReport:
|
|
|
130
148
|
@dataclass
|
|
131
149
|
class Edit:
|
|
132
150
|
"""表示一个编辑操作"""
|
|
151
|
+
|
|
133
152
|
file_path: str
|
|
134
153
|
line_start: int
|
|
135
154
|
line_end: int
|
|
@@ -140,57 +159,57 @@ class Edit:
|
|
|
140
159
|
|
|
141
160
|
class TestDiscoverer:
|
|
142
161
|
"""测试文件发现器"""
|
|
143
|
-
|
|
162
|
+
|
|
144
163
|
# 测试文件命名模式
|
|
145
164
|
TEST_PATTERNS = {
|
|
146
|
-
|
|
147
|
-
r
|
|
148
|
-
r
|
|
165
|
+
"python": [
|
|
166
|
+
r"test_.*\.py$",
|
|
167
|
+
r".*_test\.py$",
|
|
149
168
|
],
|
|
150
|
-
|
|
151
|
-
r
|
|
152
|
-
r
|
|
169
|
+
"javascript": [
|
|
170
|
+
r".*\.test\.(js|ts|jsx|tsx)$",
|
|
171
|
+
r".*\.spec\.(js|ts|jsx|tsx)$",
|
|
153
172
|
],
|
|
154
|
-
|
|
155
|
-
r
|
|
173
|
+
"rust": [
|
|
174
|
+
r".*_test\.rs$",
|
|
156
175
|
],
|
|
157
|
-
|
|
158
|
-
r
|
|
159
|
-
r
|
|
176
|
+
"java": [
|
|
177
|
+
r".*Test\.java$",
|
|
178
|
+
r".*Tests\.java$",
|
|
160
179
|
],
|
|
161
|
-
|
|
162
|
-
r
|
|
180
|
+
"go": [
|
|
181
|
+
r".*_test\.go$",
|
|
163
182
|
],
|
|
164
183
|
}
|
|
165
|
-
|
|
184
|
+
|
|
166
185
|
def __init__(self, project_root: str):
|
|
167
186
|
self.project_root = project_root
|
|
168
|
-
|
|
187
|
+
|
|
169
188
|
def find_test_files(self, file_path: str) -> List[str]:
|
|
170
189
|
"""查找与文件相关的测试文件"""
|
|
171
190
|
test_files: List[str] = []
|
|
172
|
-
|
|
191
|
+
|
|
173
192
|
# 检测语言
|
|
174
193
|
language = self._detect_language(file_path)
|
|
175
194
|
if not language:
|
|
176
195
|
return test_files
|
|
177
|
-
|
|
196
|
+
|
|
178
197
|
# 获取测试文件模式
|
|
179
198
|
patterns = self.TEST_PATTERNS.get(language, [])
|
|
180
199
|
if not patterns:
|
|
181
200
|
return test_files
|
|
182
|
-
|
|
201
|
+
|
|
183
202
|
# 获取文件的基础名称(不含扩展名)
|
|
184
203
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
185
|
-
|
|
204
|
+
|
|
186
205
|
# 在项目根目录搜索测试文件
|
|
187
206
|
for root, dirs, files in os.walk(self.project_root):
|
|
188
207
|
# 跳过隐藏目录和常见忽略目录
|
|
189
208
|
dirs[:] = filter_walk_dirs(dirs)
|
|
190
|
-
|
|
209
|
+
|
|
191
210
|
for file in files:
|
|
192
211
|
file_path_full = os.path.join(root, file)
|
|
193
|
-
|
|
212
|
+
|
|
194
213
|
# 检查是否匹配测试文件模式
|
|
195
214
|
for pattern in patterns:
|
|
196
215
|
if re.match(pattern, file, re.IGNORECASE):
|
|
@@ -198,71 +217,73 @@ class TestDiscoverer:
|
|
|
198
217
|
if self._might_test_file(file_path_full, file_path, base_name):
|
|
199
218
|
test_files.append(file_path_full)
|
|
200
219
|
break
|
|
201
|
-
|
|
220
|
+
|
|
202
221
|
return list(set(test_files))
|
|
203
|
-
|
|
204
|
-
def _might_test_file(
|
|
222
|
+
|
|
223
|
+
def _might_test_file(
|
|
224
|
+
self, test_file: str, target_file: str, base_name: str
|
|
225
|
+
) -> bool:
|
|
205
226
|
"""判断测试文件是否可能测试目标文件"""
|
|
206
227
|
# 读取测试文件内容,查找目标文件的引用
|
|
207
228
|
try:
|
|
208
|
-
with open(test_file,
|
|
229
|
+
with open(test_file, "r", encoding="utf-8", errors="replace") as f:
|
|
209
230
|
content = f.read()
|
|
210
|
-
|
|
231
|
+
|
|
211
232
|
# 检查是否导入或引用了目标文件
|
|
212
233
|
# 简单的启发式方法:检查文件名、模块名等
|
|
213
234
|
target_base = os.path.splitext(os.path.basename(target_file))[0]
|
|
214
|
-
|
|
235
|
+
|
|
215
236
|
# 检查导入语句
|
|
216
237
|
import_patterns = [
|
|
217
|
-
rf
|
|
218
|
-
rf
|
|
219
|
-
rf
|
|
238
|
+
rf"import\s+.*{re.escape(target_base)}",
|
|
239
|
+
rf"from\s+.*{re.escape(target_base)}",
|
|
240
|
+
rf"use\s+.*{re.escape(target_base)}", # Rust
|
|
220
241
|
]
|
|
221
|
-
|
|
242
|
+
|
|
222
243
|
for pattern in import_patterns:
|
|
223
244
|
if re.search(pattern, content, re.IGNORECASE):
|
|
224
245
|
return True
|
|
225
|
-
|
|
246
|
+
|
|
226
247
|
# 检查文件名是否出现在测试文件中
|
|
227
248
|
if target_base.lower() in content.lower():
|
|
228
249
|
return True
|
|
229
|
-
|
|
250
|
+
|
|
230
251
|
except Exception:
|
|
231
252
|
pass
|
|
232
|
-
|
|
253
|
+
|
|
233
254
|
return False
|
|
234
|
-
|
|
255
|
+
|
|
235
256
|
def _detect_language(self, file_path: str) -> Optional[str]:
|
|
236
257
|
"""检测文件语言"""
|
|
237
258
|
ext = os.path.splitext(file_path)[1].lower()
|
|
238
259
|
ext_map = {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
".py": "python",
|
|
261
|
+
".js": "javascript",
|
|
262
|
+
".ts": "javascript",
|
|
263
|
+
".jsx": "javascript",
|
|
264
|
+
".tsx": "javascript",
|
|
265
|
+
".rs": "rust",
|
|
266
|
+
".java": "java",
|
|
267
|
+
".go": "go",
|
|
247
268
|
}
|
|
248
269
|
return ext_map.get(ext)
|
|
249
270
|
|
|
250
271
|
|
|
251
272
|
class ImpactAnalyzer:
|
|
252
273
|
"""编辑影响范围分析器"""
|
|
253
|
-
|
|
274
|
+
|
|
254
275
|
def __init__(self, context_manager: ContextManager):
|
|
255
276
|
self.context_manager = context_manager
|
|
256
277
|
self.project_root = context_manager.project_root
|
|
257
278
|
self.test_discoverer = TestDiscoverer(self.project_root)
|
|
258
|
-
|
|
279
|
+
|
|
259
280
|
def analyze_edit_impact(self, file_path: str, edits: List[Edit]) -> ImpactReport:
|
|
260
281
|
"""分析编辑的影响范围
|
|
261
|
-
|
|
282
|
+
|
|
262
283
|
Args:
|
|
263
284
|
file_path: 被编辑的文件路径
|
|
264
285
|
edits: 编辑操作列表
|
|
265
|
-
|
|
286
|
+
|
|
266
287
|
Returns:
|
|
267
288
|
ImpactReport: 影响分析报告
|
|
268
289
|
"""
|
|
@@ -270,7 +291,7 @@ class ImpactAnalyzer:
|
|
|
270
291
|
affected_symbols: Set[Symbol] = set()
|
|
271
292
|
affected_files: Set[str] = {file_path}
|
|
272
293
|
interface_changes: List[InterfaceChange] = []
|
|
273
|
-
|
|
294
|
+
|
|
274
295
|
# 1. 分析每个编辑的影响
|
|
275
296
|
for edit in edits:
|
|
276
297
|
# 分析符号影响
|
|
@@ -279,45 +300,47 @@ class ImpactAnalyzer:
|
|
|
279
300
|
affected_symbols.add(symbol)
|
|
280
301
|
symbol_impacts = self._analyze_symbol_impact(symbol, edit)
|
|
281
302
|
impacts.extend(symbol_impacts)
|
|
282
|
-
|
|
303
|
+
|
|
283
304
|
# 收集受影响的文件
|
|
284
305
|
for impact in symbol_impacts:
|
|
285
306
|
if impact.impact_type == ImpactType.REFERENCE:
|
|
286
307
|
affected_files.add(impact.target)
|
|
287
308
|
elif impact.impact_type == ImpactType.DEPENDENT:
|
|
288
309
|
affected_files.add(impact.target)
|
|
289
|
-
|
|
310
|
+
|
|
290
311
|
# 2. 分析依赖链影响
|
|
291
312
|
dependency_impacts = self._analyze_dependency_chain(file_path)
|
|
292
313
|
impacts.extend(dependency_impacts)
|
|
293
314
|
for impact in dependency_impacts:
|
|
294
315
|
affected_files.add(impact.target)
|
|
295
|
-
|
|
316
|
+
|
|
296
317
|
# 3. 检测接口变更
|
|
297
318
|
if edits:
|
|
298
319
|
# 需要读取文件内容来比较
|
|
299
320
|
interface_changes = self._detect_interface_changes(file_path, edits)
|
|
300
321
|
for change in interface_changes:
|
|
301
322
|
affected_files.add(change.file_path)
|
|
302
|
-
|
|
323
|
+
|
|
303
324
|
# 4. 查找相关测试
|
|
304
325
|
test_files = self.test_discoverer.find_test_files(file_path)
|
|
305
326
|
for test_file in test_files:
|
|
306
|
-
impacts.append(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
327
|
+
impacts.append(
|
|
328
|
+
Impact(
|
|
329
|
+
impact_type=ImpactType.TEST,
|
|
330
|
+
target=test_file,
|
|
331
|
+
description=f"可能测试 {os.path.basename(file_path)} 的测试文件",
|
|
332
|
+
)
|
|
333
|
+
)
|
|
311
334
|
affected_files.add(test_file)
|
|
312
|
-
|
|
335
|
+
|
|
313
336
|
# 5. 评估风险等级
|
|
314
337
|
risk_level = self._assess_risk(impacts, interface_changes)
|
|
315
|
-
|
|
338
|
+
|
|
316
339
|
# 6. 生成建议
|
|
317
340
|
recommendations = self._generate_recommendations(
|
|
318
341
|
impacts, interface_changes, affected_files, test_files
|
|
319
342
|
)
|
|
320
|
-
|
|
343
|
+
|
|
321
344
|
return ImpactReport(
|
|
322
345
|
affected_files=list(affected_files),
|
|
323
346
|
affected_symbols=list(affected_symbols),
|
|
@@ -327,255 +350,291 @@ class ImpactAnalyzer:
|
|
|
327
350
|
risk_level=risk_level,
|
|
328
351
|
recommendations=recommendations,
|
|
329
352
|
)
|
|
330
|
-
|
|
353
|
+
|
|
331
354
|
def _find_symbols_in_edit(self, file_path: str, edit: Edit) -> List[Symbol]:
|
|
332
355
|
"""查找编辑区域内的符号"""
|
|
333
356
|
symbols = self.context_manager.symbol_table.get_file_symbols(file_path)
|
|
334
|
-
|
|
357
|
+
|
|
335
358
|
# 找出在编辑范围内的符号
|
|
336
359
|
affected_symbols = []
|
|
337
360
|
for symbol in symbols:
|
|
338
361
|
# 检查符号是否与编辑区域重叠
|
|
339
|
-
if (
|
|
340
|
-
symbol.
|
|
362
|
+
if (
|
|
363
|
+
symbol.line_start <= edit.line_end
|
|
364
|
+
and symbol.line_end >= edit.line_start
|
|
365
|
+
):
|
|
341
366
|
affected_symbols.append(symbol)
|
|
342
|
-
|
|
367
|
+
|
|
343
368
|
return affected_symbols
|
|
344
|
-
|
|
369
|
+
|
|
345
370
|
def _analyze_symbol_impact(self, symbol: Symbol, edit: Edit) -> List[Impact]:
|
|
346
371
|
"""分析符号编辑的影响"""
|
|
347
372
|
impacts = []
|
|
348
|
-
|
|
373
|
+
|
|
349
374
|
# 1. 查找所有引用该符号的位置
|
|
350
375
|
references = self.context_manager.find_references(symbol.name, symbol.file_path)
|
|
351
376
|
for ref in references:
|
|
352
|
-
impacts.append(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
377
|
+
impacts.append(
|
|
378
|
+
Impact(
|
|
379
|
+
impact_type=ImpactType.REFERENCE,
|
|
380
|
+
target=ref.file_path,
|
|
381
|
+
description=f"引用符号 {symbol.name}",
|
|
382
|
+
line=ref.line,
|
|
383
|
+
severity=RiskLevel.MEDIUM
|
|
384
|
+
if symbol.kind in ("function", "class")
|
|
385
|
+
else RiskLevel.LOW,
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
360
389
|
# 2. 查找依赖该符号的其他符号(在同一文件中)
|
|
361
|
-
if symbol.kind in (
|
|
390
|
+
if symbol.kind in ("function", "class"):
|
|
362
391
|
dependents = self._find_dependent_symbols(symbol)
|
|
363
392
|
for dep in dependents:
|
|
364
|
-
impacts.append(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
impacts.append(
|
|
394
|
+
Impact(
|
|
395
|
+
impact_type=ImpactType.DEPENDENT,
|
|
396
|
+
target=dep.file_path,
|
|
397
|
+
description=f"依赖符号 {symbol.name}",
|
|
398
|
+
line=dep.line_start,
|
|
399
|
+
severity=RiskLevel.MEDIUM,
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
372
403
|
return impacts
|
|
373
|
-
|
|
404
|
+
|
|
374
405
|
def _find_dependent_symbols(self, symbol: Symbol) -> List[Symbol]:
|
|
375
406
|
"""查找依赖该符号的其他符号"""
|
|
376
407
|
dependents = []
|
|
377
|
-
|
|
408
|
+
|
|
378
409
|
# 获取同一文件中的所有符号
|
|
379
|
-
file_symbols = self.context_manager.symbol_table.get_file_symbols(
|
|
380
|
-
|
|
410
|
+
file_symbols = self.context_manager.symbol_table.get_file_symbols(
|
|
411
|
+
symbol.file_path
|
|
412
|
+
)
|
|
413
|
+
|
|
381
414
|
# 查找在符号定义之后的符号(可能使用该符号)
|
|
382
415
|
for other_symbol in file_symbols:
|
|
383
|
-
if (
|
|
384
|
-
other_symbol.
|
|
416
|
+
if (
|
|
417
|
+
other_symbol.line_start > symbol.line_end
|
|
418
|
+
and other_symbol.name != symbol.name
|
|
419
|
+
):
|
|
385
420
|
# 简单检查:如果符号名出现在其他符号的范围内,可能依赖
|
|
386
421
|
# 这里使用简单的启发式方法
|
|
387
422
|
content = self.context_manager._get_file_content(symbol.file_path)
|
|
388
423
|
if content:
|
|
389
424
|
# 提取其他符号的代码区域
|
|
390
|
-
lines = content.split(
|
|
391
|
-
if other_symbol.line_start <= len(
|
|
392
|
-
|
|
425
|
+
lines = content.split("\n")
|
|
426
|
+
if other_symbol.line_start <= len(
|
|
427
|
+
lines
|
|
428
|
+
) and other_symbol.line_end <= len(lines):
|
|
429
|
+
region = "\n".join(
|
|
430
|
+
lines[other_symbol.line_start - 1 : other_symbol.line_end]
|
|
431
|
+
)
|
|
393
432
|
if symbol.name in region:
|
|
394
433
|
dependents.append(other_symbol)
|
|
395
|
-
|
|
434
|
+
|
|
396
435
|
return dependents
|
|
397
|
-
|
|
436
|
+
|
|
398
437
|
def _analyze_dependency_chain(self, file_path: str) -> List[Impact]:
|
|
399
438
|
"""分析依赖链,找出所有可能受影响的文件"""
|
|
400
439
|
impacts = []
|
|
401
|
-
|
|
440
|
+
|
|
402
441
|
# 获取依赖该文件的所有文件(传递闭包)
|
|
403
442
|
visited = set()
|
|
404
443
|
to_process = [file_path]
|
|
405
|
-
|
|
444
|
+
|
|
406
445
|
while to_process:
|
|
407
446
|
current = to_process.pop(0)
|
|
408
447
|
if current in visited:
|
|
409
448
|
continue
|
|
410
449
|
visited.add(current)
|
|
411
|
-
|
|
450
|
+
|
|
412
451
|
dependents = self.context_manager.dependency_graph.get_dependents(current)
|
|
413
452
|
for dependent in dependents:
|
|
414
453
|
if dependent not in visited:
|
|
415
|
-
impacts.append(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
454
|
+
impacts.append(
|
|
455
|
+
Impact(
|
|
456
|
+
impact_type=ImpactType.DEPENDENCY_CHAIN,
|
|
457
|
+
target=dependent,
|
|
458
|
+
description=f"间接依赖 {os.path.basename(file_path)}",
|
|
459
|
+
severity=RiskLevel.LOW,
|
|
460
|
+
)
|
|
461
|
+
)
|
|
421
462
|
to_process.append(dependent)
|
|
422
|
-
|
|
463
|
+
|
|
423
464
|
return impacts
|
|
424
|
-
|
|
425
|
-
def _detect_interface_changes(
|
|
465
|
+
|
|
466
|
+
def _detect_interface_changes(
|
|
467
|
+
self, file_path: str, edits: List[Edit]
|
|
468
|
+
) -> List[InterfaceChange]:
|
|
426
469
|
"""检测接口变更(函数签名、类定义等)"""
|
|
427
470
|
changes: List[InterfaceChange] = []
|
|
428
|
-
|
|
471
|
+
|
|
429
472
|
# 读取文件内容
|
|
430
473
|
content_before = self._get_file_content_before_edit(file_path, edits)
|
|
431
474
|
content_after = self._get_file_content_after_edit(file_path, edits)
|
|
432
|
-
|
|
475
|
+
|
|
433
476
|
if not content_before or not content_after:
|
|
434
477
|
return changes
|
|
435
|
-
|
|
478
|
+
|
|
436
479
|
# 解析AST并比较
|
|
437
480
|
try:
|
|
438
481
|
tree_before = ast.parse(content_before, filename=file_path)
|
|
439
482
|
tree_after = ast.parse(content_after, filename=file_path)
|
|
440
|
-
|
|
483
|
+
|
|
441
484
|
# 提取函数和类定义
|
|
442
485
|
defs_before = self._extract_definitions(tree_before)
|
|
443
486
|
defs_after = self._extract_definitions(tree_after)
|
|
444
|
-
|
|
487
|
+
|
|
445
488
|
# 比较定义
|
|
446
489
|
for name, def_before in defs_before.items():
|
|
447
490
|
if name in defs_after:
|
|
448
491
|
def_after = defs_after[name]
|
|
449
|
-
change = self._compare_definition(
|
|
492
|
+
change = self._compare_definition(
|
|
493
|
+
name, def_before, def_after, file_path
|
|
494
|
+
)
|
|
450
495
|
if change:
|
|
451
496
|
changes.append(change)
|
|
452
497
|
else:
|
|
453
498
|
# 定义被删除
|
|
454
|
-
changes.append(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
499
|
+
changes.append(
|
|
500
|
+
InterfaceChange(
|
|
501
|
+
symbol_name=name,
|
|
502
|
+
change_type="removed",
|
|
503
|
+
file_path=file_path,
|
|
504
|
+
line=def_before["line"],
|
|
505
|
+
description=f"符号 {name} 被删除",
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
462
509
|
# 检查新增的定义
|
|
463
510
|
for name, def_after in defs_after.items():
|
|
464
511
|
if name not in defs_before:
|
|
465
|
-
changes.append(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
512
|
+
changes.append(
|
|
513
|
+
InterfaceChange(
|
|
514
|
+
symbol_name=name,
|
|
515
|
+
change_type="added",
|
|
516
|
+
file_path=file_path,
|
|
517
|
+
line=def_after["line"],
|
|
518
|
+
description=f"新增符号 {name}",
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
473
522
|
except SyntaxError:
|
|
474
523
|
# 如果解析失败,跳过接口变更检测
|
|
475
524
|
pass
|
|
476
|
-
|
|
525
|
+
|
|
477
526
|
return changes
|
|
478
|
-
|
|
527
|
+
|
|
479
528
|
def _extract_definitions(self, tree: ast.AST) -> Dict[str, Dict]:
|
|
480
529
|
"""从AST中提取函数和类定义"""
|
|
481
530
|
definitions = {}
|
|
482
|
-
|
|
531
|
+
|
|
483
532
|
for node in ast.walk(tree):
|
|
484
533
|
if isinstance(node, ast.FunctionDef):
|
|
485
534
|
# 提取函数签名
|
|
486
535
|
args = [arg.arg for arg in node.args.args]
|
|
487
536
|
signature = f"{node.name}({', '.join(args)})"
|
|
488
537
|
definitions[node.name] = {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
538
|
+
"type": "function",
|
|
539
|
+
"line": node.lineno,
|
|
540
|
+
"signature": signature,
|
|
541
|
+
"args": args,
|
|
542
|
+
"node": node,
|
|
494
543
|
}
|
|
495
544
|
elif isinstance(node, ast.ClassDef):
|
|
496
545
|
definitions[node.name] = {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
546
|
+
"type": "class",
|
|
547
|
+
"line": node.lineno,
|
|
548
|
+
"signature": node.name,
|
|
549
|
+
"node": node,
|
|
501
550
|
}
|
|
502
|
-
|
|
551
|
+
|
|
503
552
|
return definitions
|
|
504
|
-
|
|
505
|
-
def _compare_definition(
|
|
553
|
+
|
|
554
|
+
def _compare_definition(
|
|
555
|
+
self, name: str, def_before: Dict, def_after: Dict, file_path: str
|
|
556
|
+
) -> Optional[InterfaceChange]:
|
|
506
557
|
"""比较两个定义,检测接口变更"""
|
|
507
|
-
if def_before[
|
|
558
|
+
if def_before["type"] != def_after["type"]:
|
|
508
559
|
return InterfaceChange(
|
|
509
560
|
symbol_name=name,
|
|
510
|
-
change_type=
|
|
561
|
+
change_type="signature",
|
|
511
562
|
file_path=file_path,
|
|
512
|
-
line=def_after[
|
|
513
|
-
before=def_before[
|
|
514
|
-
after=def_after[
|
|
515
|
-
description=f"符号 {name} 的类型从 {def_before['type']} 变为 {def_after['type']}"
|
|
563
|
+
line=def_after["line"],
|
|
564
|
+
before=def_before["signature"],
|
|
565
|
+
after=def_after["signature"],
|
|
566
|
+
description=f"符号 {name} 的类型从 {def_before['type']} 变为 {def_after['type']}",
|
|
516
567
|
)
|
|
517
|
-
|
|
518
|
-
if def_before[
|
|
568
|
+
|
|
569
|
+
if def_before["type"] == "function":
|
|
519
570
|
# 比较函数参数
|
|
520
|
-
args_before = def_before.get(
|
|
521
|
-
args_after = def_after.get(
|
|
522
|
-
|
|
571
|
+
args_before = def_before.get("args", [])
|
|
572
|
+
args_after = def_after.get("args", [])
|
|
573
|
+
|
|
523
574
|
if args_before != args_after:
|
|
524
575
|
return InterfaceChange(
|
|
525
576
|
symbol_name=name,
|
|
526
|
-
change_type=
|
|
577
|
+
change_type="signature",
|
|
527
578
|
file_path=file_path,
|
|
528
|
-
line=def_after[
|
|
529
|
-
before=def_before[
|
|
530
|
-
after=def_after[
|
|
531
|
-
description=f"函数 {name} 的参数从 ({', '.join(args_before)}) 变为 ({', '.join(args_after)})"
|
|
579
|
+
line=def_after["line"],
|
|
580
|
+
before=def_before["signature"],
|
|
581
|
+
after=def_after["signature"],
|
|
582
|
+
description=f"函数 {name} 的参数从 ({', '.join(args_before)}) 变为 ({', '.join(args_after)})",
|
|
532
583
|
)
|
|
533
|
-
|
|
584
|
+
|
|
534
585
|
return None
|
|
535
|
-
|
|
536
|
-
def _get_file_content_before_edit(
|
|
586
|
+
|
|
587
|
+
def _get_file_content_before_edit(
|
|
588
|
+
self, file_path: str, edits: List[Edit]
|
|
589
|
+
) -> Optional[str]:
|
|
537
590
|
"""获取编辑前的文件内容"""
|
|
538
591
|
try:
|
|
539
|
-
with open(file_path,
|
|
592
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
540
593
|
return f.read()
|
|
541
594
|
except Exception:
|
|
542
595
|
return None
|
|
543
|
-
|
|
544
|
-
def _get_file_content_after_edit(
|
|
596
|
+
|
|
597
|
+
def _get_file_content_after_edit(
|
|
598
|
+
self, file_path: str, edits: List[Edit]
|
|
599
|
+
) -> Optional[str]:
|
|
545
600
|
"""获取编辑后的文件内容(模拟)"""
|
|
546
601
|
# 这里应该根据edits模拟编辑后的内容
|
|
547
602
|
# 为了简化,我们直接读取当前文件内容
|
|
548
603
|
# 在实际使用中,应该根据edits应用变更
|
|
549
604
|
try:
|
|
550
|
-
with open(file_path,
|
|
605
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
551
606
|
return f.read()
|
|
552
607
|
except Exception:
|
|
553
608
|
return None
|
|
554
|
-
|
|
555
|
-
def _assess_risk(
|
|
609
|
+
|
|
610
|
+
def _assess_risk(
|
|
611
|
+
self, impacts: List[Impact], interface_changes: List[InterfaceChange]
|
|
612
|
+
) -> RiskLevel:
|
|
556
613
|
"""评估编辑风险等级"""
|
|
557
614
|
# 统计高风险因素
|
|
558
615
|
high_risk_count = 0
|
|
559
616
|
medium_risk_count = 0
|
|
560
|
-
|
|
617
|
+
|
|
561
618
|
# 接口变更通常是高风险
|
|
562
619
|
if interface_changes:
|
|
563
620
|
high_risk_count += len(interface_changes)
|
|
564
|
-
|
|
621
|
+
|
|
565
622
|
# 统计影响数量
|
|
566
|
-
reference_count = sum(
|
|
623
|
+
reference_count = sum(
|
|
624
|
+
1 for i in impacts if i.impact_type == ImpactType.REFERENCE
|
|
625
|
+
)
|
|
567
626
|
if reference_count > 10:
|
|
568
627
|
high_risk_count += 1
|
|
569
628
|
elif reference_count > 5:
|
|
570
629
|
medium_risk_count += 1
|
|
571
|
-
|
|
630
|
+
|
|
572
631
|
# 检查是否有高风险的影响
|
|
573
632
|
for impact in impacts:
|
|
574
633
|
if impact.severity == RiskLevel.HIGH:
|
|
575
634
|
high_risk_count += 1
|
|
576
635
|
elif impact.severity == RiskLevel.MEDIUM:
|
|
577
636
|
medium_risk_count += 1
|
|
578
|
-
|
|
637
|
+
|
|
579
638
|
# 评估风险等级
|
|
580
639
|
if high_risk_count > 0 or medium_risk_count > 3:
|
|
581
640
|
return RiskLevel.HIGH
|
|
@@ -583,7 +642,7 @@ class ImpactAnalyzer:
|
|
|
583
642
|
return RiskLevel.MEDIUM
|
|
584
643
|
else:
|
|
585
644
|
return RiskLevel.LOW
|
|
586
|
-
|
|
645
|
+
|
|
587
646
|
def _generate_recommendations(
|
|
588
647
|
self,
|
|
589
648
|
impacts: List[Impact],
|
|
@@ -593,56 +652,58 @@ class ImpactAnalyzer:
|
|
|
593
652
|
) -> List[str]:
|
|
594
653
|
"""生成修复建议"""
|
|
595
654
|
recommendations = []
|
|
596
|
-
|
|
655
|
+
|
|
597
656
|
# 如果有接口变更,建议检查所有调用点
|
|
598
657
|
if interface_changes:
|
|
599
658
|
recommendations.append(
|
|
600
659
|
f"检测到 {len(interface_changes)} 个接口变更,请检查所有调用点并更新相关代码"
|
|
601
660
|
)
|
|
602
|
-
|
|
661
|
+
|
|
603
662
|
# 如果有测试文件,建议运行测试
|
|
604
663
|
if test_files:
|
|
605
664
|
recommendations.append(
|
|
606
665
|
f"发现 {len(test_files)} 个相关测试文件,建议运行测试确保功能正常"
|
|
607
666
|
)
|
|
608
|
-
|
|
667
|
+
|
|
609
668
|
# 如果影响文件较多,建议增量测试
|
|
610
669
|
if len(affected_files) > 5:
|
|
611
670
|
recommendations.append(
|
|
612
671
|
f"编辑影响了 {len(affected_files)} 个文件,建议进行增量测试"
|
|
613
672
|
)
|
|
614
|
-
|
|
673
|
+
|
|
615
674
|
# 如果有大量引用,建议代码审查
|
|
616
|
-
reference_count = sum(
|
|
675
|
+
reference_count = sum(
|
|
676
|
+
1 for i in impacts if i.impact_type == ImpactType.REFERENCE
|
|
677
|
+
)
|
|
617
678
|
if reference_count > 10:
|
|
618
679
|
recommendations.append(
|
|
619
680
|
f"检测到 {reference_count} 个符号引用,建议进行代码审查"
|
|
620
681
|
)
|
|
621
|
-
|
|
682
|
+
|
|
622
683
|
if not recommendations:
|
|
623
684
|
recommendations.append("编辑影响范围较小,建议进行基本测试")
|
|
624
|
-
|
|
685
|
+
|
|
625
686
|
return recommendations
|
|
626
687
|
|
|
627
688
|
|
|
628
689
|
def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
629
690
|
"""从git diff中解析编辑操作
|
|
630
|
-
|
|
691
|
+
|
|
631
692
|
Args:
|
|
632
693
|
file_path: 文件路径
|
|
633
694
|
project_root: 项目根目录
|
|
634
|
-
|
|
695
|
+
|
|
635
696
|
Returns:
|
|
636
697
|
List[Edit]: 编辑操作列表
|
|
637
698
|
"""
|
|
638
699
|
edits: List[Edit] = []
|
|
639
|
-
|
|
700
|
+
|
|
640
701
|
try:
|
|
641
702
|
# 获取文件的git diff
|
|
642
703
|
abs_path = os.path.abspath(file_path)
|
|
643
704
|
if not os.path.exists(abs_path):
|
|
644
705
|
return edits
|
|
645
|
-
|
|
706
|
+
|
|
646
707
|
# 检查是否有git仓库
|
|
647
708
|
try:
|
|
648
709
|
subprocess.run(
|
|
@@ -655,7 +716,7 @@ def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
|
655
716
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
656
717
|
# 不是git仓库或git不可用,返回空列表
|
|
657
718
|
return edits
|
|
658
|
-
|
|
719
|
+
|
|
659
720
|
# 获取HEAD的hash
|
|
660
721
|
try:
|
|
661
722
|
result = subprocess.run(
|
|
@@ -668,7 +729,7 @@ def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
|
668
729
|
head_exists = result.returncode == 0 and result.stdout.strip()
|
|
669
730
|
except Exception:
|
|
670
731
|
head_exists = False
|
|
671
|
-
|
|
732
|
+
|
|
672
733
|
# 临时添加文件到git索引(如果是新文件)
|
|
673
734
|
subprocess.run(
|
|
674
735
|
["git", "add", "-N", "--", abs_path],
|
|
@@ -677,7 +738,7 @@ def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
|
677
738
|
stdout=subprocess.DEVNULL,
|
|
678
739
|
stderr=subprocess.DEVNULL,
|
|
679
740
|
)
|
|
680
|
-
|
|
741
|
+
|
|
681
742
|
try:
|
|
682
743
|
# 获取diff
|
|
683
744
|
cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", abs_path]
|
|
@@ -690,79 +751,93 @@ def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
|
690
751
|
errors="replace",
|
|
691
752
|
check=False,
|
|
692
753
|
)
|
|
693
|
-
|
|
754
|
+
|
|
694
755
|
if result.returncode != 0 or not result.stdout:
|
|
695
756
|
return edits
|
|
696
|
-
|
|
757
|
+
|
|
697
758
|
diff_text = result.stdout
|
|
698
|
-
|
|
759
|
+
|
|
699
760
|
# 解析diff文本
|
|
700
|
-
lines = diff_text.split(
|
|
761
|
+
lines = diff_text.split("\n")
|
|
701
762
|
current_hunk_start = None
|
|
702
763
|
current_line_num: Optional[int] = None
|
|
703
764
|
before_lines: List[str] = []
|
|
704
765
|
after_lines: List[str] = []
|
|
705
766
|
in_hunk = False
|
|
706
|
-
|
|
767
|
+
|
|
707
768
|
for line in lines:
|
|
708
769
|
# 解析hunk header: @@ -start,count +start,count @@
|
|
709
|
-
if line.startswith(
|
|
770
|
+
if line.startswith("@@"):
|
|
710
771
|
# 保存之前的hunk
|
|
711
772
|
if in_hunk and current_hunk_start is not None:
|
|
712
773
|
if before_lines or after_lines:
|
|
713
|
-
edits.append(
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
774
|
+
edits.append(
|
|
775
|
+
Edit(
|
|
776
|
+
file_path=abs_path,
|
|
777
|
+
line_start=current_hunk_start,
|
|
778
|
+
line_end=current_hunk_start + len(after_lines) - 1
|
|
779
|
+
if after_lines
|
|
780
|
+
else current_hunk_start,
|
|
781
|
+
before="\n".join(before_lines),
|
|
782
|
+
after="\n".join(after_lines),
|
|
783
|
+
edit_type="modify"
|
|
784
|
+
if before_lines and after_lines
|
|
785
|
+
else ("delete" if before_lines else "add"),
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
|
|
722
789
|
# 解析新的hunk
|
|
723
|
-
match = re.search(
|
|
790
|
+
match = re.search(
|
|
791
|
+
r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line
|
|
792
|
+
)
|
|
724
793
|
if match:
|
|
725
794
|
old_start = int(match.group(1))
|
|
726
795
|
new_start = int(match.group(3))
|
|
727
|
-
|
|
796
|
+
|
|
728
797
|
current_hunk_start = new_start
|
|
729
798
|
current_line_num = old_start
|
|
730
799
|
before_lines = []
|
|
731
800
|
after_lines = []
|
|
732
801
|
in_hunk = True
|
|
733
802
|
continue
|
|
734
|
-
|
|
803
|
+
|
|
735
804
|
if not in_hunk:
|
|
736
805
|
continue
|
|
737
|
-
|
|
806
|
+
|
|
738
807
|
# 解析diff行
|
|
739
|
-
if line.startswith(
|
|
808
|
+
if line.startswith("-") and not line.startswith("---"):
|
|
740
809
|
# 删除的行
|
|
741
810
|
before_lines.append(line[1:])
|
|
742
811
|
if current_line_num is not None:
|
|
743
812
|
current_line_num += 1
|
|
744
|
-
elif line.startswith(
|
|
813
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
745
814
|
# 新增的行
|
|
746
815
|
after_lines.append(line[1:])
|
|
747
|
-
elif line.startswith(
|
|
816
|
+
elif line.startswith(" "):
|
|
748
817
|
# 未改变的行
|
|
749
818
|
before_lines.append(line[1:])
|
|
750
819
|
after_lines.append(line[1:])
|
|
751
820
|
if current_line_num is not None:
|
|
752
821
|
current_line_num += 1
|
|
753
|
-
|
|
822
|
+
|
|
754
823
|
# 保存最后一个hunk
|
|
755
824
|
if in_hunk and current_hunk_start is not None:
|
|
756
825
|
if before_lines or after_lines:
|
|
757
|
-
edits.append(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
826
|
+
edits.append(
|
|
827
|
+
Edit(
|
|
828
|
+
file_path=abs_path,
|
|
829
|
+
line_start=current_hunk_start,
|
|
830
|
+
line_end=current_hunk_start + len(after_lines) - 1
|
|
831
|
+
if after_lines
|
|
832
|
+
else current_hunk_start,
|
|
833
|
+
before="\n".join(before_lines),
|
|
834
|
+
after="\n".join(after_lines),
|
|
835
|
+
edit_type="modify"
|
|
836
|
+
if before_lines and after_lines
|
|
837
|
+
else ("delete" if before_lines else "add"),
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
|
|
766
841
|
finally:
|
|
767
842
|
# 清理临时添加的文件
|
|
768
843
|
subprocess.run(
|
|
@@ -772,10 +847,9 @@ def parse_git_diff_to_edits(file_path: str, project_root: str) -> List[Edit]:
|
|
|
772
847
|
stdout=subprocess.DEVNULL,
|
|
773
848
|
stderr=subprocess.DEVNULL,
|
|
774
849
|
)
|
|
775
|
-
|
|
850
|
+
|
|
776
851
|
except Exception:
|
|
777
852
|
# 解析失败时返回空列表
|
|
778
853
|
pass
|
|
779
|
-
|
|
780
|
-
return edits
|
|
781
854
|
|
|
855
|
+
return edits
|