thailint 0.11.0__py3-none-any.whl → 0.13.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.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +118 -7
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli/utils.py +29 -9
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/__init__.py +22 -9
- src/linters/stringly_typed/config.py +32 -8
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +102 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +9 -5
- src/linters/stringly_typed/python/analyzer.py +159 -9
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +630 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +405 -0
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.11.0.dist-info/METADATA +0 -1661
- thailint-0.11.0.dist-info/RECORD +0 -150
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect function calls with string literal arguments in Python AST
|
|
3
|
+
|
|
4
|
+
Scope: Find function and method calls that consistently receive string arguments
|
|
5
|
+
|
|
6
|
+
Overview: Provides FunctionCallTracker class that traverses Python AST to find function
|
|
7
|
+
and method calls where string literals are passed as arguments. Tracks the function
|
|
8
|
+
name, parameter index, and string value to enable cross-file aggregation. When a
|
|
9
|
+
function is called with the same set of limited string values across files, it
|
|
10
|
+
suggests the parameter should be an enum. Handles both simple function calls
|
|
11
|
+
(foo("value")) and method calls (obj.method("value")).
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for AST parsing, dataclasses for pattern structure,
|
|
14
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
15
|
+
|
|
16
|
+
Exports: FunctionCallTracker class, FunctionCallPattern dataclass
|
|
17
|
+
|
|
18
|
+
Interfaces: FunctionCallTracker.find_patterns(tree) -> list[FunctionCallPattern]
|
|
19
|
+
|
|
20
|
+
Implementation: AST NodeVisitor pattern with Call node handling for string arguments
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- invalid-name: visit_Call follows AST NodeVisitor method naming convention
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FunctionCallPattern:
|
|
34
|
+
"""Represents a function call with a string literal argument.
|
|
35
|
+
|
|
36
|
+
Captures information about a function or method call where a string literal
|
|
37
|
+
is passed as an argument, enabling cross-file analysis to detect limited
|
|
38
|
+
value sets that should be enums.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
function_name: str
|
|
42
|
+
"""Fully qualified function name (e.g., 'process' or 'obj.method')."""
|
|
43
|
+
|
|
44
|
+
param_index: int
|
|
45
|
+
"""Index of the parameter receiving the string value (0-indexed)."""
|
|
46
|
+
|
|
47
|
+
string_value: str
|
|
48
|
+
"""The string literal value passed to the function."""
|
|
49
|
+
|
|
50
|
+
line_number: int
|
|
51
|
+
"""Line number where the call occurs (1-indexed)."""
|
|
52
|
+
|
|
53
|
+
column: int
|
|
54
|
+
"""Column number where the call starts (0-indexed)."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FunctionCallTracker(ast.NodeVisitor):
|
|
58
|
+
"""Tracks function calls with string literal arguments.
|
|
59
|
+
|
|
60
|
+
Finds patterns like 'process("active")' and 'obj.set_status("pending")' where
|
|
61
|
+
string literals are used for arguments that could be enums.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
"""Initialize the tracker."""
|
|
66
|
+
self.patterns: list[FunctionCallPattern] = []
|
|
67
|
+
|
|
68
|
+
def find_patterns(self, tree: ast.AST) -> list[FunctionCallPattern]:
|
|
69
|
+
"""Find all function calls with string arguments in the AST.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
tree: The AST to analyze
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of FunctionCallPattern instances for each detected call
|
|
76
|
+
"""
|
|
77
|
+
self.patterns = []
|
|
78
|
+
self.visit(tree)
|
|
79
|
+
return self.patterns
|
|
80
|
+
|
|
81
|
+
def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name
|
|
82
|
+
"""Visit a Call node to check for string arguments.
|
|
83
|
+
|
|
84
|
+
Handles both simple function calls and method calls, extracting
|
|
85
|
+
the function name and any string literal arguments.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
node: The Call node to analyze
|
|
89
|
+
"""
|
|
90
|
+
function_name = self._extract_function_name(node.func)
|
|
91
|
+
if function_name is None:
|
|
92
|
+
self.generic_visit(node)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
self._check_positional_args(node, function_name)
|
|
96
|
+
self.generic_visit(node)
|
|
97
|
+
|
|
98
|
+
def _extract_function_name(self, func_node: ast.expr) -> str | None:
|
|
99
|
+
"""Extract the function name from a call expression.
|
|
100
|
+
|
|
101
|
+
Handles simple names (foo) and attribute access (obj.method).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
func_node: The function expression node
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Function name string or None if not extractable
|
|
108
|
+
"""
|
|
109
|
+
if isinstance(func_node, ast.Name):
|
|
110
|
+
return func_node.id
|
|
111
|
+
if isinstance(func_node, ast.Attribute):
|
|
112
|
+
return self._extract_attribute_name(func_node)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def _extract_attribute_name(self, node: ast.Attribute) -> str | None:
|
|
116
|
+
"""Extract function name from an attribute access.
|
|
117
|
+
|
|
118
|
+
Builds qualified names like 'obj.method' or 'a.b.method'.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
node: The Attribute node
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Qualified function name or None if too complex
|
|
125
|
+
"""
|
|
126
|
+
parts: list[str] = [node.attr]
|
|
127
|
+
current = node.value
|
|
128
|
+
depth = 0
|
|
129
|
+
|
|
130
|
+
while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
|
|
131
|
+
if isinstance(current, ast.Name):
|
|
132
|
+
parts.append(current.id)
|
|
133
|
+
break
|
|
134
|
+
if isinstance(current, ast.Attribute):
|
|
135
|
+
parts.append(current.attr)
|
|
136
|
+
current = current.value
|
|
137
|
+
depth += 1
|
|
138
|
+
else:
|
|
139
|
+
# Complex expression (call result, subscript, etc.)
|
|
140
|
+
# Use placeholder to maintain function identity
|
|
141
|
+
parts.append("_")
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
return ".".join(reversed(parts))
|
|
145
|
+
|
|
146
|
+
def _check_positional_args(self, node: ast.Call, function_name: str) -> None:
|
|
147
|
+
"""Check positional arguments for string literals.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
node: The Call node
|
|
151
|
+
function_name: Extracted function name
|
|
152
|
+
"""
|
|
153
|
+
for param_index, arg in enumerate(node.args):
|
|
154
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
155
|
+
self._add_pattern(node, function_name, param_index, arg.value)
|
|
156
|
+
|
|
157
|
+
def _add_pattern(
|
|
158
|
+
self, node: ast.Call, function_name: str, param_index: int, string_value: str
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Create and add a function call pattern to results.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
node: The Call node containing the pattern
|
|
164
|
+
function_name: Name of the function being called
|
|
165
|
+
param_index: Index of the string argument
|
|
166
|
+
string_value: The string literal value
|
|
167
|
+
"""
|
|
168
|
+
pattern = FunctionCallPattern(
|
|
169
|
+
function_name=function_name,
|
|
170
|
+
param_index=param_index,
|
|
171
|
+
string_value=string_value,
|
|
172
|
+
line_number=node.lineno,
|
|
173
|
+
column=node.col_offset,
|
|
174
|
+
)
|
|
175
|
+
self.patterns.append(pattern)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect scattered string comparisons in Python AST
|
|
3
|
+
|
|
4
|
+
Scope: Find equality/inequality comparisons with string literals across Python files
|
|
5
|
+
|
|
6
|
+
Overview: Provides ComparisonTracker class that traverses Python AST to find scattered
|
|
7
|
+
string comparisons like `if env == "production"`. Tracks the variable name, compared
|
|
8
|
+
string value, and operator to enable cross-file aggregation. When a variable is compared
|
|
9
|
+
to multiple unique string values across files, it suggests the variable should be an enum.
|
|
10
|
+
Excludes common false positives like `__name__ == "__main__"` and type name checks.
|
|
11
|
+
|
|
12
|
+
Dependencies: ast module for AST parsing, dataclasses for pattern structure,
|
|
13
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
14
|
+
|
|
15
|
+
Exports: ComparisonTracker class, ComparisonPattern dataclass
|
|
16
|
+
|
|
17
|
+
Interfaces: ComparisonTracker.find_patterns(tree) -> list[ComparisonPattern]
|
|
18
|
+
|
|
19
|
+
Implementation: AST NodeVisitor pattern with Compare node handling for string comparisons
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- invalid-name: visit_Compare follows AST NodeVisitor method naming convention
|
|
23
|
+
- srp: Tracker implements AST visitor pattern with multiple visit methods.
|
|
24
|
+
Methods support single responsibility of comparison pattern detection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import ast
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ComparisonPattern:
|
|
35
|
+
"""Represents a string comparison found in Python code.
|
|
36
|
+
|
|
37
|
+
Captures information about a comparison like `if (env == "production")` to
|
|
38
|
+
enable cross-file analysis for detecting scattered string comparisons that
|
|
39
|
+
suggest missing enums.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
variable_name: str
|
|
43
|
+
"""Variable name being compared (e.g., 'env' or 'self.status')."""
|
|
44
|
+
|
|
45
|
+
compared_value: str
|
|
46
|
+
"""The string literal value being compared to."""
|
|
47
|
+
|
|
48
|
+
operator: str
|
|
49
|
+
"""The comparison operator ('==' or '!=')."""
|
|
50
|
+
|
|
51
|
+
line_number: int
|
|
52
|
+
"""Line number where the comparison occurs (1-indexed)."""
|
|
53
|
+
|
|
54
|
+
column: int
|
|
55
|
+
"""Column number where the comparison starts (0-indexed)."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Excluded variable names that are common false positives
|
|
59
|
+
_EXCLUDED_VARIABLES = frozenset(
|
|
60
|
+
{
|
|
61
|
+
"__name__",
|
|
62
|
+
"__class__.__name__",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Excluded values that are common in legitimate comparisons
|
|
67
|
+
_EXCLUDED_VALUES = frozenset(
|
|
68
|
+
{
|
|
69
|
+
"__main__",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ComparisonTracker(ast.NodeVisitor): # thailint: ignore[srp]
|
|
75
|
+
"""Tracks scattered string comparisons in Python AST.
|
|
76
|
+
|
|
77
|
+
Finds patterns like `if env == "production"` and `if status != "deleted"` where
|
|
78
|
+
string literals are used for comparisons that could use enums instead.
|
|
79
|
+
|
|
80
|
+
Note: Method count exceeds SRP limit because AST traversal requires multiple helper
|
|
81
|
+
methods for extracting variable names, attribute names, and pattern filtering. All
|
|
82
|
+
methods support the single responsibility of tracking string comparisons.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
"""Initialize the tracker."""
|
|
87
|
+
self.patterns: list[ComparisonPattern] = []
|
|
88
|
+
|
|
89
|
+
def find_patterns(self, tree: ast.AST) -> list[ComparisonPattern]:
|
|
90
|
+
"""Find all string comparisons in the AST.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
tree: The AST to analyze
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of ComparisonPattern instances for each detected comparison
|
|
97
|
+
"""
|
|
98
|
+
self.patterns = []
|
|
99
|
+
self.visit(tree)
|
|
100
|
+
return self.patterns
|
|
101
|
+
|
|
102
|
+
def visit_Compare(self, node: ast.Compare) -> None: # pylint: disable=invalid-name
|
|
103
|
+
"""Visit a Compare node to check for string comparisons.
|
|
104
|
+
|
|
105
|
+
Handles both `var == "string"` and `"string" == var` patterns.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
node: The Compare node to analyze
|
|
109
|
+
"""
|
|
110
|
+
self._check_comparison(node)
|
|
111
|
+
self.generic_visit(node)
|
|
112
|
+
|
|
113
|
+
def _check_comparison(self, node: ast.Compare) -> None:
|
|
114
|
+
"""Check if comparison is a string comparison to track.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
node: The Compare node to check
|
|
118
|
+
"""
|
|
119
|
+
# Only handle simple binary comparisons
|
|
120
|
+
if len(node.ops) != 1 or len(node.comparators) != 1:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
operator = node.ops[0]
|
|
124
|
+
if not isinstance(operator, (ast.Eq, ast.NotEq)):
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
op_str = "==" if isinstance(operator, ast.Eq) else "!="
|
|
128
|
+
left = node.left
|
|
129
|
+
right = node.comparators[0]
|
|
130
|
+
|
|
131
|
+
# Try both orientations: var == "string" and "string" == var
|
|
132
|
+
self._try_extract_pattern(left, right, op_str, node)
|
|
133
|
+
self._try_extract_pattern(right, left, op_str, node)
|
|
134
|
+
|
|
135
|
+
def _try_extract_pattern(
|
|
136
|
+
self,
|
|
137
|
+
var_side: ast.expr,
|
|
138
|
+
string_side: ast.expr,
|
|
139
|
+
operator: str,
|
|
140
|
+
node: ast.Compare,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Try to extract a pattern from a comparison.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
var_side: The expression that might be a variable
|
|
146
|
+
string_side: The expression that might be a string literal
|
|
147
|
+
operator: The comparison operator
|
|
148
|
+
node: The original Compare node for location info
|
|
149
|
+
"""
|
|
150
|
+
# Check if string_side is a string literal
|
|
151
|
+
if not isinstance(string_side, ast.Constant):
|
|
152
|
+
return
|
|
153
|
+
if not isinstance(string_side.value, str):
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
string_value = string_side.value
|
|
157
|
+
|
|
158
|
+
# Extract variable name
|
|
159
|
+
var_name = self._extract_variable_name(var_side)
|
|
160
|
+
if var_name is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Check for excluded patterns
|
|
164
|
+
if self._should_exclude(var_name, string_value):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
self._add_pattern(var_name, string_value, operator, node)
|
|
168
|
+
|
|
169
|
+
def _extract_variable_name(self, node: ast.expr) -> str | None:
|
|
170
|
+
"""Extract variable name from an expression.
|
|
171
|
+
|
|
172
|
+
Handles simple names (var) and attribute access (obj.attr).
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
node: The expression to extract from
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Variable name string or None if not extractable
|
|
179
|
+
"""
|
|
180
|
+
if isinstance(node, ast.Name):
|
|
181
|
+
return node.id
|
|
182
|
+
if isinstance(node, ast.Attribute):
|
|
183
|
+
return self._extract_attribute_name(node)
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def _extract_attribute_name(self, node: ast.Attribute) -> str | None:
|
|
187
|
+
"""Extract attribute name from an attribute access.
|
|
188
|
+
|
|
189
|
+
Builds qualified names like 'obj.attr' or 'a.b.attr'.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
node: The Attribute node
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Qualified attribute name or None if too complex
|
|
196
|
+
"""
|
|
197
|
+
parts: list[str] = [node.attr]
|
|
198
|
+
current = node.value
|
|
199
|
+
depth = 0
|
|
200
|
+
|
|
201
|
+
while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
|
|
202
|
+
if isinstance(current, ast.Name):
|
|
203
|
+
parts.append(current.id)
|
|
204
|
+
break
|
|
205
|
+
if isinstance(current, ast.Attribute):
|
|
206
|
+
parts.append(current.attr)
|
|
207
|
+
current = current.value
|
|
208
|
+
depth += 1
|
|
209
|
+
else:
|
|
210
|
+
# Complex expression, still return what we have
|
|
211
|
+
parts.append("_")
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
return ".".join(reversed(parts))
|
|
215
|
+
|
|
216
|
+
def _should_exclude(self, var_name: str, string_value: str) -> bool:
|
|
217
|
+
"""Check if this comparison should be excluded.
|
|
218
|
+
|
|
219
|
+
Filters out common patterns that are not stringly-typed code:
|
|
220
|
+
- __name__ == "__main__"
|
|
221
|
+
- __class__.__name__ checks
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
var_name: The variable name
|
|
225
|
+
string_value: The string value
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if the comparison should be excluded
|
|
229
|
+
"""
|
|
230
|
+
if var_name in _EXCLUDED_VARIABLES:
|
|
231
|
+
return True
|
|
232
|
+
if string_value in _EXCLUDED_VALUES:
|
|
233
|
+
return True
|
|
234
|
+
# Also exclude if the full qualified name ends with __name__
|
|
235
|
+
if var_name.endswith("__name__"):
|
|
236
|
+
return True
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def _add_pattern(
|
|
240
|
+
self, var_name: str, string_value: str, operator: str, node: ast.Compare
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Create and add a comparison pattern to results.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
var_name: The variable name
|
|
246
|
+
string_value: The string value being compared
|
|
247
|
+
operator: The comparison operator
|
|
248
|
+
node: The Compare node for location info
|
|
249
|
+
"""
|
|
250
|
+
pattern = ComparisonPattern(
|
|
251
|
+
variable_name=var_name,
|
|
252
|
+
compared_value=string_value,
|
|
253
|
+
operator=operator,
|
|
254
|
+
line_number=node.lineno,
|
|
255
|
+
column=node.col_offset,
|
|
256
|
+
)
|
|
257
|
+
self.patterns.append(pattern)
|
|
@@ -15,6 +15,9 @@ Exports: extract_from_condition, is_simple_string_equality, get_string_constant
|
|
|
15
15
|
Interfaces: Functions for extracting string comparisons from AST nodes
|
|
16
16
|
|
|
17
17
|
Implementation: Recursive traversal of BoolOp nodes with Compare extraction
|
|
18
|
+
|
|
19
|
+
Suppressions:
|
|
20
|
+
- type:ignore[attr-defined]: AST node attribute access varies by node type (value.value)
|
|
18
21
|
"""
|
|
19
22
|
|
|
20
23
|
import ast
|
|
@@ -18,6 +18,9 @@ Exports: ConditionalPatternDetector class, EqualityChainPattern dataclass
|
|
|
18
18
|
Interfaces: ConditionalPatternDetector.find_patterns(tree) -> list[EqualityChainPattern]
|
|
19
19
|
|
|
20
20
|
Implementation: AST NodeVisitor pattern with If node chain traversal and Match statement handling
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- invalid-name: visit_If, visit_Match follow AST NodeVisitor method naming convention
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
26
|
import ast
|
|
@@ -110,7 +113,7 @@ class ConditionalPatternDetector(ast.NodeVisitor):
|
|
|
110
113
|
"""
|
|
111
114
|
pattern = analyze_match_statement(node, EqualityChainPattern)
|
|
112
115
|
if pattern is not None:
|
|
113
|
-
self.patterns.append(pattern)
|
|
116
|
+
self.patterns.append(pattern)
|
|
114
117
|
self.generic_visit(node)
|
|
115
118
|
|
|
116
119
|
def _analyze_if_chain(self, node: ast.If) -> None:
|
|
@@ -17,16 +17,22 @@ Interfaces: MatchStatementAnalyzer.analyze(node) -> EqualityChainPattern | None
|
|
|
17
17
|
Implementation: AST pattern matching for MatchValue nodes with string constants
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
20
22
|
import ast
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
21
24
|
|
|
22
25
|
from .constants import MIN_VALUES_FOR_PATTERN
|
|
23
26
|
from .variable_extractor import extract_variable_name
|
|
24
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .conditional_detector import EqualityChainPattern
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
def analyze_match_statement(
|
|
27
33
|
node: ast.Match,
|
|
28
|
-
pattern_class: type,
|
|
29
|
-
) ->
|
|
34
|
+
pattern_class: type[EqualityChainPattern],
|
|
35
|
+
) -> EqualityChainPattern | None:
|
|
30
36
|
"""Analyze a match statement for string case patterns.
|
|
31
37
|
|
|
32
38
|
Args:
|
|
@@ -18,6 +18,9 @@ Exports: MembershipValidationDetector class, MembershipPattern dataclass
|
|
|
18
18
|
Interfaces: MembershipValidationDetector.find_patterns(tree) -> list[MembershipPattern]
|
|
19
19
|
|
|
20
20
|
Implementation: AST NodeVisitor pattern with Compare node handling for In/NotIn operators
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- invalid-name: visit_Compare follows AST NodeVisitor method naming convention
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
26
|
import ast
|