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