coverage-tool 1.0.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 (47) hide show
  1. coverage_tool/__init__.py +7 -0
  2. coverage_tool/analyzers/__init__.py +14 -0
  3. coverage_tool/analyzers/dependency.py +513 -0
  4. coverage_tool/converters/__init__.py +60 -0
  5. coverage_tool/converters/base.py +83 -0
  6. coverage_tool/converters/cunit_converter.py +47 -0
  7. coverage_tool/converters/factory.py +36 -0
  8. coverage_tool/converters/generic_converter.py +62 -0
  9. coverage_tool/converters/go_converter.py +178 -0
  10. coverage_tool/converters/gtest_converter.py +63 -0
  11. coverage_tool/core/__init__.py +18 -0
  12. coverage_tool/core/config.py +66 -0
  13. coverage_tool/core/reporter.py +270 -0
  14. coverage_tool/example.py +102 -0
  15. coverage_tool/handlers/__init__.py +13 -0
  16. coverage_tool/handlers/compilation.py +322 -0
  17. coverage_tool/handlers/env_checker.py +312 -0
  18. coverage_tool/main.py +559 -0
  19. coverage_tool/parsers/__init__.py +15 -0
  20. coverage_tool/parsers/junit.py +172 -0
  21. coverage_tool/runners/__init__.py +26 -0
  22. coverage_tool/runners/base.py +249 -0
  23. coverage_tool/runners/c_runner.py +66 -0
  24. coverage_tool/runners/cpp_runner.py +65 -0
  25. coverage_tool/runners/factory.py +41 -0
  26. coverage_tool/runners/go_runner.py +158 -0
  27. coverage_tool/runners/java_runner.py +54 -0
  28. coverage_tool/runners/python_runner.py +99 -0
  29. coverage_tool/test_removers/__init__.py +38 -0
  30. coverage_tool/test_removers/base.py +233 -0
  31. coverage_tool/test_removers/c_remover.py +210 -0
  32. coverage_tool/test_removers/cpp_remover.py +272 -0
  33. coverage_tool/test_removers/factory.py +45 -0
  34. coverage_tool/test_removers/go_remover.py +112 -0
  35. coverage_tool/test_removers/java_remover.py +216 -0
  36. coverage_tool/test_removers/python_remover.py +172 -0
  37. coverage_tool/utils/__init__.py +23 -0
  38. coverage_tool/utils/dependency_graph.py +52 -0
  39. coverage_tool/utils/file_backup.py +112 -0
  40. coverage_tool/utils/helpers.py +89 -0
  41. coverage_tool/utils/logger.py +54 -0
  42. coverage_tool/utils/progress.py +41 -0
  43. coverage_tool-1.0.0.dist-info/METADATA +484 -0
  44. coverage_tool-1.0.0.dist-info/RECORD +47 -0
  45. coverage_tool-1.0.0.dist-info/WHEEL +5 -0
  46. coverage_tool-1.0.0.dist-info/entry_points.txt +2 -0
  47. coverage_tool-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Java测试删除器
4
+ 使用正则表达式解析Java测试文件
5
+ """
6
+
7
+ import re
8
+ from typing import List, Optional
9
+ from pathlib import Path
10
+
11
+ from coverage_tool.test_removers.base import BaseSmartTestRemover, TestFunction
12
+ from coverage_tool.parsers import TestCase
13
+
14
+
15
+ class JavaSmartTestRemover(BaseSmartTestRemover):
16
+ """Java智能测试删除器"""
17
+
18
+ # 预编译正则表达式提高性能
19
+ TEST_METHOD_PATTERN = re.compile(
20
+ r'(@Test(?:\([^)]*\))?\s*)?' # @Test 注解(可选)
21
+ r'(?:@\w+\s*)*' # 其他注解
22
+ r'(?:public\s+)?' # 访问修饰符
23
+ r'(?:void|static)?\s+' # 返回类型
24
+ r'(\w+)\s*' # 方法名
25
+ r'\([^)]*\)\s*' # 参数列表
26
+ r'\{', # 方法开始
27
+ re.MULTILINE | re.DOTALL
28
+ )
29
+
30
+ def _is_build_failure(self, test_name: str) -> bool:
31
+ """检查是否是Java构建失败的特殊测试名称"""
32
+ build_failure_patterns = [
33
+ '[build failed]',
34
+ '[compilation failed]',
35
+ 'Compilation failure',
36
+ 'cannot find symbol',
37
+ 'package does not exist',
38
+ 'class not found',
39
+ ]
40
+ return any(pattern in test_name for pattern in build_failure_patterns)
41
+
42
+ def remove_all_tests_in_package(self, package_path: str) -> tuple:
43
+ """
44
+ 删除指定Java包内的所有测试文件
45
+ 用于处理包级编译错误
46
+ """
47
+ deleted_count = 0
48
+ deleted_files = []
49
+
50
+ # Java包路径格式: com.example.test
51
+ package_parts = package_path.split('.')
52
+
53
+ if len(package_parts) > 0:
54
+ # 将包路径转换为目录路径
55
+ for i in range(len(package_parts)):
56
+ subdir = '/'.join(package_parts[i:])
57
+
58
+ # 搜索可能的目录结构
59
+ search_paths = [
60
+ Path(self.project_dir) / subdir,
61
+ Path(self.project_dir) / 'src' / 'test' / 'java' / subdir,
62
+ Path(self.project_dir) / 'test' / subdir,
63
+ Path(self.project_dir) / 'tests' / subdir,
64
+ ]
65
+
66
+ for dir_path in search_paths:
67
+ if dir_path.exists() and dir_path.is_dir():
68
+ # 查找该目录下的所有测试文件
69
+ test_files = list(dir_path.glob("*Test.java")) + list(dir_path.glob("Test*.java"))
70
+
71
+ if test_files:
72
+ self.logger.info(f"[编译错误] 在包 {package_path} 目录 {dir_path} 发现 {len(test_files)} 个测试文件")
73
+
74
+ for test_file in test_files:
75
+ try:
76
+ # 备份文件
77
+ self.backup_manager.backup_file(str(test_file))
78
+ # 删除文件
79
+ test_file.unlink()
80
+ deleted_count += 1
81
+ deleted_files.append(str(test_file))
82
+ self.logger.info(f"[编译错误] ✓ 已删除测试文件: {test_file}")
83
+ except Exception as e:
84
+ self.logger.error(f"[编译错误] 删除文件失败: {test_file}: {e}")
85
+
86
+ return deleted_count, deleted_files
87
+
88
+ # 如果没找到,尝试递归搜索
89
+ for search_pattern in ["**/*Test.java", "**/Test*.java"]:
90
+ for test_file in Path(self.project_dir).rglob(search_pattern):
91
+ try:
92
+ with open(test_file, 'r', encoding='utf-8') as f:
93
+ content = f.read()
94
+ # 检查文件是否包含该包的导入或引用
95
+ if f"package {package_path}" in content or f"import {package_path}" in content:
96
+ self.backup_manager.backup_file(str(test_file))
97
+ test_file.unlink()
98
+ deleted_count += 1
99
+ deleted_files.append(str(test_file))
100
+ self.logger.info(f"[编译错误] ✓ 已删除测试文件: {test_file}")
101
+ except:
102
+ continue
103
+
104
+ return deleted_count, deleted_files
105
+
106
+ def find_test_functions(self, file_path: str) -> List[TestFunction]:
107
+ """查找Java测试方法"""
108
+ test_methods = []
109
+
110
+ try:
111
+ with open(file_path, 'r', encoding='utf-8') as f:
112
+ content = f.read()
113
+
114
+ for match in self.TEST_METHOD_PATTERN.finditer(content):
115
+ has_test_annotation = match.group(1) is not None
116
+ method_name = match.group(2)
117
+
118
+ # 如果方法名以 test 开头或有@Test注解
119
+ if has_test_annotation or method_name.startswith('test'):
120
+ # 计算行号
121
+ start_pos = match.start()
122
+ line_num = content[:start_pos].count('\n') + 1
123
+
124
+ # 找到方法结束位置
125
+ brace_count = 0
126
+ end_pos = match.end()
127
+ for i, char in enumerate(content[match.end():]):
128
+ if char == '{':
129
+ brace_count += 1
130
+ elif char == '}':
131
+ brace_count -= 1
132
+ if brace_count == 0:
133
+ end_pos = match.end() + i + 1
134
+ break
135
+
136
+ end_line = content[:end_pos].count('\n') + 1
137
+
138
+ test_func = TestFunction(
139
+ name=method_name,
140
+ file_path=file_path,
141
+ start_line=line_num,
142
+ end_line=end_line,
143
+ decorators=['Test'] if has_test_annotation else []
144
+ )
145
+ test_methods.append(test_func)
146
+
147
+ except Exception as e:
148
+ self.logger.error(f"解析Java文件失败 {file_path}: {e}")
149
+
150
+ return test_methods
151
+
152
+ def _get_deletion_range(self, test_func: TestFunction, lines: List[str]) -> tuple:
153
+ """
154
+ 获取Java测试方法的删除范围
155
+ 考虑注解
156
+ """
157
+ start_idx = self._adjust_start_for_decorators(
158
+ test_func, lines, comment_prefixes=['//']
159
+ )
160
+ end_idx = test_func.end_line
161
+ return start_idx, end_idx
162
+
163
+ def find_test_file(self, test_case: TestCase) -> Optional[str]:
164
+ """查找Java测试文件"""
165
+ classname = test_case.classname
166
+ test_name = test_case.name
167
+
168
+ # 处理特殊的构建失败错误
169
+ if self._is_build_failure(test_name):
170
+ self.logger.info(f"[编译错误] 检测到Java编译失败: {test_name}")
171
+ # 如果是编译失败,classname通常包含包路径
172
+ # 尝试从包名推断目录
173
+ package_parts = classname.split('.')
174
+ if len(package_parts) > 1:
175
+ # 尝试不同的包目录组合
176
+ for i in range(len(package_parts)):
177
+ subdir = '/'.join(package_parts[i:])
178
+ search_paths = [
179
+ Path(self.project_dir) / subdir,
180
+ Path(self.project_dir) / 'src' / 'test' / 'java' / subdir,
181
+ Path(self.project_dir) / 'test' / subdir,
182
+ ]
183
+ for dir_path in search_paths:
184
+ if dir_path.exists() and dir_path.is_dir():
185
+ # 查找该目录下的测试文件
186
+ for test_file in dir_path.glob("*Test.java"):
187
+ if test_file.exists():
188
+ self.logger.info(f"[编译错误] 找到包 {classname} 的测试文件: {test_file}")
189
+ return str(test_file)
190
+
191
+ parts = classname.split('.')
192
+
193
+ if len(parts) > 1:
194
+ # 完整的包路径
195
+ package_path = '/'.join(parts[:-1])
196
+ class_name = parts[-1]
197
+ patterns = [
198
+ f"{package_path}/{class_name}Test.java",
199
+ f"{package_path}/Test{class_name}.java",
200
+ f"test/{package_path}/{class_name}Test.java",
201
+ f"src/test/java/{package_path}/{class_name}Test.java",
202
+ ]
203
+ else:
204
+ class_name = parts[0]
205
+ patterns = [
206
+ f"{class_name}Test.java",
207
+ f"Test{class_name}.java",
208
+ f"test/{class_name}Test.java",
209
+ ]
210
+
211
+ for pattern in patterns:
212
+ file_path = Path(self.project_dir) / pattern
213
+ if file_path.exists():
214
+ return str(file_path)
215
+
216
+ return None
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Python测试删除器
4
+ 使用AST解析Python测试文件
5
+ """
6
+
7
+ import ast
8
+ import os
9
+ from typing import List, Optional
10
+ from pathlib import Path
11
+
12
+ from coverage_tool.test_removers.base import BaseSmartTestRemover, TestFunction
13
+ from coverage_tool.parsers import TestCase
14
+
15
+
16
+ class PythonSmartTestRemover(BaseSmartTestRemover):
17
+ """Python智能测试删除器 - 使用AST解析"""
18
+
19
+ def find_test_functions(self, file_path: str) -> List[TestFunction]:
20
+ """使用AST查找Python测试函数"""
21
+ test_functions = []
22
+
23
+ try:
24
+ with open(file_path, 'r', encoding='utf-8') as f:
25
+ content = f.read()
26
+
27
+ tree = ast.parse(content)
28
+
29
+ for node in ast.walk(tree):
30
+ if isinstance(node, ast.FunctionDef):
31
+ # 检查是否是测试函数
32
+ if self._is_test_function(node):
33
+ # 获取函数在文件中的位置
34
+ start_line = node.lineno
35
+ end_line = node.end_lineno if hasattr(node, 'end_lineno') else start_line
36
+
37
+ # 获取装饰器
38
+ decorators = []
39
+ for decorator in node.decorator_list:
40
+ if isinstance(decorator, ast.Name):
41
+ decorators.append(decorator.id)
42
+ elif isinstance(decorator, ast.Call):
43
+ if isinstance(decorator.func, ast.Name):
44
+ decorators.append(decorator.func.id)
45
+
46
+ test_func = TestFunction(
47
+ name=node.name,
48
+ file_path=file_path,
49
+ start_line=start_line,
50
+ end_line=end_line or start_line,
51
+ decorators=decorators,
52
+ is_class_method=self._is_class_method(node, tree),
53
+ class_name=self._get_class_name(node, tree)
54
+ )
55
+ test_functions.append(test_func)
56
+
57
+ elif isinstance(node, ast.ClassDef):
58
+ # 处理测试类中的方法
59
+ if self._is_test_class(node):
60
+ for item in node.body:
61
+ if isinstance(item, ast.FunctionDef) and self._is_test_method(item):
62
+ start_line = item.lineno
63
+ end_line = item.end_lineno if hasattr(item, 'end_lineno') else start_line
64
+
65
+ test_func = TestFunction(
66
+ name=item.name,
67
+ file_path=file_path,
68
+ start_line=start_line,
69
+ end_line=end_line or start_line,
70
+ is_class_method=True,
71
+ class_name=node.name
72
+ )
73
+ test_functions.append(test_func)
74
+
75
+ except SyntaxError as e:
76
+ self.logger.error(f"Python语法错误 {file_path}: {e}")
77
+ except Exception as e:
78
+ self.logger.error(f"解析Python文件失败 {file_path}: {e}")
79
+
80
+ return test_functions
81
+
82
+ def _is_test_function(self, node: ast.FunctionDef) -> bool:
83
+ """检查是否是测试函数"""
84
+ # 函数名以 test_ 开头
85
+ if node.name.startswith('test_'):
86
+ return True
87
+
88
+ # 检查是否有测试相关的装饰器
89
+ test_decorators = {'pytest', 'fixture', 'parametrize'}
90
+ for decorator in node.decorator_list:
91
+ if isinstance(decorator, ast.Name):
92
+ if decorator.id.lower() in test_decorators or decorator.id.startswith('pytest'):
93
+ return True
94
+
95
+ return False
96
+
97
+ def _is_test_class(self, node: ast.ClassDef) -> bool:
98
+ """检查是否是测试类"""
99
+ # 类名以 Test 开头
100
+ if node.name.startswith('Test'):
101
+ return True
102
+
103
+ # 检查继承
104
+ for base in node.bases:
105
+ if isinstance(base, ast.Name):
106
+ if 'test' in base.id.lower():
107
+ return True
108
+
109
+ return False
110
+
111
+ def _is_test_method(self, node: ast.FunctionDef) -> bool:
112
+ """检查是否是测试方法"""
113
+ return node.name.startswith('test_')
114
+
115
+ def _is_class_method(self, node: ast.FunctionDef, tree: ast.AST) -> bool:
116
+ """检查函数是否是类方法"""
117
+ for parent in ast.walk(tree):
118
+ if isinstance(parent, ast.ClassDef):
119
+ for item in parent.body:
120
+ if isinstance(item, ast.FunctionDef) and item.name == node.name:
121
+ return True
122
+ return False
123
+
124
+ def _get_class_name(self, node: ast.FunctionDef, tree: ast.AST) -> Optional[str]:
125
+ """获取函数所属的类名"""
126
+ for parent in ast.walk(tree):
127
+ if isinstance(parent, ast.ClassDef):
128
+ for item in parent.body:
129
+ if isinstance(item, ast.FunctionDef) and item.name == node.name:
130
+ return parent.name
131
+ return None
132
+
133
+ def _get_deletion_range(self, test_func: TestFunction, lines: List[str]) -> tuple:
134
+ """
135
+ 获取Python测试函数的删除范围
136
+ 考虑装饰器和空行
137
+ """
138
+ start_idx = self._adjust_start_for_decorators(
139
+ test_func, lines, comment_prefixes=['#']
140
+ )
141
+ end_idx = test_func.end_line
142
+ return start_idx, end_idx
143
+
144
+ def find_test_file(self, test_case: TestCase) -> Optional[str]:
145
+ """查找Python测试文件"""
146
+ classname = test_case.classname
147
+
148
+ # 可能的文件模式
149
+ patterns = [
150
+ f"{classname.replace('.', '/')}.py",
151
+ f"test_{classname.replace('.', '_')}.py",
152
+ f"{classname.replace('.', '_')}_test.py",
153
+ f"tests/{classname.replace('.', '/')}.py",
154
+ f"tests/test_{classname.replace('.', '_')}.py",
155
+ ]
156
+
157
+ for pattern in patterns:
158
+ file_path = Path(self.project_dir) / pattern
159
+ if file_path.exists():
160
+ return str(file_path)
161
+
162
+ # 搜索包含测试函数的文件
163
+ for test_file in Path(self.project_dir).rglob("*test*.py"):
164
+ try:
165
+ with open(test_file, 'r', encoding='utf-8') as f:
166
+ content = f.read()
167
+ if f"def {test_case.name}(" in content:
168
+ return str(test_file)
169
+ except:
170
+ continue
171
+
172
+ return None
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 工具类集合包
4
+ 提供文件备份、日志、依赖分析等通用功能
5
+ """
6
+
7
+ from .file_backup import FileBackup, FileBackupManager
8
+ from .dependency_graph import DependencyGraph
9
+ from .logger import Logger
10
+ from .progress import ProgressTracker
11
+ from .helpers import temporary_directory, safe_delete_file, find_files_by_patterns, parse_error_locations
12
+
13
+ __all__ = [
14
+ 'FileBackup',
15
+ 'FileBackupManager',
16
+ 'DependencyGraph',
17
+ 'Logger',
18
+ 'ProgressTracker',
19
+ 'temporary_directory',
20
+ 'safe_delete_file',
21
+ 'find_files_by_patterns',
22
+ 'parse_error_locations',
23
+ ]
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 依赖关系图模块
4
+ """
5
+
6
+ from typing import Dict, Set
7
+
8
+
9
+ class DependencyGraph:
10
+ """依赖关系图"""
11
+
12
+ def __init__(self):
13
+ self.dependencies: Dict[str, Set[str]] = {} # file -> set of files it depends on
14
+ self.dependents: Dict[str, Set[str]] = {} # file -> set of files that depend on it
15
+
16
+ def add_dependency(self, file_path: str, depends_on: str):
17
+ """添加依赖关系"""
18
+ if file_path not in self.dependencies:
19
+ self.dependencies[file_path] = set()
20
+ self.dependencies[file_path].add(depends_on)
21
+
22
+ if depends_on not in self.dependents:
23
+ self.dependents[depends_on] = set()
24
+ self.dependents[depends_on].add(file_path)
25
+
26
+ def get_dependents(self, file_path: str) -> Set[str]:
27
+ """获取依赖于该文件的文件列表"""
28
+ return self.dependents.get(file_path, set())
29
+
30
+ def get_dependencies(self, file_path: str) -> Set[str]:
31
+ """获取该文件依赖的文件列表"""
32
+ return self.dependencies.get(file_path, set())
33
+
34
+ def get_impact_files(self, file_path: str) -> Set[str]:
35
+ """获取删除该文件会影响的所有文件(递归)"""
36
+ impacted = set()
37
+ to_process = {file_path}
38
+ processed = set()
39
+
40
+ while to_process:
41
+ current = to_process.pop()
42
+ if current in processed:
43
+ continue
44
+ processed.add(current)
45
+
46
+ dependents = self.get_dependents(current)
47
+ for dependent in dependents:
48
+ if dependent != file_path:
49
+ impacted.add(dependent)
50
+ to_process.add(dependent)
51
+
52
+ return impacted
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 文件备份管理模块
4
+ """
5
+
6
+ import os
7
+ import shutil
8
+ import hashlib
9
+ import tempfile
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from typing import Dict, List, Optional
13
+
14
+
15
+ @dataclass
16
+ class FileBackup:
17
+ """文件备份记录"""
18
+ original_path: str
19
+ backup_path: str
20
+ checksum: str
21
+ timestamp: datetime = field(default_factory=datetime.now)
22
+
23
+
24
+ class FileBackupManager:
25
+ """文件备份管理器"""
26
+
27
+ def __init__(self, backup_dir: Optional[str] = None):
28
+ self.backup_dir = backup_dir or tempfile.mkdtemp(prefix="coverage_backup_")
29
+ self.backups: Dict[str, FileBackup] = {}
30
+ self._ensure_backup_dir()
31
+
32
+ def _ensure_backup_dir(self):
33
+ """确保备份目录存在"""
34
+ if not os.path.exists(self.backup_dir):
35
+ os.makedirs(self.backup_dir, exist_ok=True)
36
+
37
+ def _calculate_checksum(self, file_path: str) -> str:
38
+ """计算文件校验和"""
39
+ try:
40
+ with open(file_path, 'rb') as f:
41
+ return hashlib.md5(f.read()).hexdigest()
42
+ except:
43
+ return ""
44
+
45
+ def backup_file(self, file_path: str) -> Optional[FileBackup]:
46
+ """备份单个文件"""
47
+ if not os.path.exists(file_path):
48
+ return None
49
+
50
+ # 计算相对路径
51
+ abs_path = os.path.abspath(file_path)
52
+ backup_file_path = os.path.join(
53
+ self.backup_dir,
54
+ abs_path.replace(os.sep, '_').replace(':', '_')
55
+ )
56
+
57
+ # 确保备份目录存在
58
+ os.makedirs(os.path.dirname(backup_file_path), exist_ok=True)
59
+
60
+ # 复制文件
61
+ shutil.copy2(abs_path, backup_file_path)
62
+
63
+ # 创建备份记录
64
+ backup = FileBackup(
65
+ original_path=abs_path,
66
+ backup_path=backup_file_path,
67
+ checksum=self._calculate_checksum(abs_path)
68
+ )
69
+
70
+ self.backups[abs_path] = backup
71
+ return backup
72
+
73
+ def backup_files(self, file_paths: List[str]) -> List[FileBackup]:
74
+ """批量备份文件"""
75
+ backups = []
76
+ for file_path in file_paths:
77
+ backup = self.backup_file(file_path)
78
+ if backup:
79
+ backups.append(backup)
80
+ return backups
81
+
82
+ def restore_file(self, file_path: str) -> bool:
83
+ """恢复单个文件"""
84
+ abs_path = os.path.abspath(file_path)
85
+ if abs_path not in self.backups:
86
+ return False
87
+
88
+ backup = self.backups[abs_path]
89
+ if os.path.exists(backup.backup_path):
90
+ shutil.copy2(backup.backup_path, abs_path)
91
+ return True
92
+ return False
93
+
94
+ def restore_all(self) -> List[str]:
95
+ """恢复所有备份的文件"""
96
+ restored = []
97
+ for file_path in self.backups:
98
+ if self.restore_file(file_path):
99
+ restored.append(file_path)
100
+ return restored
101
+
102
+ def cleanup(self):
103
+ """清理备份目录"""
104
+ if os.path.exists(self.backup_dir):
105
+ shutil.rmtree(self.backup_dir, ignore_errors=True)
106
+ self.backups.clear()
107
+
108
+ def __enter__(self):
109
+ return self
110
+
111
+ def __exit__(self, exc_type, exc_val, exc_tb):
112
+ self.cleanup()
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 辅助函数模块
4
+ """
5
+
6
+ import os
7
+ import shutil
8
+ import re
9
+ import tempfile
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import List, Dict, Optional
13
+
14
+ from .file_backup import FileBackupManager
15
+
16
+
17
+ @contextmanager
18
+ def temporary_directory():
19
+ """临时目录上下文管理器"""
20
+ temp_dir = tempfile.mkdtemp()
21
+ try:
22
+ yield temp_dir
23
+ finally:
24
+ shutil.rmtree(temp_dir, ignore_errors=True)
25
+
26
+
27
+ def safe_delete_file(file_path: str, backup_manager: Optional[FileBackupManager] = None) -> bool:
28
+ """安全删除文件(先备份)"""
29
+ if not os.path.exists(file_path):
30
+ return False
31
+
32
+ # 先备份
33
+ if backup_manager:
34
+ backup_manager.backup_file(file_path)
35
+
36
+ try:
37
+ os.remove(file_path)
38
+ return True
39
+ except Exception as e:
40
+ print(f"删除文件失败 {file_path}: {e}")
41
+ return False
42
+
43
+
44
+ def find_files_by_patterns(directory: str, patterns: List[str]) -> List[str]:
45
+ """根据多个模式查找文件"""
46
+ found_files = set()
47
+ path = Path(directory)
48
+
49
+ for pattern in patterns:
50
+ for file_path in path.rglob(pattern):
51
+ if file_path.is_file():
52
+ found_files.add(str(file_path))
53
+
54
+ return sorted(list(found_files))
55
+
56
+
57
+ def parse_error_locations(errors: List[str], project_dir: str) -> Dict[str, List[int]]:
58
+ """从错误信息中解析出错的文件和行号"""
59
+ error_locations: Dict[str, List[int]] = {}
60
+
61
+ # 各种编译错误模式
62
+ patterns = [
63
+ r'([^\s]+\.\w+):(\d+):', # file:line:col
64
+ r'([^\s]+\.\w+)\((\d+)\)', # file(line)
65
+ r'File "([^"]+)", line (\d+)', # Python style
66
+ r'([^\s]+\.\w+):(\d+)', # file:line
67
+ ]
68
+
69
+ for error in errors:
70
+ for pattern in patterns:
71
+ matches = re.findall(pattern, error)
72
+ for match in matches:
73
+ if isinstance(match, tuple):
74
+ file_path, line_str = match
75
+ else:
76
+ continue
77
+
78
+ # 规范化路径
79
+ if not os.path.isabs(file_path):
80
+ file_path = os.path.join(project_dir, file_path)
81
+ file_path = os.path.normpath(file_path)
82
+
83
+ if os.path.exists(file_path):
84
+ line_num = int(line_str)
85
+ if file_path not in error_locations:
86
+ error_locations[file_path] = []
87
+ error_locations[file_path].append(line_num)
88
+
89
+ return error_locations