thailint 0.11.0__py3-none-any.whl → 0.12.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/cli/linters/code_smells.py +114 -7
- src/cli/utils.py +29 -9
- src/linters/stringly_typed/__init__.py +22 -9
- src/linters/stringly_typed/config.py +28 -3
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +137 -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 +344 -0
- src/linters/stringly_typed/python/__init__.py +9 -5
- src/linters/stringly_typed/python/analyzer.py +155 -9
- src/linters/stringly_typed/python/call_tracker.py +172 -0
- src/linters/stringly_typed/python/comparison_tracker.py +252 -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 +329 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
- src/linters/stringly_typed/violation_generator.py +376 -0
- {thailint-0.11.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
- {thailint-0.11.0.dist-info → thailint-0.12.0.dist-info}/RECORD +25 -11
- {thailint-0.11.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.12.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared ignore pattern matching utilities
|
|
3
|
+
|
|
4
|
+
Scope: Common ignore pattern checking for stringly-typed linter components
|
|
5
|
+
|
|
6
|
+
Overview: Provides shared utility functions for checking if file paths match ignore patterns.
|
|
7
|
+
Used by both the main linter and violation generator to avoid duplicating ignore pattern
|
|
8
|
+
matching logic. Centralizes the ignore pattern matching algorithm.
|
|
9
|
+
|
|
10
|
+
Dependencies: pathlib.Path, fnmatch
|
|
11
|
+
|
|
12
|
+
Exports: is_ignored function
|
|
13
|
+
|
|
14
|
+
Interfaces: is_ignored(file_path, ignore_patterns) -> bool
|
|
15
|
+
|
|
16
|
+
Implementation: Glob pattern matching with fnmatch for flexible ignore patterns
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fnmatch
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_ignored(file_path: str | Path, ignore_patterns: list[str]) -> bool:
|
|
24
|
+
"""Check if file path matches any ignore pattern.
|
|
25
|
+
|
|
26
|
+
Supports glob patterns like:
|
|
27
|
+
- **/tests/** - matches any file in tests directories
|
|
28
|
+
- **/*_test.py - matches any file ending in _test.py
|
|
29
|
+
- tests/ - simple substring match
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
file_path: Path to check (string or Path object)
|
|
33
|
+
ignore_patterns: List of patterns to match against
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if file should be ignored
|
|
37
|
+
"""
|
|
38
|
+
if not ignore_patterns:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
path_str = str(file_path)
|
|
42
|
+
|
|
43
|
+
for pattern in ignore_patterns:
|
|
44
|
+
# Use fnmatch for glob-style patterns
|
|
45
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
46
|
+
return True
|
|
47
|
+
# Also check if pattern appears as substring (for simple patterns)
|
|
48
|
+
if pattern in path_str:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main stringly-typed linter rule with cross-file detection
|
|
3
|
+
|
|
4
|
+
Scope: StringlyTypedRule implementing MultiLanguageLintRule for cross-file pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Implements stringly-typed linter rule following MultiLanguageLintRule interface with
|
|
7
|
+
cross-file detection using SQLite storage. Orchestrates pattern detection by delegating to
|
|
8
|
+
language-specific analyzers (Python, TypeScript). During check() phase, patterns are collected
|
|
9
|
+
into storage. During finalize() phase, storage is queried for patterns appearing across
|
|
10
|
+
multiple files and violations are generated. Maintains minimal orchestration logic to comply
|
|
11
|
+
with SRP.
|
|
12
|
+
|
|
13
|
+
Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringlyTypedAnalyzer,
|
|
14
|
+
StringlyTypedStorage, StorageInitializer, ViolationGenerator, StringlyTypedConfig
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedRule class
|
|
17
|
+
|
|
18
|
+
Interfaces: StringlyTypedRule.check(context) -> list[Violation],
|
|
19
|
+
StringlyTypedRule.finalize() -> list[Violation]
|
|
20
|
+
|
|
21
|
+
Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
|
|
22
|
+
Delegates all logic to helper classes, maintains only orchestration and state.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
31
|
+
from src.core.linter_utils import load_linter_config
|
|
32
|
+
from src.core.types import Violation
|
|
33
|
+
|
|
34
|
+
from .config import StringlyTypedConfig
|
|
35
|
+
from .ignore_utils import is_ignored
|
|
36
|
+
from .python.analyzer import (
|
|
37
|
+
AnalysisResult,
|
|
38
|
+
ComparisonResult,
|
|
39
|
+
FunctionCallResult,
|
|
40
|
+
PythonStringlyTypedAnalyzer,
|
|
41
|
+
)
|
|
42
|
+
from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
|
|
43
|
+
from .storage_initializer import StorageInitializer
|
|
44
|
+
from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
|
|
45
|
+
from .violation_generator import ViolationGenerator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_string_set_hash(values: set[str]) -> int:
|
|
49
|
+
"""Compute consistent hash for a set of strings.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
values: Set of string values to hash
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Hash value based on sorted, lowercased strings
|
|
56
|
+
"""
|
|
57
|
+
return hash(tuple(sorted(s.lower() for s in values)))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
|
|
61
|
+
"""Check if context and storage are ready for analysis."""
|
|
62
|
+
return bool(context.file_path and context.file_content and storage)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
|
|
66
|
+
"""Convert AnalysisResult to StoredPattern.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
result: Analysis result from language analyzer
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
StoredPattern for storage
|
|
73
|
+
"""
|
|
74
|
+
return StoredPattern(
|
|
75
|
+
file_path=result.file_path,
|
|
76
|
+
line_number=result.line_number,
|
|
77
|
+
column=result.column,
|
|
78
|
+
variable_name=result.variable_name,
|
|
79
|
+
string_set_hash=compute_string_set_hash(result.string_values),
|
|
80
|
+
string_values=sorted(result.string_values),
|
|
81
|
+
pattern_type=result.pattern_type,
|
|
82
|
+
details=result.details,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
|
|
87
|
+
"""Convert FunctionCallResult to StoredFunctionCall.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
result: Function call result from language analyzer
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
StoredFunctionCall for storage
|
|
94
|
+
"""
|
|
95
|
+
return StoredFunctionCall(
|
|
96
|
+
file_path=result.file_path,
|
|
97
|
+
line_number=result.line_number,
|
|
98
|
+
column=result.column,
|
|
99
|
+
function_name=result.function_name,
|
|
100
|
+
param_index=result.param_index,
|
|
101
|
+
string_value=result.string_value,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
|
|
106
|
+
"""Convert ComparisonResult to StoredComparison.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
result: Comparison result from language analyzer
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
StoredComparison for storage
|
|
113
|
+
"""
|
|
114
|
+
return StoredComparison(
|
|
115
|
+
file_path=result.file_path,
|
|
116
|
+
line_number=result.line_number,
|
|
117
|
+
column=result.column,
|
|
118
|
+
variable_name=result.variable_name,
|
|
119
|
+
compared_value=result.compared_value,
|
|
120
|
+
operator=result.operator,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class StringlyTypedComponents:
|
|
126
|
+
"""Component dependencies for stringly-typed linter."""
|
|
127
|
+
|
|
128
|
+
storage_initializer: StorageInitializer
|
|
129
|
+
violation_generator: ViolationGenerator
|
|
130
|
+
python_analyzer: PythonStringlyTypedAnalyzer
|
|
131
|
+
typescript_analyzer: TypeScriptStringlyTypedAnalyzer
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
135
|
+
"""Detects stringly-typed patterns across project files.
|
|
136
|
+
|
|
137
|
+
Uses two-phase pattern:
|
|
138
|
+
1. check() - Collects patterns into SQLite storage (returns empty list)
|
|
139
|
+
2. finalize() - Queries storage and generates violations for cross-file patterns
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self) -> None:
|
|
143
|
+
"""Initialize the stringly-typed rule with helper components."""
|
|
144
|
+
self._storage: StringlyTypedStorage | None = None
|
|
145
|
+
self._initialized = False
|
|
146
|
+
self._config: StringlyTypedConfig | None = None
|
|
147
|
+
|
|
148
|
+
# Helper components grouped to reduce instance attributes
|
|
149
|
+
self._helpers = StringlyTypedComponents(
|
|
150
|
+
storage_initializer=StorageInitializer(),
|
|
151
|
+
violation_generator=ViolationGenerator(),
|
|
152
|
+
python_analyzer=PythonStringlyTypedAnalyzer(),
|
|
153
|
+
typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def rule_id(self) -> str:
|
|
158
|
+
"""Unique identifier for this rule."""
|
|
159
|
+
return "stringly-typed.repeated-validation"
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def rule_name(self) -> str:
|
|
163
|
+
"""Human-readable name for this rule."""
|
|
164
|
+
return "Stringly-Typed Pattern"
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def description(self) -> str:
|
|
168
|
+
"""Description of what this rule checks."""
|
|
169
|
+
return "Detects stringly-typed code patterns that should use enums"
|
|
170
|
+
|
|
171
|
+
def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
|
|
172
|
+
"""Load configuration from context.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
context: Lint context with metadata
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
StringlyTypedConfig instance
|
|
179
|
+
"""
|
|
180
|
+
return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
|
|
181
|
+
|
|
182
|
+
def _check_python(
|
|
183
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
184
|
+
) -> list[Violation]:
|
|
185
|
+
"""Analyze Python code and store patterns.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
context: Lint context with file content
|
|
189
|
+
config: Stringly-typed configuration
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Empty list (violations generated in finalize)
|
|
193
|
+
"""
|
|
194
|
+
self._ensure_storage_initialized(context, config)
|
|
195
|
+
self._analyze_python_file(context, config)
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
def _check_typescript(
|
|
199
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
200
|
+
) -> list[Violation]:
|
|
201
|
+
"""Analyze TypeScript code and store patterns.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
context: Lint context with file content
|
|
205
|
+
config: Stringly-typed configuration
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Empty list (violations generated in finalize)
|
|
209
|
+
"""
|
|
210
|
+
self._ensure_storage_initialized(context, config)
|
|
211
|
+
self._analyze_typescript_file(context, config)
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
def _analyze_typescript_file(
|
|
215
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Analyze TypeScript file and store patterns.
|
|
218
|
+
|
|
219
|
+
Uses single-parse optimization to avoid duplicate parsing overhead.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
context: Lint context with file content
|
|
223
|
+
config: Stringly-typed configuration
|
|
224
|
+
"""
|
|
225
|
+
if not self._should_analyze(context, config):
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
file_path = Path(context.file_path) # type: ignore[arg-type]
|
|
229
|
+
file_content = context.file_content or ""
|
|
230
|
+
self._helpers.typescript_analyzer.config = config
|
|
231
|
+
|
|
232
|
+
# Single-parse optimization: parse once, run both trackers
|
|
233
|
+
call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
|
|
234
|
+
file_content, file_path
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
238
|
+
self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
|
|
239
|
+
|
|
240
|
+
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
241
|
+
self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
|
|
242
|
+
|
|
243
|
+
def _ensure_storage_initialized(
|
|
244
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Initialize storage and analyzers on first call.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
context: Lint context
|
|
250
|
+
config: Stringly-typed configuration
|
|
251
|
+
"""
|
|
252
|
+
if not self._initialized:
|
|
253
|
+
self._storage = self._helpers.storage_initializer.initialize(context, config)
|
|
254
|
+
self._config = config
|
|
255
|
+
self._initialized = True
|
|
256
|
+
|
|
257
|
+
def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
|
|
258
|
+
"""Analyze Python file and store patterns.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
context: Lint context with file content
|
|
262
|
+
config: Stringly-typed configuration
|
|
263
|
+
"""
|
|
264
|
+
if not self._should_analyze(context, config):
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
file_path = Path(context.file_path) # type: ignore[arg-type]
|
|
268
|
+
file_content = context.file_content or ""
|
|
269
|
+
self._helpers.python_analyzer.config = config
|
|
270
|
+
|
|
271
|
+
self._store_validation_patterns(file_content, file_path)
|
|
272
|
+
self._store_function_calls(file_content, file_path)
|
|
273
|
+
self._store_comparisons(file_content, file_path)
|
|
274
|
+
|
|
275
|
+
def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
|
|
276
|
+
"""Check if file should be analyzed.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
context: Lint context
|
|
280
|
+
config: Configuration
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if file should be analyzed
|
|
284
|
+
"""
|
|
285
|
+
if not _is_ready_for_analysis(context, self._storage):
|
|
286
|
+
return False
|
|
287
|
+
file_path = Path(context.file_path) # type: ignore[arg-type]
|
|
288
|
+
return not is_ignored(file_path, config.ignore)
|
|
289
|
+
|
|
290
|
+
def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
|
|
291
|
+
"""Analyze and store validation patterns.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
file_content: Python source code
|
|
295
|
+
file_path: Path to file
|
|
296
|
+
"""
|
|
297
|
+
results = self._helpers.python_analyzer.analyze(file_content, file_path)
|
|
298
|
+
self._storage.add_patterns([_convert_to_stored_pattern(r) for r in results]) # type: ignore[union-attr]
|
|
299
|
+
|
|
300
|
+
def _store_function_calls(self, file_content: str, file_path: Path) -> None:
|
|
301
|
+
"""Analyze and store function call patterns.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
file_content: Python source code
|
|
305
|
+
file_path: Path to file
|
|
306
|
+
"""
|
|
307
|
+
call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
|
|
308
|
+
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
309
|
+
self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
|
|
310
|
+
|
|
311
|
+
def _store_comparisons(self, file_content: str, file_path: Path) -> None:
|
|
312
|
+
"""Analyze and store Python comparison patterns.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
file_content: Python source code
|
|
316
|
+
file_path: Path to file
|
|
317
|
+
"""
|
|
318
|
+
comparison_results = self._helpers.python_analyzer.analyze_comparisons(
|
|
319
|
+
file_content, file_path
|
|
320
|
+
)
|
|
321
|
+
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
322
|
+
self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
|
|
323
|
+
|
|
324
|
+
def finalize(self) -> list[Violation]:
|
|
325
|
+
"""Generate violations after all files processed.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of violations for patterns appearing in multiple files
|
|
329
|
+
"""
|
|
330
|
+
if not self._storage or not self._config:
|
|
331
|
+
return []
|
|
332
|
+
|
|
333
|
+
# Generate violations from cross-file patterns
|
|
334
|
+
violations = self._helpers.violation_generator.generate_violations(
|
|
335
|
+
self._storage, self.rule_id, self._config
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Cleanup and reset state for next run
|
|
339
|
+
self._storage.close()
|
|
340
|
+
self._storage = None
|
|
341
|
+
self._config = None
|
|
342
|
+
self._initialized = False
|
|
343
|
+
|
|
344
|
+
return violations
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Python-specific detection for stringly-typed patterns
|
|
3
3
|
|
|
4
|
-
Scope: Python AST analysis for membership validation and
|
|
4
|
+
Scope: Python AST analysis for membership validation, equality chains, and function calls
|
|
5
5
|
|
|
6
6
|
Overview: Exposes Python analysis components for detecting stringly-typed patterns in Python
|
|
7
7
|
source code. Includes validation_detector for finding 'x in ("a", "b")' patterns,
|
|
8
|
-
conditional_detector for finding if/elif chains and match statements,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
conditional_detector for finding if/elif chains and match statements, call_tracker for
|
|
9
|
+
finding function calls with string arguments, and analyzer for coordinating detection
|
|
10
|
+
across Python files. Uses AST traversal to identify where plain strings are used instead
|
|
11
|
+
of proper enums or typed alternatives.
|
|
11
12
|
|
|
12
13
|
Dependencies: ast module for Python AST parsing
|
|
13
14
|
|
|
14
|
-
Exports: MembershipValidationDetector, ConditionalPatternDetector,
|
|
15
|
+
Exports: MembershipValidationDetector, ConditionalPatternDetector, FunctionCallTracker,
|
|
16
|
+
PythonStringlyTypedAnalyzer
|
|
15
17
|
|
|
16
18
|
Interfaces: Detector and analyzer classes for Python stringly-typed pattern detection
|
|
17
19
|
|
|
@@ -19,11 +21,13 @@ Implementation: AST NodeVisitor pattern for traversing Python syntax trees
|
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
from .analyzer import PythonStringlyTypedAnalyzer
|
|
24
|
+
from .call_tracker import FunctionCallTracker
|
|
22
25
|
from .conditional_detector import ConditionalPatternDetector
|
|
23
26
|
from .validation_detector import MembershipValidationDetector
|
|
24
27
|
|
|
25
28
|
__all__ = [
|
|
26
29
|
"ConditionalPatternDetector",
|
|
30
|
+
"FunctionCallTracker",
|
|
27
31
|
"MembershipValidationDetector",
|
|
28
32
|
"PythonStringlyTypedAnalyzer",
|
|
29
33
|
]
|
|
@@ -5,17 +5,21 @@ Scope: Orchestrate detection of all stringly-typed patterns in Python files
|
|
|
5
5
|
|
|
6
6
|
Overview: Provides PythonStringlyTypedAnalyzer class that coordinates detection of
|
|
7
7
|
stringly-typed patterns across Python source files. Uses MembershipValidationDetector
|
|
8
|
-
to find 'x in ("a", "b")' patterns
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
12
14
|
|
|
13
15
|
Dependencies: ast module, MembershipValidationDetector, ConditionalPatternDetector,
|
|
14
|
-
StringlyTypedConfig
|
|
16
|
+
FunctionCallTracker, StringlyTypedConfig
|
|
15
17
|
|
|
16
|
-
Exports: PythonStringlyTypedAnalyzer class, AnalysisResult dataclass
|
|
18
|
+
Exports: PythonStringlyTypedAnalyzer class, AnalysisResult dataclass, FunctionCallResult dataclass,
|
|
19
|
+
ComparisonResult dataclass
|
|
17
20
|
|
|
18
|
-
Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[AnalysisResult]
|
|
21
|
+
Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[AnalysisResult],
|
|
22
|
+
PythonStringlyTypedAnalyzer.analyze_function_calls(code, file_path) -> list[FunctionCallResult]
|
|
19
23
|
|
|
20
24
|
Implementation: Facade pattern coordinating multiple detectors with unified result format
|
|
21
25
|
"""
|
|
@@ -25,6 +29,8 @@ from dataclasses import dataclass
|
|
|
25
29
|
from pathlib import Path
|
|
26
30
|
|
|
27
31
|
from ..config import StringlyTypedConfig
|
|
32
|
+
from .call_tracker import FunctionCallPattern, FunctionCallTracker
|
|
33
|
+
from .comparison_tracker import ComparisonPattern, ComparisonTracker
|
|
28
34
|
from .conditional_detector import ConditionalPatternDetector, EqualityChainPattern
|
|
29
35
|
from .validation_detector import MembershipPattern, MembershipValidationDetector
|
|
30
36
|
|
|
@@ -59,12 +65,72 @@ class AnalysisResult:
|
|
|
59
65
|
"""Human-readable description of the detected pattern."""
|
|
60
66
|
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
@dataclass
|
|
69
|
+
class FunctionCallResult:
|
|
70
|
+
"""Represents a function call with a string argument.
|
|
71
|
+
|
|
72
|
+
Provides information about a single function call with a string literal
|
|
73
|
+
argument, enabling aggregation across files to detect limited value sets.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
function_name: str
|
|
77
|
+
"""Fully qualified function name (e.g., 'process' or 'obj.method')."""
|
|
78
|
+
|
|
79
|
+
param_index: int
|
|
80
|
+
"""Index of the parameter receiving the string value (0-indexed)."""
|
|
81
|
+
|
|
82
|
+
string_value: str
|
|
83
|
+
"""The string literal value passed to the function."""
|
|
84
|
+
|
|
85
|
+
file_path: Path
|
|
86
|
+
"""Path to the file containing the call."""
|
|
87
|
+
|
|
88
|
+
line_number: int
|
|
89
|
+
"""Line number where the call occurs (1-indexed)."""
|
|
90
|
+
|
|
91
|
+
column: int
|
|
92
|
+
"""Column number where the call starts (0-indexed)."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ComparisonResult:
|
|
97
|
+
"""Represents a string comparison found in Python code.
|
|
98
|
+
|
|
99
|
+
Provides information about a comparison like `if env == "production"` to
|
|
100
|
+
enable cross-file aggregation for detecting scattered comparisons that
|
|
101
|
+
suggest missing enums.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
variable_name: str
|
|
105
|
+
"""Variable name being compared (e.g., 'env' or 'self.status')."""
|
|
106
|
+
|
|
107
|
+
compared_value: str
|
|
108
|
+
"""The string literal value being compared to."""
|
|
109
|
+
|
|
110
|
+
operator: str
|
|
111
|
+
"""The comparison operator ('==' or '!=')."""
|
|
112
|
+
|
|
113
|
+
file_path: Path
|
|
114
|
+
"""Path to the file containing the comparison."""
|
|
115
|
+
|
|
116
|
+
line_number: int
|
|
117
|
+
"""Line number where the comparison occurs (1-indexed)."""
|
|
118
|
+
|
|
119
|
+
column: int
|
|
120
|
+
"""Column number where the comparison starts (0-indexed)."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class PythonStringlyTypedAnalyzer: # thailint: ignore srp
|
|
63
124
|
"""Analyzes Python code for stringly-typed patterns.
|
|
64
125
|
|
|
65
126
|
Coordinates detection of various stringly-typed patterns including membership
|
|
66
|
-
validation ('x in ("a", "b")')
|
|
127
|
+
validation ('x in ("a", "b")'), equality chains ('if x == "a" elif x == "b"'),
|
|
128
|
+
and function calls with string arguments ('process("active")').
|
|
67
129
|
Provides configuration-aware analysis with filtering support.
|
|
130
|
+
|
|
131
|
+
Note: Method count exceeds SRP limit due to analyzer coordination role. Multiple
|
|
132
|
+
analysis methods are required for different pattern types (membership, conditional,
|
|
133
|
+
function calls, comparisons) and their converters.
|
|
68
134
|
"""
|
|
69
135
|
|
|
70
136
|
def __init__(self, config: StringlyTypedConfig | None = None) -> None:
|
|
@@ -76,6 +142,8 @@ class PythonStringlyTypedAnalyzer:
|
|
|
76
142
|
self.config = config or StringlyTypedConfig()
|
|
77
143
|
self._membership_detector = MembershipValidationDetector()
|
|
78
144
|
self._conditional_detector = ConditionalPatternDetector()
|
|
145
|
+
self._call_tracker = FunctionCallTracker()
|
|
146
|
+
self._comparison_tracker = ComparisonTracker()
|
|
79
147
|
|
|
80
148
|
def analyze(self, code: str, file_path: Path) -> list[AnalysisResult]:
|
|
81
149
|
"""Analyze Python code for stringly-typed patterns.
|
|
@@ -196,3 +264,81 @@ class PythonStringlyTypedAnalyzer:
|
|
|
196
264
|
"match_statement": "Match statement",
|
|
197
265
|
}
|
|
198
266
|
return labels.get(pattern_type, "Conditional pattern")
|
|
267
|
+
|
|
268
|
+
def analyze_function_calls(self, code: str, file_path: Path) -> list[FunctionCallResult]:
|
|
269
|
+
"""Analyze Python code for function calls with string arguments.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
code: Python source code to analyze
|
|
273
|
+
file_path: Path to the file being analyzed
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of FunctionCallResult instances for each detected call
|
|
277
|
+
"""
|
|
278
|
+
tree = self._parse_code(code)
|
|
279
|
+
if tree is None:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
call_patterns = self._call_tracker.find_patterns(tree)
|
|
283
|
+
return [self._convert_call_pattern(pattern, file_path) for pattern in call_patterns]
|
|
284
|
+
|
|
285
|
+
def _convert_call_pattern(
|
|
286
|
+
self, pattern: FunctionCallPattern, file_path: Path
|
|
287
|
+
) -> FunctionCallResult:
|
|
288
|
+
"""Convert a FunctionCallPattern to FunctionCallResult.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
pattern: Detected function call pattern
|
|
292
|
+
file_path: Path to the file containing the call
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
FunctionCallResult representing the call
|
|
296
|
+
"""
|
|
297
|
+
return FunctionCallResult(
|
|
298
|
+
function_name=pattern.function_name,
|
|
299
|
+
param_index=pattern.param_index,
|
|
300
|
+
string_value=pattern.string_value,
|
|
301
|
+
file_path=file_path,
|
|
302
|
+
line_number=pattern.line_number,
|
|
303
|
+
column=pattern.column,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def analyze_comparisons(self, code: str, file_path: Path) -> list[ComparisonResult]:
|
|
307
|
+
"""Analyze Python code for string comparisons.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
code: Python source code to analyze
|
|
311
|
+
file_path: Path to the file being analyzed
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of ComparisonResult instances for each detected comparison
|
|
315
|
+
"""
|
|
316
|
+
tree = self._parse_code(code)
|
|
317
|
+
if tree is None:
|
|
318
|
+
return []
|
|
319
|
+
|
|
320
|
+
comparison_patterns = self._comparison_tracker.find_patterns(tree)
|
|
321
|
+
return [
|
|
322
|
+
self._convert_comparison_pattern(pattern, file_path) for pattern in comparison_patterns
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
def _convert_comparison_pattern(
|
|
326
|
+
self, pattern: ComparisonPattern, file_path: Path
|
|
327
|
+
) -> ComparisonResult:
|
|
328
|
+
"""Convert a ComparisonPattern to ComparisonResult.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
pattern: Detected comparison pattern
|
|
332
|
+
file_path: Path to the file containing the comparison
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
ComparisonResult representing the comparison
|
|
336
|
+
"""
|
|
337
|
+
return ComparisonResult(
|
|
338
|
+
variable_name=pattern.variable_name,
|
|
339
|
+
compared_value=pattern.compared_value,
|
|
340
|
+
operator=pattern.operator,
|
|
341
|
+
file_path=file_path,
|
|
342
|
+
line_number=pattern.line_number,
|
|
343
|
+
column=pattern.column,
|
|
344
|
+
)
|