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/__init__.py +3 -0
- pyrefactor/__main__.py +231 -0
- pyrefactor/analyzer.py +185 -0
- pyrefactor/ast_visitor.py +197 -0
- pyrefactor/config.py +224 -0
- pyrefactor/detectors/__init__.py +23 -0
- pyrefactor/detectors/boolean_logic.py +231 -0
- pyrefactor/detectors/comparisons.py +353 -0
- pyrefactor/detectors/complexity.py +248 -0
- pyrefactor/detectors/context_manager.py +188 -0
- pyrefactor/detectors/control_flow.py +156 -0
- pyrefactor/detectors/dict_operations.py +346 -0
- pyrefactor/detectors/duplication.py +358 -0
- pyrefactor/detectors/loops.py +267 -0
- pyrefactor/detectors/performance.py +267 -0
- pyrefactor/models.py +98 -0
- pyrefactor/py.typed +0 -0
- pyrefactor/reporter.py +208 -0
- pyrefactor-1.0.1.dist-info/METADATA +353 -0
- pyrefactor-1.0.1.dist-info/RECORD +24 -0
- pyrefactor-1.0.1.dist-info/WHEEL +5 -0
- pyrefactor-1.0.1.dist-info/entry_points.txt +2 -0
- pyrefactor-1.0.1.dist-info/licenses/LICENSE.md +70 -0
- pyrefactor-1.0.1.dist-info/top_level.txt +1 -0
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
|