thailint 0.2.0__py3-none-any.whl → 0.15.3__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/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -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 +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -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 +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- 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 +100 -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 +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- 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 +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -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 +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Coordinate Python stringly-typed pattern detection
|
|
3
|
+
|
|
4
|
+
Scope: Orchestrate detection of all stringly-typed patterns in Python files
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonStringlyTypedAnalyzer class that coordinates detection of
|
|
7
|
+
stringly-typed patterns across Python source files. Uses MembershipValidationDetector
|
|
8
|
+
to find 'x in ("a", "b")' patterns, ConditionalPatternDetector to find if/elif chains
|
|
9
|
+
and match statements, and FunctionCallTracker to find function calls with string
|
|
10
|
+
arguments. Returns unified AnalysisResult objects for validation patterns and
|
|
11
|
+
FunctionCallResult objects for function calls. Handles AST parsing errors gracefully
|
|
12
|
+
and provides a single entry point for Python analysis. Supports configuration options
|
|
13
|
+
for filtering and thresholds.
|
|
14
|
+
|
|
15
|
+
Dependencies: ast module, MembershipValidationDetector, ConditionalPatternDetector,
|
|
16
|
+
FunctionCallTracker, StringlyTypedConfig
|
|
17
|
+
|
|
18
|
+
Exports: PythonStringlyTypedAnalyzer class, AnalysisResult dataclass, FunctionCallResult dataclass,
|
|
19
|
+
ComparisonResult dataclass
|
|
20
|
+
|
|
21
|
+
Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[AnalysisResult],
|
|
22
|
+
PythonStringlyTypedAnalyzer.analyze_function_calls(code, file_path) -> list[FunctionCallResult]
|
|
23
|
+
|
|
24
|
+
Implementation: Facade pattern coordinating multiple detectors with unified result format
|
|
25
|
+
|
|
26
|
+
Suppressions:
|
|
27
|
+
- srp: Analyzer coordinates multiple detectors (membership, conditional, call tracker).
|
|
28
|
+
Facade pattern justifies combining orchestration methods.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import ast
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from ..config import StringlyTypedConfig
|
|
36
|
+
from .call_tracker import FunctionCallPattern, FunctionCallTracker
|
|
37
|
+
from .comparison_tracker import ComparisonPattern, ComparisonTracker
|
|
38
|
+
from .conditional_detector import ConditionalPatternDetector, EqualityChainPattern
|
|
39
|
+
from .validation_detector import MembershipPattern, MembershipValidationDetector
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class AnalysisResult:
|
|
44
|
+
"""Represents a stringly-typed pattern detected in Python code.
|
|
45
|
+
|
|
46
|
+
Provides a unified representation of detected patterns from all detectors,
|
|
47
|
+
including pattern type, string values, location, and contextual information.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
pattern_type: str
|
|
51
|
+
"""Type of pattern detected: 'membership_validation', 'equality_chain', etc."""
|
|
52
|
+
|
|
53
|
+
string_values: set[str]
|
|
54
|
+
"""Set of string values used in the pattern."""
|
|
55
|
+
|
|
56
|
+
file_path: Path
|
|
57
|
+
"""Path to the file containing the pattern."""
|
|
58
|
+
|
|
59
|
+
line_number: int
|
|
60
|
+
"""Line number where the pattern occurs (1-indexed)."""
|
|
61
|
+
|
|
62
|
+
column: int
|
|
63
|
+
"""Column number where the pattern starts (0-indexed)."""
|
|
64
|
+
|
|
65
|
+
variable_name: str | None
|
|
66
|
+
"""Variable name involved in the pattern, if identifiable."""
|
|
67
|
+
|
|
68
|
+
details: str
|
|
69
|
+
"""Human-readable description of the detected pattern."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class FunctionCallResult:
|
|
74
|
+
"""Represents a function call with a string argument.
|
|
75
|
+
|
|
76
|
+
Provides information about a single function call with a string literal
|
|
77
|
+
argument, enabling aggregation across files to detect limited value sets.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
function_name: str
|
|
81
|
+
"""Fully qualified function name (e.g., 'process' or 'obj.method')."""
|
|
82
|
+
|
|
83
|
+
param_index: int
|
|
84
|
+
"""Index of the parameter receiving the string value (0-indexed)."""
|
|
85
|
+
|
|
86
|
+
string_value: str
|
|
87
|
+
"""The string literal value passed to the function."""
|
|
88
|
+
|
|
89
|
+
file_path: Path
|
|
90
|
+
"""Path to the file containing the call."""
|
|
91
|
+
|
|
92
|
+
line_number: int
|
|
93
|
+
"""Line number where the call occurs (1-indexed)."""
|
|
94
|
+
|
|
95
|
+
column: int
|
|
96
|
+
"""Column number where the call starts (0-indexed)."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ComparisonResult:
|
|
101
|
+
"""Represents a string comparison found in Python code.
|
|
102
|
+
|
|
103
|
+
Provides information about a comparison like `if env == "production"` to
|
|
104
|
+
enable cross-file aggregation for detecting scattered comparisons that
|
|
105
|
+
suggest missing enums.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
variable_name: str
|
|
109
|
+
"""Variable name being compared (e.g., 'env' or 'self.status')."""
|
|
110
|
+
|
|
111
|
+
compared_value: str
|
|
112
|
+
"""The string literal value being compared to."""
|
|
113
|
+
|
|
114
|
+
operator: str
|
|
115
|
+
"""The comparison operator ('==' or '!=')."""
|
|
116
|
+
|
|
117
|
+
file_path: Path
|
|
118
|
+
"""Path to the file containing the comparison."""
|
|
119
|
+
|
|
120
|
+
line_number: int
|
|
121
|
+
"""Line number where the comparison occurs (1-indexed)."""
|
|
122
|
+
|
|
123
|
+
column: int
|
|
124
|
+
"""Column number where the comparison starts (0-indexed)."""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PythonStringlyTypedAnalyzer: # thailint: ignore[srp]
|
|
128
|
+
"""Analyzes Python code for stringly-typed patterns.
|
|
129
|
+
|
|
130
|
+
Coordinates detection of various stringly-typed patterns including membership
|
|
131
|
+
validation ('x in ("a", "b")'), equality chains ('if x == "a" elif x == "b"'),
|
|
132
|
+
and function calls with string arguments ('process("active")').
|
|
133
|
+
Provides configuration-aware analysis with filtering support.
|
|
134
|
+
|
|
135
|
+
Note: Method count exceeds SRP limit due to analyzer coordination role. Multiple
|
|
136
|
+
analysis methods are required for different pattern types (membership, conditional,
|
|
137
|
+
function calls, comparisons) and their converters.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, config: StringlyTypedConfig | None = None) -> None:
|
|
141
|
+
"""Initialize the analyzer with optional configuration.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
config: Configuration for stringly-typed detection. Uses defaults if None.
|
|
145
|
+
"""
|
|
146
|
+
self.config = config or StringlyTypedConfig()
|
|
147
|
+
self._membership_detector = MembershipValidationDetector()
|
|
148
|
+
self._conditional_detector = ConditionalPatternDetector()
|
|
149
|
+
self._call_tracker = FunctionCallTracker()
|
|
150
|
+
self._comparison_tracker = ComparisonTracker()
|
|
151
|
+
|
|
152
|
+
def analyze(self, code: str, file_path: Path) -> list[AnalysisResult]:
|
|
153
|
+
"""Analyze Python code for stringly-typed patterns.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
code: Python source code to analyze
|
|
157
|
+
file_path: Path to the file being analyzed
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of AnalysisResult instances for each detected pattern
|
|
161
|
+
"""
|
|
162
|
+
tree = self._parse_code(code)
|
|
163
|
+
if tree is None:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
results: list[AnalysisResult] = []
|
|
167
|
+
|
|
168
|
+
# Detect membership validation patterns
|
|
169
|
+
membership_patterns = self._membership_detector.find_patterns(tree)
|
|
170
|
+
results.extend(
|
|
171
|
+
self._convert_membership_pattern(pattern, file_path) for pattern in membership_patterns
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Detect equality chain patterns
|
|
175
|
+
conditional_patterns = self._conditional_detector.find_patterns(tree)
|
|
176
|
+
results.extend(
|
|
177
|
+
self._convert_conditional_pattern(pattern, file_path)
|
|
178
|
+
for pattern in conditional_patterns
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return results
|
|
182
|
+
|
|
183
|
+
def _parse_code(self, code: str) -> ast.AST | None:
|
|
184
|
+
"""Parse Python source code into an AST.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
code: Python source code to parse
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
AST if parsing succeeds, None if parsing fails
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
return ast.parse(code)
|
|
194
|
+
except SyntaxError:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def _convert_membership_pattern(
|
|
198
|
+
self, pattern: MembershipPattern, file_path: Path
|
|
199
|
+
) -> AnalysisResult:
|
|
200
|
+
"""Convert a MembershipPattern to unified AnalysisResult.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
pattern: Detected membership pattern
|
|
204
|
+
file_path: Path to the file containing the pattern
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
AnalysisResult representing the pattern
|
|
208
|
+
"""
|
|
209
|
+
values_str = ", ".join(sorted(pattern.string_values))
|
|
210
|
+
var_info = f" on '{pattern.variable_name}'" if pattern.variable_name else ""
|
|
211
|
+
details = (
|
|
212
|
+
f"Membership validation{var_info} with {len(pattern.string_values)} "
|
|
213
|
+
f"string values ({pattern.operator}): {values_str}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return AnalysisResult(
|
|
217
|
+
pattern_type="membership_validation",
|
|
218
|
+
string_values=pattern.string_values,
|
|
219
|
+
file_path=file_path,
|
|
220
|
+
line_number=pattern.line_number,
|
|
221
|
+
column=pattern.column,
|
|
222
|
+
variable_name=pattern.variable_name,
|
|
223
|
+
details=details,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _convert_conditional_pattern(
|
|
227
|
+
self, pattern: EqualityChainPattern, file_path: Path
|
|
228
|
+
) -> AnalysisResult:
|
|
229
|
+
"""Convert an EqualityChainPattern to unified AnalysisResult.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
pattern: Detected equality chain pattern
|
|
233
|
+
file_path: Path to the file containing the pattern
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
AnalysisResult representing the pattern
|
|
237
|
+
"""
|
|
238
|
+
values_str = ", ".join(sorted(pattern.string_values))
|
|
239
|
+
var_info = f" on '{pattern.variable_name}'" if pattern.variable_name else ""
|
|
240
|
+
pattern_label = self._get_pattern_label(pattern.pattern_type)
|
|
241
|
+
details = (
|
|
242
|
+
f"{pattern_label}{var_info} with {len(pattern.string_values)} "
|
|
243
|
+
f"string values: {values_str}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return AnalysisResult(
|
|
247
|
+
pattern_type=pattern.pattern_type,
|
|
248
|
+
string_values=pattern.string_values,
|
|
249
|
+
file_path=file_path,
|
|
250
|
+
line_number=pattern.line_number,
|
|
251
|
+
column=pattern.column,
|
|
252
|
+
variable_name=pattern.variable_name,
|
|
253
|
+
details=details,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def _get_pattern_label(self, pattern_type: str) -> str:
|
|
257
|
+
"""Get human-readable label for a pattern type.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
pattern_type: The pattern type string
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Human-readable label for the pattern
|
|
264
|
+
"""
|
|
265
|
+
labels = {
|
|
266
|
+
"equality_chain": "Equality chain",
|
|
267
|
+
"or_combined": "Or-combined comparison",
|
|
268
|
+
"match_statement": "Match statement",
|
|
269
|
+
}
|
|
270
|
+
return labels.get(pattern_type, "Conditional pattern")
|
|
271
|
+
|
|
272
|
+
def analyze_function_calls(self, code: str, file_path: Path) -> list[FunctionCallResult]:
|
|
273
|
+
"""Analyze Python code for function calls with string arguments.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
code: Python source code to analyze
|
|
277
|
+
file_path: Path to the file being analyzed
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of FunctionCallResult instances for each detected call
|
|
281
|
+
"""
|
|
282
|
+
tree = self._parse_code(code)
|
|
283
|
+
if tree is None:
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
call_patterns = self._call_tracker.find_patterns(tree)
|
|
287
|
+
return [self._convert_call_pattern(pattern, file_path) for pattern in call_patterns]
|
|
288
|
+
|
|
289
|
+
def _convert_call_pattern(
|
|
290
|
+
self, pattern: FunctionCallPattern, file_path: Path
|
|
291
|
+
) -> FunctionCallResult:
|
|
292
|
+
"""Convert a FunctionCallPattern to FunctionCallResult.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
pattern: Detected function call pattern
|
|
296
|
+
file_path: Path to the file containing the call
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
FunctionCallResult representing the call
|
|
300
|
+
"""
|
|
301
|
+
return FunctionCallResult(
|
|
302
|
+
function_name=pattern.function_name,
|
|
303
|
+
param_index=pattern.param_index,
|
|
304
|
+
string_value=pattern.string_value,
|
|
305
|
+
file_path=file_path,
|
|
306
|
+
line_number=pattern.line_number,
|
|
307
|
+
column=pattern.column,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def analyze_comparisons(self, code: str, file_path: Path) -> list[ComparisonResult]:
|
|
311
|
+
"""Analyze Python code for string comparisons.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
code: Python source code to analyze
|
|
315
|
+
file_path: Path to the file being analyzed
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of ComparisonResult instances for each detected comparison
|
|
319
|
+
"""
|
|
320
|
+
tree = self._parse_code(code)
|
|
321
|
+
if tree is None:
|
|
322
|
+
return []
|
|
323
|
+
|
|
324
|
+
comparison_patterns = self._comparison_tracker.find_patterns(tree)
|
|
325
|
+
return [
|
|
326
|
+
self._convert_comparison_pattern(pattern, file_path) for pattern in comparison_patterns
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
def _convert_comparison_pattern(
|
|
330
|
+
self, pattern: ComparisonPattern, file_path: Path
|
|
331
|
+
) -> ComparisonResult:
|
|
332
|
+
"""Convert a ComparisonPattern to ComparisonResult.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
pattern: Detected comparison pattern
|
|
336
|
+
file_path: Path to the file containing the comparison
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ComparisonResult representing the comparison
|
|
340
|
+
"""
|
|
341
|
+
return ComparisonResult(
|
|
342
|
+
variable_name=pattern.variable_name,
|
|
343
|
+
compared_value=pattern.compared_value,
|
|
344
|
+
operator=pattern.operator,
|
|
345
|
+
file_path=file_path,
|
|
346
|
+
line_number=pattern.line_number,
|
|
347
|
+
column=pattern.column,
|
|
348
|
+
)
|
|
@@ -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)
|