pyrefactor 1.0.4__tar.gz → 1.0.6__tar.gz
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.
- {pyrefactor-1.0.4/src/pyrefactor.egg-info → pyrefactor-1.0.6}/PKG-INFO +1 -1
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/__init__.py +1 -1
- {pyrefactor-1.0.4 → pyrefactor-1.0.6/src/pyrefactor.egg-info}/PKG-INFO +1 -1
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor.egg-info/SOURCES.txt +0 -5
- pyrefactor-1.0.4/tests/test_hypothesis_analyzer.py +0 -499
- pyrefactor-1.0.4/tests/test_hypothesis_ast_visitor.py +0 -563
- pyrefactor-1.0.4/tests/test_hypothesis_config.py +0 -382
- pyrefactor-1.0.4/tests/test_hypothesis_models.py +0 -429
- pyrefactor-1.0.4/tests/test_hypothesis_reporter.py +0 -526
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/LICENSE.md +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/README.md +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/pyproject.toml +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/setup.cfg +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/__main__.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/analyzer.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/ast_visitor.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/config.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/__init__.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/boolean_logic.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/comparisons.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/complexity.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/context_manager.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/control_flow.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/dict_operations.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/duplication.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/loops.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/detectors/performance.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/models.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/py.typed +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor/reporter.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor.egg-info/entry_points.txt +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor.egg-info/requires.txt +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/src/pyrefactor.egg-info/top_level.txt +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_analyzer.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_boolean_logic_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_cli.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_comparisons_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_complexity_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_config.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_context_manager_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_control_flow_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_dict_operations_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_duplication_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_integration.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_loops_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_models.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_performance_detector.py +0 -0
- {pyrefactor-1.0.4 → pyrefactor-1.0.6}/tests/test_reporter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrefactor
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement
|
|
5
5
|
Author: tboy1337
|
|
6
6
|
Maintainer: tboy1337
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrefactor
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement
|
|
5
5
|
Author: tboy1337
|
|
6
6
|
Maintainer: tboy1337
|
|
@@ -35,11 +35,6 @@ tests/test_context_manager_detector.py
|
|
|
35
35
|
tests/test_control_flow_detector.py
|
|
36
36
|
tests/test_dict_operations_detector.py
|
|
37
37
|
tests/test_duplication_detector.py
|
|
38
|
-
tests/test_hypothesis_analyzer.py
|
|
39
|
-
tests/test_hypothesis_ast_visitor.py
|
|
40
|
-
tests/test_hypothesis_config.py
|
|
41
|
-
tests/test_hypothesis_models.py
|
|
42
|
-
tests/test_hypothesis_reporter.py
|
|
43
38
|
tests/test_integration.py
|
|
44
39
|
tests/test_loops_detector.py
|
|
45
40
|
tests/test_models.py
|
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
"""Property-based tests for Analyzer using Hypothesis."""
|
|
2
|
-
|
|
3
|
-
import tempfile
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from hypothesis import assume, given, settings
|
|
7
|
-
from hypothesis import strategies as st
|
|
8
|
-
|
|
9
|
-
from pyrefactor.analyzer import Analyzer
|
|
10
|
-
from pyrefactor.config import ComplexityConfig, Config
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Strategies for file paths and patterns
|
|
14
|
-
@st.composite
|
|
15
|
-
def file_path_strategy(draw: st.DrawFn) -> str:
|
|
16
|
-
"""Generate a file path string."""
|
|
17
|
-
parts = draw(
|
|
18
|
-
st.lists(
|
|
19
|
-
st.text(
|
|
20
|
-
min_size=1,
|
|
21
|
-
max_size=20,
|
|
22
|
-
alphabet=st.characters(
|
|
23
|
-
whitelist_categories=("Ll", "Lu", "Nd"),
|
|
24
|
-
min_codepoint=48,
|
|
25
|
-
max_codepoint=122,
|
|
26
|
-
).filter(lambda c: c not in r'\/:*?"<>|'),
|
|
27
|
-
),
|
|
28
|
-
min_size=1,
|
|
29
|
-
max_size=3,
|
|
30
|
-
)
|
|
31
|
-
)
|
|
32
|
-
return "/".join(parts) + ".py"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@st.composite
|
|
36
|
-
def exclude_pattern_strategy(draw: st.DrawFn) -> str:
|
|
37
|
-
"""Generate an exclusion pattern."""
|
|
38
|
-
return draw(
|
|
39
|
-
st.text(
|
|
40
|
-
min_size=1,
|
|
41
|
-
max_size=30,
|
|
42
|
-
alphabet=st.characters(whitelist_categories=("Ll", "Lu", "Nd", "P")),
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@st.composite
|
|
48
|
-
def valid_python_code_strategy(draw: st.DrawFn) -> str:
|
|
49
|
-
"""Generate valid Python code snippets."""
|
|
50
|
-
func_name = draw(
|
|
51
|
-
st.text(
|
|
52
|
-
min_size=1,
|
|
53
|
-
max_size=15,
|
|
54
|
-
alphabet=st.characters(
|
|
55
|
-
whitelist_categories=("Ll",), min_codepoint=97, max_codepoint=122
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
)
|
|
59
|
-
num_lines = draw(st.integers(min_value=1, max_value=20))
|
|
60
|
-
return f'def {func_name}():\n{chr(10).join(f" x{i} = {i}" for i in range(num_lines))}'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class TestAnalyzerFileFilteringProperties:
|
|
64
|
-
"""Property-based tests for file filtering logic."""
|
|
65
|
-
|
|
66
|
-
@given(
|
|
67
|
-
st.lists(
|
|
68
|
-
st.text(
|
|
69
|
-
min_size=1,
|
|
70
|
-
max_size=50,
|
|
71
|
-
alphabet=st.characters(whitelist_categories=("Ll", "Lu", "Nd", "P")),
|
|
72
|
-
),
|
|
73
|
-
max_size=5,
|
|
74
|
-
)
|
|
75
|
-
)
|
|
76
|
-
def test_filter_preserves_empty_list(self, exclude_patterns: list[str]) -> None:
|
|
77
|
-
"""Property: Filtering an empty file list returns empty list."""
|
|
78
|
-
config = Config(exclude_patterns=exclude_patterns)
|
|
79
|
-
analyzer = Analyzer(config)
|
|
80
|
-
result = analyzer._filter_excluded_files([])
|
|
81
|
-
assert not result
|
|
82
|
-
|
|
83
|
-
@given(st.lists(exclude_pattern_strategy(), max_size=5))
|
|
84
|
-
def test_filter_with_no_patterns_returns_all(self, patterns: list[str]) -> None:
|
|
85
|
-
"""Property: With no exclusion patterns, all files pass through."""
|
|
86
|
-
config = Config(exclude_patterns=[])
|
|
87
|
-
analyzer = Analyzer(config)
|
|
88
|
-
|
|
89
|
-
# Create some test paths
|
|
90
|
-
test_paths = [
|
|
91
|
-
Path("test1.py"),
|
|
92
|
-
Path("test2.py"),
|
|
93
|
-
Path("subdir/test3.py"),
|
|
94
|
-
]
|
|
95
|
-
|
|
96
|
-
result = analyzer._filter_excluded_files(test_paths)
|
|
97
|
-
assert len(result) == len(test_paths)
|
|
98
|
-
|
|
99
|
-
@given(
|
|
100
|
-
st.text(
|
|
101
|
-
min_size=1,
|
|
102
|
-
max_size=20,
|
|
103
|
-
alphabet=st.characters(whitelist_categories=("Ll",)),
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
def test_filter_excludes_matching_pattern(self, pattern: str) -> None:
|
|
107
|
-
"""Property: Files matching exclusion pattern are filtered out."""
|
|
108
|
-
config = Config(exclude_patterns=[pattern])
|
|
109
|
-
analyzer = Analyzer(config)
|
|
110
|
-
|
|
111
|
-
# Create paths: some with pattern, some without
|
|
112
|
-
matching_path = Path(f"dir/{pattern}/test.py")
|
|
113
|
-
non_matching_path = Path("other/test.py")
|
|
114
|
-
|
|
115
|
-
result = analyzer._filter_excluded_files([matching_path, non_matching_path])
|
|
116
|
-
|
|
117
|
-
# Matching path should be excluded
|
|
118
|
-
assert matching_path not in result
|
|
119
|
-
# Non-matching path should remain if pattern isn't in it
|
|
120
|
-
if pattern not in str(non_matching_path):
|
|
121
|
-
assert non_matching_path in result
|
|
122
|
-
|
|
123
|
-
@given(st.lists(exclude_pattern_strategy(), min_size=1, max_size=5))
|
|
124
|
-
def test_filter_result_subset_of_input(self, patterns: list[str]) -> None:
|
|
125
|
-
"""Property: Filtered result is always a subset of input."""
|
|
126
|
-
config = Config(exclude_patterns=patterns)
|
|
127
|
-
analyzer = Analyzer(config)
|
|
128
|
-
|
|
129
|
-
test_paths = [
|
|
130
|
-
Path("test1.py"),
|
|
131
|
-
Path("test2.py"),
|
|
132
|
-
Path("excluded/test.py"),
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
result = analyzer._filter_excluded_files(test_paths)
|
|
136
|
-
|
|
137
|
-
# Result should be a subset
|
|
138
|
-
assert len(result) <= len(test_paths)
|
|
139
|
-
assert all(path in test_paths for path in result)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class TestAnalyzerBasicProperties:
|
|
143
|
-
"""Property-based tests for basic analyzer behavior."""
|
|
144
|
-
|
|
145
|
-
@given(st.integers(min_value=1, max_value=100))
|
|
146
|
-
def test_analyzer_accepts_any_positive_config_values(
|
|
147
|
-
self, max_branches: int
|
|
148
|
-
) -> None:
|
|
149
|
-
"""Property: Analyzer can be initialized with any valid config."""
|
|
150
|
-
config = Config(complexity=ComplexityConfig(max_branches=max_branches))
|
|
151
|
-
analyzer = Analyzer(config)
|
|
152
|
-
assert analyzer.config.complexity.max_branches == max_branches
|
|
153
|
-
|
|
154
|
-
def test_analyzer_preserves_config(self) -> None:
|
|
155
|
-
"""Property: Analyzer preserves the config it's initialized with."""
|
|
156
|
-
config = Config(
|
|
157
|
-
complexity=ComplexityConfig(max_branches=15, max_nesting_depth=5)
|
|
158
|
-
)
|
|
159
|
-
analyzer = Analyzer(config)
|
|
160
|
-
|
|
161
|
-
assert analyzer.config.complexity.max_branches == 15
|
|
162
|
-
assert analyzer.config.complexity.max_nesting_depth == 5
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class TestAnalyzerFileAnalysisProperties:
|
|
166
|
-
"""Property-based tests for file analysis."""
|
|
167
|
-
|
|
168
|
-
@given(valid_python_code_strategy())
|
|
169
|
-
@settings(max_examples=1000, deadline=None)
|
|
170
|
-
def test_analyze_file_returns_file_analysis(self, code: str) -> None:
|
|
171
|
-
"""Property: Analyzing a file always returns a FileAnalysis object."""
|
|
172
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
173
|
-
f.write(code)
|
|
174
|
-
temp_path = Path(f.name)
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
config = Config()
|
|
178
|
-
analyzer = Analyzer(config)
|
|
179
|
-
result = analyzer.analyze_file(temp_path)
|
|
180
|
-
|
|
181
|
-
assert result.file_path == str(temp_path)
|
|
182
|
-
assert isinstance(result.issues, list)
|
|
183
|
-
assert result.lines_of_code >= 0
|
|
184
|
-
finally:
|
|
185
|
-
temp_path.unlink()
|
|
186
|
-
|
|
187
|
-
@given(st.integers(min_value=1, max_value=50))
|
|
188
|
-
@settings(max_examples=1000, deadline=None)
|
|
189
|
-
def test_analyze_file_counts_lines_correctly(self, num_lines: int) -> None:
|
|
190
|
-
"""Property: Analyzer correctly counts lines of code."""
|
|
191
|
-
code = "\n".join(f"x{i} = {i}" for i in range(num_lines))
|
|
192
|
-
|
|
193
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
194
|
-
f.write(code)
|
|
195
|
-
temp_path = Path(f.name)
|
|
196
|
-
|
|
197
|
-
try:
|
|
198
|
-
config = Config()
|
|
199
|
-
analyzer = Analyzer(config)
|
|
200
|
-
result = analyzer.analyze_file(temp_path)
|
|
201
|
-
|
|
202
|
-
# Should count the number of lines
|
|
203
|
-
assert result.lines_of_code == num_lines
|
|
204
|
-
finally:
|
|
205
|
-
temp_path.unlink()
|
|
206
|
-
|
|
207
|
-
def test_analyze_file_with_syntax_error_sets_parse_error(self) -> None:
|
|
208
|
-
"""Property: Files with syntax errors have parse_error set."""
|
|
209
|
-
invalid_code = "def broken syntax here"
|
|
210
|
-
|
|
211
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
212
|
-
f.write(invalid_code)
|
|
213
|
-
temp_path = Path(f.name)
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
config = Config()
|
|
217
|
-
analyzer = Analyzer(config)
|
|
218
|
-
result = analyzer.analyze_file(temp_path)
|
|
219
|
-
|
|
220
|
-
assert result.parse_error is not None
|
|
221
|
-
assert "Syntax error" in result.parse_error
|
|
222
|
-
finally:
|
|
223
|
-
temp_path.unlink()
|
|
224
|
-
|
|
225
|
-
def test_analyze_empty_file_succeeds(self) -> None:
|
|
226
|
-
"""Property: Analyzing an empty file doesn't crash."""
|
|
227
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
228
|
-
f.write("")
|
|
229
|
-
temp_path = Path(f.name)
|
|
230
|
-
|
|
231
|
-
try:
|
|
232
|
-
config = Config()
|
|
233
|
-
analyzer = Analyzer(config)
|
|
234
|
-
result = analyzer.analyze_file(temp_path)
|
|
235
|
-
|
|
236
|
-
assert result.lines_of_code == 0
|
|
237
|
-
# Empty file should parse successfully
|
|
238
|
-
assert result.parse_error is None or result.parse_error == ""
|
|
239
|
-
finally:
|
|
240
|
-
temp_path.unlink()
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
class TestAnalyzerMultipleFilesProperties:
|
|
244
|
-
"""Property-based tests for analyzing multiple files."""
|
|
245
|
-
|
|
246
|
-
@given(st.integers(min_value=1, max_value=5))
|
|
247
|
-
@settings(max_examples=1000, deadline=None)
|
|
248
|
-
def test_analyze_files_processes_all_files(self, num_files: int) -> None:
|
|
249
|
-
"""Property: analyze_files processes all provided files."""
|
|
250
|
-
temp_files = []
|
|
251
|
-
temp_dir = tempfile.mkdtemp()
|
|
252
|
-
|
|
253
|
-
try:
|
|
254
|
-
for i in range(num_files):
|
|
255
|
-
temp_path = Path(temp_dir) / f"test{i}.py"
|
|
256
|
-
temp_path.write_text(f"def func{i}(): pass")
|
|
257
|
-
temp_files.append(temp_path)
|
|
258
|
-
|
|
259
|
-
config = Config()
|
|
260
|
-
analyzer = Analyzer(config)
|
|
261
|
-
result = analyzer.analyze_files(temp_files)
|
|
262
|
-
|
|
263
|
-
# Should analyze all files
|
|
264
|
-
assert result.files_analyzed() == num_files
|
|
265
|
-
finally:
|
|
266
|
-
for temp_file in temp_files:
|
|
267
|
-
temp_file.unlink()
|
|
268
|
-
Path(temp_dir).rmdir()
|
|
269
|
-
|
|
270
|
-
def test_analyze_directory_finds_python_files(self) -> None:
|
|
271
|
-
"""Property: analyze_directory finds all .py files."""
|
|
272
|
-
temp_dir = Path(tempfile.mkdtemp())
|
|
273
|
-
|
|
274
|
-
try:
|
|
275
|
-
# Create some Python files
|
|
276
|
-
(temp_dir / "test1.py").write_text("def func1(): pass")
|
|
277
|
-
(temp_dir / "test2.py").write_text("def func2(): pass")
|
|
278
|
-
(temp_dir / "readme.txt").write_text("Not Python")
|
|
279
|
-
|
|
280
|
-
config = Config()
|
|
281
|
-
analyzer = Analyzer(config)
|
|
282
|
-
result = analyzer.analyze_directory(temp_dir)
|
|
283
|
-
|
|
284
|
-
# Should find 2 Python files, not the .txt
|
|
285
|
-
assert result.files_analyzed() == 2
|
|
286
|
-
finally:
|
|
287
|
-
for file in temp_dir.iterdir():
|
|
288
|
-
file.unlink()
|
|
289
|
-
temp_dir.rmdir()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
class TestAnalyzerResultAggregationProperties:
|
|
293
|
-
"""Property-based tests for result aggregation."""
|
|
294
|
-
|
|
295
|
-
@given(st.integers(min_value=0, max_value=5))
|
|
296
|
-
@settings(max_examples=1000, deadline=None)
|
|
297
|
-
def test_result_aggregates_issues_from_all_files(self, num_files: int) -> None:
|
|
298
|
-
"""Property: Result contains all issues from all analyzed files."""
|
|
299
|
-
assume(num_files > 0)
|
|
300
|
-
|
|
301
|
-
temp_dir = Path(tempfile.mkdtemp())
|
|
302
|
-
|
|
303
|
-
try:
|
|
304
|
-
for i in range(num_files):
|
|
305
|
-
# Create code with varying complexity
|
|
306
|
-
code = f"""
|
|
307
|
-
def complex_func_{i}():
|
|
308
|
-
if x:
|
|
309
|
-
if y:
|
|
310
|
-
if z:
|
|
311
|
-
if w:
|
|
312
|
-
pass
|
|
313
|
-
"""
|
|
314
|
-
(temp_dir / f"test{i}.py").write_text(code)
|
|
315
|
-
|
|
316
|
-
config = Config(complexity=ComplexityConfig(max_nesting_depth=2))
|
|
317
|
-
analyzer = Analyzer(config)
|
|
318
|
-
result = analyzer.analyze_directory(temp_dir)
|
|
319
|
-
|
|
320
|
-
# Should have analyzed all files
|
|
321
|
-
assert result.files_analyzed() == num_files
|
|
322
|
-
|
|
323
|
-
# Total issues should be sum of issues from all files
|
|
324
|
-
total_from_files = sum(len(fa.issues) for fa in result.file_analyses)
|
|
325
|
-
assert result.total_issues() == total_from_files
|
|
326
|
-
finally:
|
|
327
|
-
for file in temp_dir.iterdir():
|
|
328
|
-
file.unlink()
|
|
329
|
-
temp_dir.rmdir()
|
|
330
|
-
|
|
331
|
-
def test_empty_directory_returns_empty_result(self) -> None:
|
|
332
|
-
"""Property: Analyzing empty directory returns result with 0 files."""
|
|
333
|
-
temp_dir = Path(tempfile.mkdtemp())
|
|
334
|
-
|
|
335
|
-
try:
|
|
336
|
-
config = Config()
|
|
337
|
-
analyzer = Analyzer(config)
|
|
338
|
-
result = analyzer.analyze_directory(temp_dir)
|
|
339
|
-
|
|
340
|
-
assert result.files_analyzed() == 0
|
|
341
|
-
assert result.total_issues() == 0
|
|
342
|
-
finally:
|
|
343
|
-
temp_dir.rmdir()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class TestAnalyzerDetectorIntegrationProperties:
|
|
347
|
-
"""Property-based tests for detector integration."""
|
|
348
|
-
|
|
349
|
-
@given(st.booleans())
|
|
350
|
-
@settings(max_examples=1000, deadline=None)
|
|
351
|
-
def test_disabled_detectors_not_run(self, performance_enabled: bool) -> None:
|
|
352
|
-
"""Property: Disabled detectors don't produce issues."""
|
|
353
|
-
code = """
|
|
354
|
-
def test_func():
|
|
355
|
-
result = []
|
|
356
|
-
for i in range(10):
|
|
357
|
-
result.append(i)
|
|
358
|
-
return result
|
|
359
|
-
"""
|
|
360
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
361
|
-
f.write(code)
|
|
362
|
-
temp_path = Path(f.name)
|
|
363
|
-
|
|
364
|
-
try:
|
|
365
|
-
config = Config()
|
|
366
|
-
config.performance.enabled = performance_enabled
|
|
367
|
-
|
|
368
|
-
analyzer = Analyzer(config)
|
|
369
|
-
result = analyzer.analyze_file(temp_path)
|
|
370
|
-
|
|
371
|
-
# Result should exist regardless
|
|
372
|
-
assert result.file_path == str(temp_path)
|
|
373
|
-
|
|
374
|
-
# If performance disabled, no performance-specific rules should fire
|
|
375
|
-
# Note: This assumes performance rules start with P
|
|
376
|
-
# Actual behavior depends on detector implementation
|
|
377
|
-
finally:
|
|
378
|
-
temp_path.unlink()
|
|
379
|
-
|
|
380
|
-
@given(st.integers(min_value=1, max_value=20))
|
|
381
|
-
@settings(max_examples=1000, deadline=None)
|
|
382
|
-
def test_complexity_threshold_affects_issues(self, max_branches: int) -> None:
|
|
383
|
-
"""Property: Higher complexity threshold produces fewer issues."""
|
|
384
|
-
# Code with 15 branches
|
|
385
|
-
code = """
|
|
386
|
-
def complex_func(x):
|
|
387
|
-
if x == 1: return 1
|
|
388
|
-
if x == 2: return 2
|
|
389
|
-
if x == 3: return 3
|
|
390
|
-
if x == 4: return 4
|
|
391
|
-
if x == 5: return 5
|
|
392
|
-
if x == 6: return 6
|
|
393
|
-
if x == 7: return 7
|
|
394
|
-
if x == 8: return 8
|
|
395
|
-
if x == 9: return 9
|
|
396
|
-
if x == 10: return 10
|
|
397
|
-
if x == 11: return 11
|
|
398
|
-
if x == 12: return 12
|
|
399
|
-
if x == 13: return 13
|
|
400
|
-
if x == 14: return 14
|
|
401
|
-
if x == 15: return 15
|
|
402
|
-
return 0
|
|
403
|
-
"""
|
|
404
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
405
|
-
f.write(code)
|
|
406
|
-
temp_path = Path(f.name)
|
|
407
|
-
|
|
408
|
-
try:
|
|
409
|
-
config = Config(complexity=ComplexityConfig(max_branches=max_branches))
|
|
410
|
-
analyzer = Analyzer(config)
|
|
411
|
-
result = analyzer.analyze_file(temp_path)
|
|
412
|
-
|
|
413
|
-
# If threshold is >= 15, no branch issues
|
|
414
|
-
# If threshold is < 15, should have branch issue
|
|
415
|
-
branch_issues = [
|
|
416
|
-
issue for issue in result.issues if "branches" in issue.message.lower()
|
|
417
|
-
]
|
|
418
|
-
|
|
419
|
-
if max_branches >= 15:
|
|
420
|
-
# Should have no branch issues
|
|
421
|
-
assert len(branch_issues) == 0
|
|
422
|
-
else:
|
|
423
|
-
# Should have at least one branch issue
|
|
424
|
-
assert len(branch_issues) >= 1
|
|
425
|
-
finally:
|
|
426
|
-
temp_path.unlink()
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
class TestAnalyzerInvariants:
|
|
430
|
-
"""Test invariants across analyzer operations."""
|
|
431
|
-
|
|
432
|
-
def test_analyzing_same_file_twice_produces_same_results(self) -> None:
|
|
433
|
-
"""Property: Analyzing same file twice produces identical results."""
|
|
434
|
-
code = """
|
|
435
|
-
def test_func():
|
|
436
|
-
if a:
|
|
437
|
-
if b:
|
|
438
|
-
if c:
|
|
439
|
-
pass
|
|
440
|
-
"""
|
|
441
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
442
|
-
f.write(code)
|
|
443
|
-
temp_path = Path(f.name)
|
|
444
|
-
|
|
445
|
-
try:
|
|
446
|
-
config = Config()
|
|
447
|
-
analyzer = Analyzer(config)
|
|
448
|
-
|
|
449
|
-
result1 = analyzer.analyze_file(temp_path)
|
|
450
|
-
result2 = analyzer.analyze_file(temp_path)
|
|
451
|
-
|
|
452
|
-
# Results should be identical
|
|
453
|
-
assert result1.file_path == result2.file_path
|
|
454
|
-
assert result1.lines_of_code == result2.lines_of_code
|
|
455
|
-
assert len(result1.issues) == len(result2.issues)
|
|
456
|
-
finally:
|
|
457
|
-
temp_path.unlink()
|
|
458
|
-
|
|
459
|
-
@given(st.lists(exclude_pattern_strategy(), max_size=5))
|
|
460
|
-
@settings(max_examples=1000, deadline=None)
|
|
461
|
-
def test_analyzer_config_immutable_during_analysis(
|
|
462
|
-
self, patterns: list[str]
|
|
463
|
-
) -> None:
|
|
464
|
-
"""Property: Config doesn't change during analysis."""
|
|
465
|
-
config = Config(exclude_patterns=patterns)
|
|
466
|
-
original_patterns = config.exclude_patterns.copy()
|
|
467
|
-
|
|
468
|
-
analyzer = Analyzer(config)
|
|
469
|
-
|
|
470
|
-
# Create and analyze a file
|
|
471
|
-
code = "def test(): pass"
|
|
472
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
473
|
-
f.write(code)
|
|
474
|
-
temp_path = Path(f.name)
|
|
475
|
-
|
|
476
|
-
try:
|
|
477
|
-
analyzer.analyze_file(temp_path)
|
|
478
|
-
|
|
479
|
-
# Config should be unchanged
|
|
480
|
-
assert analyzer.config.exclude_patterns == original_patterns
|
|
481
|
-
finally:
|
|
482
|
-
temp_path.unlink()
|
|
483
|
-
|
|
484
|
-
def test_file_analysis_always_has_file_path(self) -> None:
|
|
485
|
-
"""Property: Every FileAnalysis has a non-empty file path."""
|
|
486
|
-
code = "def test(): pass"
|
|
487
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
488
|
-
f.write(code)
|
|
489
|
-
temp_path = Path(f.name)
|
|
490
|
-
|
|
491
|
-
try:
|
|
492
|
-
config = Config()
|
|
493
|
-
analyzer = Analyzer(config)
|
|
494
|
-
result = analyzer.analyze_file(temp_path)
|
|
495
|
-
|
|
496
|
-
assert result.file_path != ""
|
|
497
|
-
assert len(result.file_path) > 0
|
|
498
|
-
finally:
|
|
499
|
-
temp_path.unlink()
|