pyrefactor 1.0.1__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.
pyrefactor/config.py ADDED
@@ -0,0 +1,224 @@
1
+ """Configuration management for PyRefactor."""
2
+
3
+ import configparser
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Optional, Union
7
+
8
+
9
+ @dataclass
10
+ class ComplexityConfig:
11
+ """Configuration for complexity detector."""
12
+
13
+ max_branches: int = 10
14
+ max_nesting_depth: int = 3
15
+ max_function_lines: int = 50
16
+ max_arguments: int = 5
17
+ max_local_variables: int = 15
18
+ max_cyclomatic_complexity: int = 10
19
+
20
+
21
+ @dataclass
22
+ class PerformanceConfig:
23
+ """Configuration for performance detector."""
24
+
25
+ enabled: bool = True
26
+
27
+
28
+ @dataclass
29
+ class DuplicationConfig:
30
+ """Configuration for duplication detector."""
31
+
32
+ enabled: bool = True
33
+ min_duplicate_lines: int = 5
34
+ similarity_threshold: float = 0.85
35
+
36
+
37
+ @dataclass
38
+ class BooleanLogicConfig:
39
+ """Configuration for boolean logic detector."""
40
+
41
+ enabled: bool = True
42
+ max_boolean_operators: int = 3
43
+
44
+
45
+ @dataclass
46
+ class LoopsConfig:
47
+ """Configuration for loops detector."""
48
+
49
+ enabled: bool = True
50
+
51
+
52
+ @dataclass
53
+ class ContextManagerConfig:
54
+ """Configuration for context manager detector."""
55
+
56
+ enabled: bool = True
57
+
58
+
59
+ @dataclass
60
+ class ControlFlowConfig:
61
+ """Configuration for control flow detector."""
62
+
63
+ enabled: bool = True
64
+
65
+
66
+ @dataclass
67
+ class DictOperationsConfig:
68
+ """Configuration for dictionary operations detector."""
69
+
70
+ enabled: bool = True
71
+
72
+
73
+ @dataclass
74
+ class ComparisonsConfig:
75
+ """Configuration for comparisons detector."""
76
+
77
+ enabled: bool = True
78
+
79
+
80
+ @dataclass
81
+ class Config:
82
+ """Main configuration for PyRefactor."""
83
+
84
+ complexity: ComplexityConfig = field(default_factory=ComplexityConfig)
85
+ performance: PerformanceConfig = field(default_factory=PerformanceConfig)
86
+ duplication: DuplicationConfig = field(default_factory=DuplicationConfig)
87
+ boolean_logic: BooleanLogicConfig = field(default_factory=BooleanLogicConfig)
88
+ loops: LoopsConfig = field(default_factory=LoopsConfig)
89
+ context_manager: ContextManagerConfig = field(default_factory=ContextManagerConfig)
90
+ control_flow: ControlFlowConfig = field(default_factory=ControlFlowConfig)
91
+ dict_operations: DictOperationsConfig = field(default_factory=DictOperationsConfig)
92
+ comparisons: ComparisonsConfig = field(default_factory=ComparisonsConfig)
93
+ exclude_patterns: list[str] = field(default_factory=list)
94
+
95
+ @staticmethod
96
+ def _parse_complexity_config(config: configparser.ConfigParser) -> dict[str, int]:
97
+ """Extract complexity configuration from config parser."""
98
+ complexity_dict: dict[str, int] = {}
99
+ if config.has_section("complexity"):
100
+ for key in [
101
+ "max_branches",
102
+ "max_nesting_depth",
103
+ "max_function_lines",
104
+ "max_arguments",
105
+ "max_local_variables",
106
+ "max_cyclomatic_complexity",
107
+ ]:
108
+ if config.has_option("complexity", key):
109
+ complexity_dict[key] = config.getint("complexity", key)
110
+ return complexity_dict
111
+
112
+ @staticmethod
113
+ def _parse_duplication_config(
114
+ config: configparser.ConfigParser,
115
+ ) -> dict[str, Union[int, float, bool]]:
116
+ """Extract duplication configuration from config parser."""
117
+ duplication_dict: dict[str, Union[int, float, bool]] = {}
118
+ if config.has_section("duplication"):
119
+ if config.has_option("duplication", "enabled"):
120
+ duplication_dict["enabled"] = config.getboolean(
121
+ "duplication", "enabled"
122
+ )
123
+ if config.has_option("duplication", "min_duplicate_lines"):
124
+ duplication_dict["min_duplicate_lines"] = config.getint(
125
+ "duplication", "min_duplicate_lines"
126
+ )
127
+ if config.has_option("duplication", "similarity_threshold"):
128
+ duplication_dict["similarity_threshold"] = config.getfloat(
129
+ "duplication", "similarity_threshold"
130
+ )
131
+ return duplication_dict
132
+
133
+ @staticmethod
134
+ def _parse_boolean_logic_config(
135
+ config: configparser.ConfigParser,
136
+ ) -> dict[str, Union[int, bool]]:
137
+ """Extract boolean logic configuration from config parser."""
138
+ boolean_dict: dict[str, Union[int, bool]] = {}
139
+ if config.has_section("boolean_logic"):
140
+ if config.has_option("boolean_logic", "enabled"):
141
+ boolean_dict["enabled"] = config.getboolean("boolean_logic", "enabled")
142
+ if config.has_option("boolean_logic", "max_boolean_operators"):
143
+ boolean_dict["max_boolean_operators"] = config.getint(
144
+ "boolean_logic", "max_boolean_operators"
145
+ )
146
+ return boolean_dict
147
+
148
+ @staticmethod
149
+ def _parse_enabled_flag(
150
+ config: configparser.ConfigParser, section: str
151
+ ) -> dict[str, bool]:
152
+ """Extract simple enabled flag from a config section."""
153
+ result: dict[str, bool] = {}
154
+ if config.has_section(section) and config.has_option(section, "enabled"):
155
+ result["enabled"] = config.getboolean(section, "enabled")
156
+ return result
157
+
158
+ @staticmethod
159
+ def _parse_exclude_patterns(config: configparser.ConfigParser) -> list[str]:
160
+ """Extract exclude patterns from config parser."""
161
+ exclude_list: list[str] = []
162
+ if config.has_section("general") and config.has_option(
163
+ "general", "exclude_patterns"
164
+ ):
165
+ patterns_str = config.get("general", "exclude_patterns")
166
+ # Parse comma-separated patterns
167
+ exclude_list = [
168
+ pattern.strip()
169
+ for pattern in patterns_str.split(",")
170
+ if pattern.strip()
171
+ ]
172
+ return exclude_list
173
+
174
+ @classmethod
175
+ def from_file(cls, config_path: Path) -> "Config":
176
+ """Load configuration from an INI file.
177
+
178
+ Technical note: This method works with dynamic INI data which mypy
179
+ cannot fully type-check. Type ignores are used to suppress Any-related
180
+ warnings while maintaining runtime safety through try-except handling.
181
+ """
182
+ try:
183
+ parser = configparser.ConfigParser()
184
+ parser.read(config_path, encoding="utf-8")
185
+
186
+ return cls(
187
+ complexity=ComplexityConfig(**cls._parse_complexity_config(parser)),
188
+ performance=PerformanceConfig(
189
+ **cls._parse_enabled_flag(parser, "performance")
190
+ ),
191
+ duplication=DuplicationConfig(**cls._parse_duplication_config(parser)), # type: ignore[arg-type]
192
+ boolean_logic=BooleanLogicConfig(**cls._parse_boolean_logic_config(parser)), # type: ignore[arg-type]
193
+ loops=LoopsConfig(**cls._parse_enabled_flag(parser, "loops")),
194
+ context_manager=ContextManagerConfig(
195
+ **cls._parse_enabled_flag(parser, "context_manager")
196
+ ),
197
+ control_flow=ControlFlowConfig(
198
+ **cls._parse_enabled_flag(parser, "control_flow")
199
+ ),
200
+ dict_operations=DictOperationsConfig(
201
+ **cls._parse_enabled_flag(parser, "dict_operations")
202
+ ),
203
+ comparisons=ComparisonsConfig(
204
+ **cls._parse_enabled_flag(parser, "comparisons")
205
+ ),
206
+ exclude_patterns=cls._parse_exclude_patterns(parser),
207
+ )
208
+ except FileNotFoundError:
209
+ return cls()
210
+ except Exception as e:
211
+ raise ValueError(f"Error loading configuration: {e}") from e
212
+
213
+ @classmethod
214
+ def load(cls, config_path: Optional[Path] = None) -> "Config":
215
+ """Load configuration from file or use defaults."""
216
+ if config_path is not None:
217
+ return cls.from_file(config_path)
218
+
219
+ # Try to find pyrefactor.ini in current directory
220
+ ini_file = Path("pyrefactor.ini")
221
+ if ini_file.exists():
222
+ return cls.from_file(ini_file)
223
+
224
+ return cls()
@@ -0,0 +1,23 @@
1
+ """Detector modules for PyRefactor."""
2
+
3
+ from .boolean_logic import BooleanLogicDetector
4
+ from .comparisons import ComparisonsDetector
5
+ from .complexity import ComplexityDetector
6
+ from .context_manager import ContextManagerDetector
7
+ from .control_flow import ControlFlowDetector
8
+ from .dict_operations import DictOperationsDetector
9
+ from .duplication import DuplicationDetector
10
+ from .loops import LoopsDetector
11
+ from .performance import PerformanceDetector
12
+
13
+ __all__ = [
14
+ "BooleanLogicDetector",
15
+ "ComparisonsDetector",
16
+ "ComplexityDetector",
17
+ "ContextManagerDetector",
18
+ "ControlFlowDetector",
19
+ "DictOperationsDetector",
20
+ "DuplicationDetector",
21
+ "LoopsDetector",
22
+ "PerformanceDetector",
23
+ ]
@@ -0,0 +1,231 @@
1
+ """Boolean logic detector for PyRefactor."""
2
+
3
+ import ast
4
+ from typing import Union, cast
5
+
6
+ from ..ast_visitor import BaseDetector
7
+ from ..models import Issue, Severity
8
+
9
+
10
+ class BooleanLogicDetector(BaseDetector):
11
+ """Detects complex boolean logic that can be simplified."""
12
+
13
+ # Minimum nesting level to trigger early return suggestion
14
+ MIN_NESTING_FOR_EARLY_RETURN = 3
15
+
16
+ def get_detector_name(self) -> str:
17
+ """Return the name of this detector."""
18
+ return "boolean_logic"
19
+
20
+ def _create_issue(
21
+ self,
22
+ node: ast.AST,
23
+ *,
24
+ severity: Severity,
25
+ rule_id: str,
26
+ message: str,
27
+ suggestion: str,
28
+ ) -> Issue:
29
+ """Create an Issue object from common parameters."""
30
+ return Issue(
31
+ file=self.file_path,
32
+ line=cast(int, getattr(node, "lineno", 0)),
33
+ column=cast(int, getattr(node, "col_offset", 0)),
34
+ severity=severity,
35
+ rule_id=rule_id,
36
+ message=message,
37
+ suggestion=suggestion,
38
+ )
39
+
40
+ def visit_BoolOp(self, node: ast.BoolOp) -> None:
41
+ """Check for complex boolean operations."""
42
+ if self.is_suppressed(node):
43
+ self.generic_visit(node)
44
+ return
45
+
46
+ # Count total operators in the expression
47
+ operator_count = self._count_operators(node)
48
+ max_operators = self.config.boolean_logic.max_boolean_operators
49
+
50
+ if operator_count > max_operators:
51
+ self.add_issue(
52
+ self._create_issue(
53
+ node,
54
+ severity=Severity.MEDIUM,
55
+ rule_id="B001",
56
+ message=f"Complex boolean expression with {operator_count} operators (max {max_operators})",
57
+ suggestion="Extract boolean sub-expressions to named variables for clarity",
58
+ )
59
+ )
60
+
61
+ self.generic_visit(node)
62
+
63
+ def visit_Compare(self, node: ast.Compare) -> None:
64
+ """Check for redundant boolean comparisons."""
65
+ if self.is_suppressed(node):
66
+ self.generic_visit(node)
67
+ return
68
+
69
+ # Check for comparison with True/False
70
+ for i, comparator in enumerate(node.comparators):
71
+ self._check_boolean_comparison(node, i, comparator)
72
+
73
+ self.generic_visit(node)
74
+
75
+ def _check_boolean_comparison(
76
+ self, node: ast.Compare, index: int, comparator: ast.expr
77
+ ) -> None:
78
+ """Check if a comparison involves a boolean constant.
79
+
80
+ Args:
81
+ node: The Compare node
82
+ index: Index of the comparator
83
+ comparator: The comparator expression
84
+ """
85
+ if not isinstance(comparator, ast.Constant):
86
+ return
87
+
88
+ if not isinstance(comparator.value, bool):
89
+ return
90
+
91
+ op = node.ops[index]
92
+ if isinstance(op, ast.Eq):
93
+ self._report_boolean_equality(node, comparator.value)
94
+ elif isinstance(op, ast.Is):
95
+ self._report_boolean_is(node)
96
+
97
+ def _report_boolean_equality(self, node: ast.Compare, value: bool) -> None:
98
+ """Report issues with boolean equality comparisons."""
99
+ rule_id, message, suggestion = (
100
+ (
101
+ "B002",
102
+ "Redundant comparison with True",
103
+ "Remove '== True' and use the boolean expression directly",
104
+ )
105
+ if value
106
+ else (
107
+ "B003",
108
+ "Redundant comparison with False",
109
+ "Use 'not expr' instead of 'expr == False'",
110
+ )
111
+ )
112
+ self.add_issue(
113
+ self._create_issue(
114
+ node,
115
+ severity=Severity.INFO,
116
+ rule_id=rule_id,
117
+ message=message,
118
+ suggestion=suggestion,
119
+ )
120
+ )
121
+
122
+ def _report_boolean_is(self, node: ast.Compare) -> None:
123
+ """Report issues with boolean 'is' comparisons."""
124
+ self.add_issue(
125
+ self._create_issue(
126
+ node,
127
+ severity=Severity.MEDIUM,
128
+ rule_id="B004",
129
+ message="Using 'is' for boolean comparison",
130
+ suggestion="Use '==' for value comparison or use the boolean directly",
131
+ )
132
+ )
133
+
134
+ def visit_FunctionDef(
135
+ self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
136
+ ) -> None:
137
+ """Track function context for early return detection."""
138
+ old_function = self.current_function
139
+ self.current_function = node
140
+ self.generic_visit(node)
141
+ self.current_function = old_function
142
+
143
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
144
+ """Track async function context for early return detection."""
145
+ self.visit_FunctionDef(node)
146
+
147
+ def visit_If(self, node: ast.If) -> None:
148
+ """Check for opportunities to use early returns."""
149
+ if self.is_suppressed(node):
150
+ self.generic_visit(node)
151
+ return
152
+
153
+ # Check for nested if statements that could use early returns
154
+ if self.current_function:
155
+ self._check_early_return_opportunity(node)
156
+
157
+ self.generic_visit(node)
158
+
159
+ def _check_early_return_opportunity(self, node: ast.If) -> None:
160
+ """Check if nested ifs could benefit from early returns."""
161
+ # Look for pattern: if x: if y: if z: return
162
+ nesting_count = 0
163
+ current: ast.AST = node
164
+
165
+ while isinstance(current, ast.If):
166
+ nesting_count += 1
167
+ # Check if body contains only another If or a return
168
+ if len(current.body) != 1:
169
+ break
170
+
171
+ first_stmt = current.body[0]
172
+ if isinstance(first_stmt, ast.If):
173
+ current = first_stmt
174
+ continue
175
+
176
+ if isinstance(first_stmt, (ast.Return, ast.Raise)):
177
+ if nesting_count >= self.MIN_NESTING_FOR_EARLY_RETURN:
178
+ self.add_issue(
179
+ self._create_issue(
180
+ node,
181
+ severity=Severity.MEDIUM,
182
+ rule_id="B005",
183
+ message=f"Deeply nested if statements ({nesting_count} levels) with early exit",
184
+ suggestion="Use guard clauses with early returns to reduce nesting",
185
+ )
186
+ )
187
+ break
188
+
189
+ def visit_UnaryOp(self, node: ast.UnaryOp) -> None:
190
+ """Check for De Morgan's law opportunities."""
191
+ if self.is_suppressed(node):
192
+ self.generic_visit(node)
193
+ return
194
+
195
+ # Check for not (a and b) or not (a or b)
196
+ if isinstance(node.op, ast.Not) and isinstance(node.operand, ast.BoolOp):
197
+ if isinstance(node.operand.op, (ast.And, ast.Or)):
198
+ rule_id, suggestion = (
199
+ ("B006", "Replace 'not (a and b)' with 'not a or not b'")
200
+ if isinstance(node.operand.op, ast.And)
201
+ else ("B007", "Replace 'not (a or b)' with 'not a and not b'")
202
+ )
203
+ self.add_issue(
204
+ self._create_issue(
205
+ node,
206
+ severity=Severity.INFO,
207
+ rule_id=rule_id,
208
+ message="Complex negation can be simplified using De Morgan's law",
209
+ suggestion=suggestion,
210
+ )
211
+ )
212
+
213
+ self.generic_visit(node)
214
+
215
+ def _count_operators(self, node: ast.BoolOp) -> int:
216
+ """Count the number of boolean operators in an expression.
217
+
218
+ Uses iterative approach for better performance on deeply nested expressions.
219
+ """
220
+ total_count = 0
221
+ stack = [node]
222
+
223
+ while stack:
224
+ current = stack.pop()
225
+ if isinstance(current, ast.BoolOp):
226
+ # n values require n-1 operators
227
+ total_count += len(current.values) - 1
228
+ # Add nested BoolOps to stack
229
+ stack.extend(v for v in current.values if isinstance(v, ast.BoolOp))
230
+
231
+ return total_count