thailint 0.12.0__py3-none-any.whl → 0.14.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 +9 -0
- src/cli/linters/code_patterns.py +107 -257
- src/cli/linters/code_smells.py +48 -165
- src/cli/linters/documentation.py +21 -95
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +26 -21
- src/cli/linters/structure_quality.py +28 -21
- 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 +95 -6
- 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 +58 -40
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/config.py +14 -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 +205 -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 +69 -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 -1
- src/linters/method_property/linter.py +5 -10
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/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/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- 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 +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.14.0.dist-info/METADATA +185 -0
- thailint-0.14.0.dist-info/RECORD +199 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,9 +10,9 @@ Overview: Handles building violation objects for function calls that consistentl
|
|
|
10
10
|
|
|
11
11
|
Dependencies: Violation, Severity, StoredFunctionCall, StringlyTypedConfig
|
|
12
12
|
|
|
13
|
-
Exports:
|
|
13
|
+
Exports: build_function_call_violations function
|
|
14
14
|
|
|
15
|
-
Interfaces:
|
|
15
|
+
Interfaces: build_function_call_violations(calls, unique_values) -> list[Violation]
|
|
16
16
|
|
|
17
17
|
Implementation: Builds violations with cross-file references and enum suggestions
|
|
18
18
|
"""
|
|
@@ -24,6 +24,21 @@ from src.core.types import Severity, Violation
|
|
|
24
24
|
from .storage import StoredFunctionCall
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def build_function_call_violations(
|
|
28
|
+
calls: list[StoredFunctionCall], unique_values: set[str]
|
|
29
|
+
) -> list[Violation]:
|
|
30
|
+
"""Build violations for all calls to a function with limited values.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
calls: All calls to the function/param
|
|
34
|
+
unique_values: Set of unique string values passed
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of violations for each call site
|
|
38
|
+
"""
|
|
39
|
+
return [_build_violation(call, calls, unique_values) for call in calls]
|
|
40
|
+
|
|
41
|
+
|
|
27
42
|
def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunctionCall]) -> str:
|
|
28
43
|
"""Build cross-reference string for other function call locations.
|
|
29
44
|
|
|
@@ -42,96 +57,79 @@ def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunc
|
|
|
42
57
|
return ", ".join(refs[:5]) # Limit to 5 references
|
|
43
58
|
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
unique_values
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return message
|
|
122
|
-
|
|
123
|
-
def _build_suggestion(self, call: StoredFunctionCall, unique_values: set[str]) -> str:
|
|
124
|
-
"""Build fix suggestion for function call pattern.
|
|
125
|
-
|
|
126
|
-
Args:
|
|
127
|
-
call: The function call
|
|
128
|
-
unique_values: Set of unique values passed
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
Human-readable suggestion
|
|
132
|
-
"""
|
|
133
|
-
return (
|
|
134
|
-
f"Consider defining an enum or type union with the "
|
|
135
|
-
f"{len(unique_values)} possible values for '{call.function_name}' "
|
|
136
|
-
f"parameter {call.param_index}."
|
|
137
|
-
)
|
|
60
|
+
def _build_violation(
|
|
61
|
+
call: StoredFunctionCall,
|
|
62
|
+
all_calls: list[StoredFunctionCall],
|
|
63
|
+
unique_values: set[str],
|
|
64
|
+
) -> Violation:
|
|
65
|
+
"""Build a single violation for a function call.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
call: The specific call to create violation for
|
|
69
|
+
all_calls: All calls to the same function/param
|
|
70
|
+
unique_values: Set of unique string values passed
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Violation instance
|
|
74
|
+
"""
|
|
75
|
+
message = _build_message(call, all_calls, unique_values)
|
|
76
|
+
suggestion = _build_suggestion(call, unique_values)
|
|
77
|
+
|
|
78
|
+
return Violation(
|
|
79
|
+
rule_id="stringly-typed.limited-values",
|
|
80
|
+
file_path=str(call.file_path),
|
|
81
|
+
line=call.line_number,
|
|
82
|
+
column=call.column,
|
|
83
|
+
message=message,
|
|
84
|
+
severity=Severity.ERROR,
|
|
85
|
+
suggestion=suggestion,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_message(
|
|
90
|
+
call: StoredFunctionCall,
|
|
91
|
+
all_calls: list[StoredFunctionCall],
|
|
92
|
+
unique_values: set[str],
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Build violation message for function call pattern.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
call: Current function call
|
|
98
|
+
all_calls: All calls to the same function/param
|
|
99
|
+
unique_values: Set of unique values passed
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Human-readable violation message
|
|
103
|
+
"""
|
|
104
|
+
file_count = len({c.file_path for c in all_calls})
|
|
105
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
|
|
106
|
+
param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
|
|
107
|
+
|
|
108
|
+
message = (
|
|
109
|
+
f"Function '{call.function_name}' {param_desc} is called with "
|
|
110
|
+
f"only {len(unique_values)} unique string values [{values_str}] "
|
|
111
|
+
f"across {file_count} file(s)."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
other_refs = _build_cross_references(call, all_calls)
|
|
115
|
+
if other_refs:
|
|
116
|
+
message += f" Also called in: {other_refs}."
|
|
117
|
+
|
|
118
|
+
return message
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_suggestion(call: StoredFunctionCall, unique_values: set[str]) -> str:
|
|
122
|
+
"""Build fix suggestion for function call pattern.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
call: The function call
|
|
126
|
+
unique_values: Set of unique values passed
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Human-readable suggestion
|
|
130
|
+
"""
|
|
131
|
+
return (
|
|
132
|
+
f"Consider defining an enum or type union with the "
|
|
133
|
+
f"{len(unique_values)} possible values for '{call.function_name}' "
|
|
134
|
+
f"parameter {call.param_index}."
|
|
135
|
+
)
|
|
@@ -20,6 +20,11 @@ Interfaces: StringlyTypedRule.check(context) -> list[Violation],
|
|
|
20
20
|
|
|
21
21
|
Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
|
|
22
22
|
Delegates all logic to helper classes, maintains only orchestration and state.
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
|
|
26
|
+
- srp: Rule class orchestrates cross-file detection with storage, analyzers, and generators.
|
|
27
|
+
Splitting would fragment the two-phase detection workflow.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
from __future__ import annotations
|
|
@@ -131,7 +136,7 @@ class StringlyTypedComponents:
|
|
|
131
136
|
typescript_analyzer: TypeScriptStringlyTypedAnalyzer
|
|
132
137
|
|
|
133
138
|
|
|
134
|
-
class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore
|
|
139
|
+
class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
135
140
|
"""Detects stringly-typed patterns across project files.
|
|
136
141
|
|
|
137
142
|
Uses two-phase pattern:
|
|
@@ -153,6 +158,19 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
153
158
|
typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
|
|
154
159
|
)
|
|
155
160
|
|
|
161
|
+
@property
|
|
162
|
+
def _active_storage(self) -> StringlyTypedStorage:
|
|
163
|
+
"""Get storage, asserting it has been initialized.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The initialized storage instance.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
AssertionError: If storage has not been initialized.
|
|
170
|
+
"""
|
|
171
|
+
assert self._storage is not None, "Storage not initialized" # nosec B101
|
|
172
|
+
return self._storage
|
|
173
|
+
|
|
156
174
|
@property
|
|
157
175
|
def rule_id(self) -> str:
|
|
158
176
|
"""Unique identifier for this rule."""
|
|
@@ -224,21 +242,31 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
224
242
|
"""
|
|
225
243
|
if not self._should_analyze(context, config):
|
|
226
244
|
return
|
|
245
|
+
# _should_analyze ensures file_path and file_content are set
|
|
246
|
+
assert context.file_path is not None # nosec B101
|
|
247
|
+
assert context.file_content is not None # nosec B101
|
|
227
248
|
|
|
228
|
-
file_path = Path(context.file_path) # type: ignore[arg-type]
|
|
229
|
-
file_content = context.file_content or ""
|
|
230
249
|
self._helpers.typescript_analyzer.config = config
|
|
231
|
-
|
|
232
|
-
# Single-parse optimization: parse once, run both trackers
|
|
233
250
|
call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
|
|
234
|
-
file_content, file_path
|
|
251
|
+
context.file_content, context.file_path
|
|
235
252
|
)
|
|
253
|
+
self._store_typescript_results(call_results, comparison_results)
|
|
236
254
|
|
|
237
|
-
|
|
238
|
-
self
|
|
255
|
+
def _store_typescript_results(
|
|
256
|
+
self,
|
|
257
|
+
call_results: list[FunctionCallResult],
|
|
258
|
+
comparison_results: list[ComparisonResult],
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Store TypeScript analysis results.
|
|
239
261
|
|
|
262
|
+
Args:
|
|
263
|
+
call_results: Function call patterns found
|
|
264
|
+
comparison_results: Comparison patterns found
|
|
265
|
+
"""
|
|
266
|
+
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
267
|
+
self._active_storage.add_function_calls(stored_calls)
|
|
240
268
|
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
241
|
-
self.
|
|
269
|
+
self._active_storage.add_comparisons(stored_comparisons)
|
|
242
270
|
|
|
243
271
|
def _ensure_storage_initialized(
|
|
244
272
|
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
@@ -263,9 +291,12 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
263
291
|
"""
|
|
264
292
|
if not self._should_analyze(context, config):
|
|
265
293
|
return
|
|
294
|
+
# _should_analyze ensures file_path and file_content are set
|
|
295
|
+
assert context.file_path is not None # nosec B101
|
|
296
|
+
assert context.file_content is not None # nosec B101
|
|
266
297
|
|
|
267
|
-
file_path =
|
|
268
|
-
file_content = context.file_content
|
|
298
|
+
file_path = context.file_path
|
|
299
|
+
file_content = context.file_content
|
|
269
300
|
self._helpers.python_analyzer.config = config
|
|
270
301
|
|
|
271
302
|
self._store_validation_patterns(file_content, file_path)
|
|
@@ -284,8 +315,9 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
284
315
|
"""
|
|
285
316
|
if not _is_ready_for_analysis(context, self._storage):
|
|
286
317
|
return False
|
|
287
|
-
|
|
288
|
-
|
|
318
|
+
# _is_ready_for_analysis ensures file_path is set
|
|
319
|
+
assert context.file_path is not None # nosec B101
|
|
320
|
+
return not is_ignored(context.file_path, config.ignore)
|
|
289
321
|
|
|
290
322
|
def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
|
|
291
323
|
"""Analyze and store validation patterns.
|
|
@@ -295,7 +327,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
295
327
|
file_path: Path to file
|
|
296
328
|
"""
|
|
297
329
|
results = self._helpers.python_analyzer.analyze(file_content, file_path)
|
|
298
|
-
self.
|
|
330
|
+
self._active_storage.add_patterns([_convert_to_stored_pattern(r) for r in results])
|
|
299
331
|
|
|
300
332
|
def _store_function_calls(self, file_content: str, file_path: Path) -> None:
|
|
301
333
|
"""Analyze and store function call patterns.
|
|
@@ -306,7 +338,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
306
338
|
"""
|
|
307
339
|
call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
|
|
308
340
|
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
309
|
-
self.
|
|
341
|
+
self._active_storage.add_function_calls(stored_calls)
|
|
310
342
|
|
|
311
343
|
def _store_comparisons(self, file_content: str, file_path: Path) -> None:
|
|
312
344
|
"""Analyze and store Python comparison patterns.
|
|
@@ -319,7 +351,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
|
|
|
319
351
|
file_content, file_path
|
|
320
352
|
)
|
|
321
353
|
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
322
|
-
self.
|
|
354
|
+
self._active_storage.add_comparisons(stored_comparisons)
|
|
323
355
|
|
|
324
356
|
def finalize(self) -> list[Violation]:
|
|
325
357
|
"""Generate violations after all files processed.
|
|
@@ -22,6 +22,10 @@ Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[Analysi
|
|
|
22
22
|
PythonStringlyTypedAnalyzer.analyze_function_calls(code, file_path) -> list[FunctionCallResult]
|
|
23
23
|
|
|
24
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.
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
31
|
import ast
|
|
@@ -120,7 +124,7 @@ class ComparisonResult:
|
|
|
120
124
|
"""Column number where the comparison starts (0-indexed)."""
|
|
121
125
|
|
|
122
126
|
|
|
123
|
-
class PythonStringlyTypedAnalyzer: # thailint: ignore
|
|
127
|
+
class PythonStringlyTypedAnalyzer: # thailint: ignore[srp]
|
|
124
128
|
"""Analyzes Python code for stringly-typed patterns.
|
|
125
129
|
|
|
126
130
|
Coordinates detection of various stringly-typed patterns including membership
|
|
@@ -10,18 +10,24 @@ Overview: Provides FunctionCallTracker class that traverses Python AST to find f
|
|
|
10
10
|
suggests the parameter should be an enum. Handles both simple function calls
|
|
11
11
|
(foo("value")) and method calls (obj.method("value")).
|
|
12
12
|
|
|
13
|
-
Dependencies: ast module for AST parsing, dataclasses for pattern structure
|
|
13
|
+
Dependencies: ast module for AST parsing, dataclasses for pattern structure,
|
|
14
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
14
15
|
|
|
15
16
|
Exports: FunctionCallTracker class, FunctionCallPattern dataclass
|
|
16
17
|
|
|
17
18
|
Interfaces: FunctionCallTracker.find_patterns(tree) -> list[FunctionCallPattern]
|
|
18
19
|
|
|
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
|
|
20
24
|
"""
|
|
21
25
|
|
|
22
26
|
import ast
|
|
23
27
|
from dataclasses import dataclass
|
|
24
28
|
|
|
29
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
@dataclass
|
|
27
33
|
class FunctionCallPattern:
|
|
@@ -119,12 +125,9 @@ class FunctionCallTracker(ast.NodeVisitor):
|
|
|
119
125
|
"""
|
|
120
126
|
parts: list[str] = [node.attr]
|
|
121
127
|
current = node.value
|
|
122
|
-
|
|
123
|
-
# Limit depth to avoid overly complex names
|
|
124
|
-
max_depth = 3
|
|
125
128
|
depth = 0
|
|
126
129
|
|
|
127
|
-
while depth <
|
|
130
|
+
while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
|
|
128
131
|
if isinstance(current, ast.Name):
|
|
129
132
|
parts.append(current.id)
|
|
130
133
|
break
|
|
@@ -9,18 +9,26 @@ Overview: Provides ComparisonTracker class that traverses Python AST to find sca
|
|
|
9
9
|
to multiple unique string values across files, it suggests the variable should be an enum.
|
|
10
10
|
Excludes common false positives like `__name__ == "__main__"` and type name checks.
|
|
11
11
|
|
|
12
|
-
Dependencies: ast module for AST parsing, dataclasses for pattern structure
|
|
12
|
+
Dependencies: ast module for AST parsing, dataclasses for pattern structure,
|
|
13
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
13
14
|
|
|
14
15
|
Exports: ComparisonTracker class, ComparisonPattern dataclass
|
|
15
16
|
|
|
16
17
|
Interfaces: ComparisonTracker.find_patterns(tree) -> list[ComparisonPattern]
|
|
17
18
|
|
|
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.
|
|
19
25
|
"""
|
|
20
26
|
|
|
21
27
|
import ast
|
|
22
28
|
from dataclasses import dataclass
|
|
23
29
|
|
|
30
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
31
|
+
|
|
24
32
|
|
|
25
33
|
@dataclass
|
|
26
34
|
class ComparisonPattern:
|
|
@@ -188,12 +196,9 @@ class ComparisonTracker(ast.NodeVisitor): # thailint: ignore[srp]
|
|
|
188
196
|
"""
|
|
189
197
|
parts: list[str] = [node.attr]
|
|
190
198
|
current = node.value
|
|
191
|
-
|
|
192
|
-
# Limit depth to avoid overly complex names
|
|
193
|
-
max_depth = 3
|
|
194
199
|
depth = 0
|
|
195
200
|
|
|
196
|
-
while depth <
|
|
201
|
+
while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
|
|
197
202
|
if isinstance(current, ast.Name):
|
|
198
203
|
parts.append(current.id)
|
|
199
204
|
break
|
|
@@ -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
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
# pylint: disable=too-many-lines
|
|
2
|
-
# Justification: Storage module for three related data types (patterns, function calls,
|
|
3
|
-
# comparisons) with their dataclasses, conversion functions, SQL schemas, and CRUD methods.
|
|
4
|
-
# Splitting into separate files would fragment cohesive SQLite storage logic.
|
|
5
2
|
"""
|
|
6
3
|
Purpose: SQLite storage manager for stringly-typed pattern detection
|
|
7
4
|
|
|
@@ -34,6 +31,13 @@ Interfaces: StringlyTypedStorage.__init__(storage_mode), add_pattern(pattern),
|
|
|
34
31
|
|
|
35
32
|
Implementation: SQLite with string_validations, function_calls, and string_comparisons tables,
|
|
36
33
|
indexed on string_set_hash, function_name+param_index, and variable_name for performance
|
|
34
|
+
|
|
35
|
+
Suppressions:
|
|
36
|
+
- too-many-lines: Storage module for three related data types with dataclasses, SQL schemas, and CRUD methods
|
|
37
|
+
- too-many-instance-attributes: StoredPattern is a pure DTO with 8 necessary fields for SQLite storage
|
|
38
|
+
- consider-using-with: NamedTemporaryFile must remain open for SQLite connection lifetime (closed in close())
|
|
39
|
+
- srp: Storage class manages SQLite for three pattern types (validations, calls, comparisons).
|
|
40
|
+
Splitting would fragment related storage operations.
|
|
37
41
|
"""
|
|
38
42
|
|
|
39
43
|
from __future__ import annotations
|
|
@@ -44,6 +48,8 @@ import tempfile
|
|
|
44
48
|
from dataclasses import dataclass
|
|
45
49
|
from pathlib import Path
|
|
46
50
|
|
|
51
|
+
from src.core.constants import StorageMode
|
|
52
|
+
|
|
47
53
|
# Row index constants for SQLite query results
|
|
48
54
|
_COL_FILE_PATH = 0
|
|
49
55
|
_COL_LINE_NUMBER = 1
|
|
@@ -244,11 +250,8 @@ def _row_to_function_call(row: tuple) -> StoredFunctionCall:
|
|
|
244
250
|
)
|
|
245
251
|
|
|
246
252
|
|
|
247
|
-
# pylint: disable=too-many-instance-attributes
|
|
248
|
-
# Justification: StoredPattern is a pure data transfer object for SQLite storage.
|
|
249
|
-
# All 8 fields are necessary: file location (3), variable info (1), hash/values (3), pattern type (1).
|
|
250
253
|
@dataclass
|
|
251
|
-
class StoredPattern:
|
|
254
|
+
class StoredPattern: # pylint: disable=too-many-instance-attributes
|
|
252
255
|
"""Represents a stringly-typed pattern stored in SQLite.
|
|
253
256
|
|
|
254
257
|
Captures all information needed to detect cross-file duplicates and generate
|
|
@@ -280,7 +283,7 @@ class StoredPattern:
|
|
|
280
283
|
"""Human-readable description of the detected pattern."""
|
|
281
284
|
|
|
282
285
|
|
|
283
|
-
class StringlyTypedStorage: # thailint: ignore
|
|
286
|
+
class StringlyTypedStorage: # thailint: ignore[srp]
|
|
284
287
|
"""SQLite-backed storage for stringly-typed pattern detection.
|
|
285
288
|
|
|
286
289
|
Stores patterns from analyzed files and provides queries to find patterns
|
|
@@ -297,13 +300,10 @@ class StringlyTypedStorage: # thailint: ignore srp
|
|
|
297
300
|
self._tempfile = None
|
|
298
301
|
|
|
299
302
|
# Create SQLite connection based on storage mode
|
|
300
|
-
if storage_mode ==
|
|
303
|
+
if storage_mode == StorageMode.MEMORY:
|
|
301
304
|
self._db = sqlite3.connect(":memory:")
|
|
302
|
-
elif storage_mode ==
|
|
303
|
-
# pylint: disable=consider-using-with
|
|
304
|
-
# Justification: tempfile must remain open for SQLite connection lifetime.
|
|
305
|
-
# It is explicitly closed in close() method when storage is finalized.
|
|
306
|
-
self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True)
|
|
305
|
+
elif storage_mode == StorageMode.TEMPFILE:
|
|
306
|
+
self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True) # pylint: disable=consider-using-with
|
|
307
307
|
self._db = sqlite3.connect(self._tempfile.name)
|
|
308
308
|
else:
|
|
309
309
|
raise ValueError(f"Invalid storage_mode: {storage_mode}")
|
|
@@ -11,19 +11,26 @@ Overview: Provides TypeScriptCallTracker class that uses tree-sitter to traverse
|
|
|
11
11
|
function calls (foo("value")) and method calls (obj.method("value")), including
|
|
12
12
|
chained method calls.
|
|
13
13
|
|
|
14
|
-
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure
|
|
14
|
+
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
|
|
15
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
15
16
|
|
|
16
17
|
Exports: TypeScriptCallTracker class, TypeScriptFunctionCallPattern dataclass
|
|
17
18
|
|
|
18
19
|
Interfaces: TypeScriptCallTracker.find_patterns(code) -> list[TypeScriptFunctionCallPattern]
|
|
19
20
|
|
|
20
21
|
Implementation: Tree-sitter node traversal with call_expression node handling for string arguments
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
25
|
+
- srp: Tracker implements tree-sitter traversal with helper methods for call extraction.
|
|
26
|
+
Methods support single responsibility of function call pattern detection.
|
|
21
27
|
"""
|
|
22
28
|
|
|
23
29
|
from dataclasses import dataclass
|
|
24
30
|
from typing import Any
|
|
25
31
|
|
|
26
32
|
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
|
|
33
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
27
34
|
|
|
28
35
|
if TREE_SITTER_AVAILABLE:
|
|
29
36
|
from tree_sitter import Node
|
|
@@ -180,10 +187,9 @@ class TypeScriptCallTracker(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
|
180
187
|
Qualified function name or None if too complex
|
|
181
188
|
"""
|
|
182
189
|
parts: list[str] = []
|
|
183
|
-
max_depth = 3
|
|
184
190
|
current: Node | None = node
|
|
185
191
|
|
|
186
|
-
for _ in range(
|
|
192
|
+
for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
|
|
187
193
|
if current is None:
|
|
188
194
|
break
|
|
189
195
|
self._add_property_name(current, parts)
|
|
@@ -10,7 +10,8 @@ Overview: Provides TypeScriptComparisonTracker class that uses tree-sitter to tr
|
|
|
10
10
|
it suggests the variable should be an enum. Excludes common false positives like template
|
|
11
11
|
literals and typeof comparisons.
|
|
12
12
|
|
|
13
|
-
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure
|
|
13
|
+
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
|
|
14
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
14
15
|
|
|
15
16
|
Exports: TypeScriptComparisonTracker class, TypeScriptComparisonPattern dataclass
|
|
16
17
|
|
|
@@ -18,12 +19,18 @@ Interfaces: TypeScriptComparisonTracker.find_patterns(code) -> list[TypeScriptCo
|
|
|
18
19
|
|
|
19
20
|
Implementation: Tree-sitter node traversal with binary_expression node handling for string
|
|
20
21
|
comparisons
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
25
|
+
- srp: Tracker implements tree-sitter traversal with helper methods for node extraction.
|
|
26
|
+
Methods support single responsibility of comparison pattern detection.
|
|
21
27
|
"""
|
|
22
28
|
|
|
23
29
|
from dataclasses import dataclass
|
|
24
30
|
from typing import Any
|
|
25
31
|
|
|
26
32
|
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
|
|
33
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
27
34
|
|
|
28
35
|
if TREE_SITTER_AVAILABLE:
|
|
29
36
|
from tree_sitter import Node
|
|
@@ -277,10 +284,9 @@ class TypeScriptComparisonTracker(TypeScriptBaseAnalyzer): # thailint: ignore[s
|
|
|
277
284
|
Qualified name or None if too complex
|
|
278
285
|
"""
|
|
279
286
|
parts: list[str] = []
|
|
280
|
-
max_depth = 3
|
|
281
287
|
current: Node | None = node
|
|
282
288
|
|
|
283
|
-
for _ in range(
|
|
289
|
+
for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
|
|
284
290
|
if current is None:
|
|
285
291
|
break
|
|
286
292
|
self._add_property_name(current, parts)
|