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,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analyzer for detecting stateless classes
|
|
3
|
+
|
|
4
|
+
Scope: AST-based analysis of Python class definitions for stateless patterns
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Python source code using AST to detect classes that have no
|
|
7
|
+
constructor (__init__ or __new__), no instance state (self.attr assignments),
|
|
8
|
+
and 2+ methods - indicating they should be refactored to module-level functions.
|
|
9
|
+
Excludes legitimate patterns like ABC, Protocol, decorated classes, and classes
|
|
10
|
+
with class-level attributes.
|
|
11
|
+
|
|
12
|
+
Dependencies: Python AST module
|
|
13
|
+
|
|
14
|
+
Exports: analyze_code function, ClassInfo dataclass
|
|
15
|
+
|
|
16
|
+
Interfaces: analyze_code(code) -> list[ClassInfo] returning detected stateless classes
|
|
17
|
+
|
|
18
|
+
Implementation: AST visitor pattern with focused helper functions for different checks
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ClassInfo:
|
|
27
|
+
"""Information about a detected stateless class."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
line: int
|
|
31
|
+
column: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def analyze_code(code: str, min_methods: int = 2) -> list[ClassInfo]:
|
|
35
|
+
"""Analyze Python code for stateless classes.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
code: Python source code
|
|
39
|
+
min_methods: Minimum methods required to flag class
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of detected stateless class info
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
tree = ast.parse(code)
|
|
46
|
+
except SyntaxError:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
return _find_stateless_classes(tree, min_methods)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_stateless_classes(tree: ast.Module, min_methods: int = 2) -> list[ClassInfo]:
|
|
53
|
+
"""Find all stateless classes in AST.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tree: Parsed AST module
|
|
57
|
+
min_methods: Minimum methods required to flag class
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of stateless class info
|
|
61
|
+
"""
|
|
62
|
+
results = []
|
|
63
|
+
for node in ast.walk(tree):
|
|
64
|
+
if isinstance(node, ast.ClassDef) and _is_stateless(node, min_methods):
|
|
65
|
+
results.append(ClassInfo(node.name, node.lineno, node.col_offset))
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_stateless(class_node: ast.ClassDef, min_methods: int = 2) -> bool:
|
|
70
|
+
"""Check if class is stateless and should be functions.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
class_node: AST ClassDef node
|
|
74
|
+
min_methods: Minimum methods required to flag class
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if class is stateless violation
|
|
78
|
+
"""
|
|
79
|
+
if _should_skip_class(class_node):
|
|
80
|
+
return False
|
|
81
|
+
return _count_methods(class_node) >= min_methods
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _should_skip_class(class_node: ast.ClassDef) -> bool:
|
|
85
|
+
"""Check if class should be skipped from analysis.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
class_node: AST ClassDef node
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if class should be skipped
|
|
92
|
+
"""
|
|
93
|
+
return (
|
|
94
|
+
_has_constructor(class_node)
|
|
95
|
+
or _is_exception_case(class_node)
|
|
96
|
+
or _has_class_attributes(class_node)
|
|
97
|
+
or _has_instance_attributes(class_node)
|
|
98
|
+
or _has_base_classes(class_node)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _has_base_classes(class_node: ast.ClassDef) -> bool:
|
|
103
|
+
"""Check if class inherits from non-trivial base classes.
|
|
104
|
+
|
|
105
|
+
Classes that inherit from other classes are using polymorphism/inheritance
|
|
106
|
+
and should not be flagged as stateless.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
class_node: AST ClassDef node
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if class has non-trivial base classes
|
|
113
|
+
"""
|
|
114
|
+
if not class_node.bases:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
for base in class_node.bases:
|
|
118
|
+
base_name = _get_base_name(base)
|
|
119
|
+
# Skip trivial bases like object
|
|
120
|
+
if base_name and base_name not in ("object",):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _count_methods(class_node: ast.ClassDef) -> int:
|
|
127
|
+
"""Count methods in class.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
class_node: AST ClassDef node
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Number of methods
|
|
134
|
+
"""
|
|
135
|
+
return sum(1 for item in class_node.body if isinstance(item, ast.FunctionDef))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _has_constructor(class_node: ast.ClassDef) -> bool:
|
|
139
|
+
"""Check if class has __init__ or __new__ method.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
class_node: AST ClassDef node
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if class has constructor
|
|
146
|
+
"""
|
|
147
|
+
constructor_names = ("__init__", "__new__")
|
|
148
|
+
return any(
|
|
149
|
+
isinstance(item, ast.FunctionDef) and item.name in constructor_names
|
|
150
|
+
for item in class_node.body
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _is_exception_case(class_node: ast.ClassDef) -> bool:
|
|
155
|
+
"""Check if class is an exception case (ABC, Protocol, or decorated).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
class_node: AST ClassDef node
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if class is ABC, Protocol, or decorated
|
|
162
|
+
"""
|
|
163
|
+
if class_node.decorator_list:
|
|
164
|
+
return True
|
|
165
|
+
return _inherits_from_abc_or_protocol(class_node)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
|
|
169
|
+
"""Check if class inherits from ABC or Protocol.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
class_node: AST ClassDef node
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if inherits from ABC or Protocol
|
|
176
|
+
"""
|
|
177
|
+
return any(_get_base_name(base) in ("ABC", "Protocol") for base in class_node.bases)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _get_base_name(base: ast.expr) -> str:
|
|
181
|
+
"""Extract name from base class expression.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
base: AST expression for base class
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Base class name or empty string
|
|
188
|
+
"""
|
|
189
|
+
if isinstance(base, ast.Name):
|
|
190
|
+
return base.id
|
|
191
|
+
if isinstance(base, ast.Attribute):
|
|
192
|
+
return base.attr
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _has_class_attributes(class_node: ast.ClassDef) -> bool:
|
|
197
|
+
"""Check if class has class-level attributes.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
class_node: AST ClassDef node
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if class has class attributes
|
|
204
|
+
"""
|
|
205
|
+
return any(isinstance(item, (ast.Assign, ast.AnnAssign)) for item in class_node.body)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
209
|
+
"""Check if methods assign to self.attr.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
class_node: AST ClassDef node
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if any method assigns to self
|
|
216
|
+
"""
|
|
217
|
+
return any(
|
|
218
|
+
isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item)
|
|
219
|
+
for item in class_node.body
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
224
|
+
"""Check if method assigns to self.attr.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
method: AST FunctionDef node
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if method assigns to self
|
|
231
|
+
"""
|
|
232
|
+
return any(_is_self_attribute_assignment(node) for node in ast.walk(method))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _is_self_attribute_assignment(node: ast.AST) -> bool:
|
|
236
|
+
"""Check if node is a self.attr assignment.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
node: AST node to check
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
True if node is self attribute assignment
|
|
243
|
+
"""
|
|
244
|
+
if not isinstance(node, ast.Assign):
|
|
245
|
+
return False
|
|
246
|
+
return any(_is_self_attribute(t) for t in node.targets)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_self_attribute(node: ast.expr) -> bool:
|
|
250
|
+
"""Check if node is a self.attr reference.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
node: AST expression node
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if node is self.attr
|
|
257
|
+
"""
|
|
258
|
+
if not isinstance(node, ast.Attribute):
|
|
259
|
+
return False
|
|
260
|
+
if not isinstance(node.value, ast.Name):
|
|
261
|
+
return False
|
|
262
|
+
return node.value.id == "self"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Legacy class wrapper for backward compatibility with linter.py
|
|
266
|
+
class StatelessClassAnalyzer:
|
|
267
|
+
"""Analyzes Python code for stateless classes.
|
|
268
|
+
|
|
269
|
+
Note: This class is a thin wrapper around module-level functions
|
|
270
|
+
to maintain backward compatibility with existing code.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, min_methods: int = 2) -> None:
|
|
274
|
+
"""Initialize the analyzer.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
min_methods: Minimum methods required to flag class
|
|
278
|
+
"""
|
|
279
|
+
self._min_methods = min_methods
|
|
280
|
+
|
|
281
|
+
def analyze(self, code: str) -> list[ClassInfo]:
|
|
282
|
+
"""Analyze Python code for stateless classes.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
code: Python source code
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
List of detected stateless class info
|
|
289
|
+
"""
|
|
290
|
+
return analyze_code(code, self._min_methods)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Stringly-typed linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Public API for stringly-typed linter module
|
|
5
|
+
|
|
6
|
+
Overview: Provides the public interface for the stringly-typed linter package. Exports
|
|
7
|
+
StringlyTypedConfig for configuration and StringlyTypedRule for linting. The stringly-typed
|
|
8
|
+
linter detects code patterns where plain strings are used instead of proper enums or typed
|
|
9
|
+
alternatives, helping identify potential type safety improvements. Supports cross-file
|
|
10
|
+
detection to find repeated string patterns across the codebase. Includes IgnoreChecker
|
|
11
|
+
for inline ignore directive support.
|
|
12
|
+
|
|
13
|
+
Dependencies: .config for StringlyTypedConfig, .linter for StringlyTypedRule,
|
|
14
|
+
.storage for StringlyTypedStorage, .ignore_checker for IgnoreChecker
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedConfig, StringlyTypedRule, StringlyTypedStorage, StoredPattern,
|
|
17
|
+
IgnoreChecker
|
|
18
|
+
|
|
19
|
+
Interfaces: Configuration loading via StringlyTypedConfig.from_dict(),
|
|
20
|
+
StringlyTypedRule.check() and finalize() for linting, IgnoreChecker.filter_violations()
|
|
21
|
+
|
|
22
|
+
Implementation: Module-level exports with __all__ definition
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from src.linters.stringly_typed.config import StringlyTypedConfig
|
|
26
|
+
from src.linters.stringly_typed.ignore_checker import IgnoreChecker
|
|
27
|
+
from src.linters.stringly_typed.linter import StringlyTypedRule
|
|
28
|
+
from src.linters.stringly_typed.storage import StoredPattern, StringlyTypedStorage
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"StringlyTypedConfig",
|
|
32
|
+
"IgnoreChecker",
|
|
33
|
+
"StringlyTypedRule",
|
|
34
|
+
"StringlyTypedStorage",
|
|
35
|
+
"StoredPattern",
|
|
36
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for stringly-typed linter
|
|
3
|
+
|
|
4
|
+
Scope: Define configurable options for stringly-typed pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides StringlyTypedConfig for customizing linter behavior including minimum
|
|
7
|
+
occurrences required to flag patterns, enum value thresholds, cross-file detection
|
|
8
|
+
settings, and ignore patterns. The stringly-typed linter detects code patterns where
|
|
9
|
+
plain strings are used instead of proper enums or typed alternatives. Integrates with
|
|
10
|
+
the orchestrator's configuration system to allow users to customize detection via
|
|
11
|
+
.thailint.yaml configuration files. Follows the same configuration pattern as other
|
|
12
|
+
thai-lint linters.
|
|
13
|
+
|
|
14
|
+
Dependencies: dataclasses, typing
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedConfig dataclass, default constants
|
|
17
|
+
|
|
18
|
+
Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
|
|
19
|
+
|
|
20
|
+
Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
|
|
21
|
+
loading from dictionary with language-specific override support
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-instance-attributes: Configuration dataclass with cohesive detection settings
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
# Default thresholds
|
|
31
|
+
DEFAULT_MIN_OCCURRENCES = 2
|
|
32
|
+
DEFAULT_MIN_VALUES_FOR_ENUM = 2
|
|
33
|
+
DEFAULT_MAX_VALUES_FOR_ENUM = 6
|
|
34
|
+
|
|
35
|
+
# Default ignore patterns - test directories are excluded by default
|
|
36
|
+
# because test fixtures commonly use string literals for mocking
|
|
37
|
+
DEFAULT_IGNORE_PATTERNS: list[str] = [
|
|
38
|
+
"**/tests/**",
|
|
39
|
+
"**/test/**",
|
|
40
|
+
"**/*_test.py",
|
|
41
|
+
"**/*_test.ts",
|
|
42
|
+
"**/*.test.ts",
|
|
43
|
+
"**/*.test.tsx",
|
|
44
|
+
"**/*.spec.ts",
|
|
45
|
+
"**/*.spec.tsx",
|
|
46
|
+
"**/*.stories.ts",
|
|
47
|
+
"**/*.stories.tsx",
|
|
48
|
+
"**/conftest.py",
|
|
49
|
+
"**/fixtures/**",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
|
|
55
|
+
"""Configuration for stringly-typed linter.
|
|
56
|
+
|
|
57
|
+
Note: Pylint too-many-instance-attributes disabled. This is a configuration
|
|
58
|
+
dataclass serving as a data container for related stringly-typed linter settings.
|
|
59
|
+
All 8 attributes are cohesively related (detection thresholds, filtering options,
|
|
60
|
+
cross-file settings, exclusion patterns). Splitting would reduce cohesion and make
|
|
61
|
+
configuration loading more complex without meaningful benefit. This follows the
|
|
62
|
+
established pattern in DRYConfig.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
enabled: bool = True
|
|
66
|
+
"""Whether the linter is enabled."""
|
|
67
|
+
|
|
68
|
+
min_occurrences: int = DEFAULT_MIN_OCCURRENCES
|
|
69
|
+
"""Minimum number of cross-file occurrences required to flag a violation."""
|
|
70
|
+
|
|
71
|
+
min_values_for_enum: int = DEFAULT_MIN_VALUES_FOR_ENUM
|
|
72
|
+
"""Minimum number of unique string values to suggest an enum."""
|
|
73
|
+
|
|
74
|
+
max_values_for_enum: int = DEFAULT_MAX_VALUES_FOR_ENUM
|
|
75
|
+
"""Maximum number of unique string values to suggest an enum (above this, not enum-worthy)."""
|
|
76
|
+
|
|
77
|
+
require_cross_file: bool = True
|
|
78
|
+
"""Whether to require cross-file occurrences to flag violations."""
|
|
79
|
+
|
|
80
|
+
ignore: list[str] = field(default_factory=list)
|
|
81
|
+
"""File patterns to ignore. Defaults merged with test directories in from_dict."""
|
|
82
|
+
|
|
83
|
+
allowed_string_sets: list[list[str]] = field(default_factory=list)
|
|
84
|
+
"""String sets that are allowed and should not be flagged."""
|
|
85
|
+
|
|
86
|
+
exclude_variables: list[str] = field(default_factory=list)
|
|
87
|
+
"""Variable names to exclude from detection."""
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
"""Validate configuration values."""
|
|
91
|
+
if self.min_occurrences < 1:
|
|
92
|
+
raise ValueError(f"min_occurrences must be at least 1, got {self.min_occurrences}")
|
|
93
|
+
if self.min_values_for_enum < 2:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"min_values_for_enum must be at least 2, got {self.min_values_for_enum}"
|
|
96
|
+
)
|
|
97
|
+
if self.max_values_for_enum < self.min_values_for_enum:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"max_values_for_enum ({self.max_values_for_enum}) must be >= "
|
|
100
|
+
f"min_values_for_enum ({self.min_values_for_enum})"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(
|
|
105
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
106
|
+
) -> "StringlyTypedConfig":
|
|
107
|
+
"""Load configuration from dictionary.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config: Dictionary containing configuration values
|
|
111
|
+
language: Programming language for language-specific overrides
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
StringlyTypedConfig instance with values from dictionary
|
|
115
|
+
"""
|
|
116
|
+
# Check for language-specific overrides first
|
|
117
|
+
if language and language in config:
|
|
118
|
+
lang_config = config[language]
|
|
119
|
+
return cls._from_merged_config(config, lang_config)
|
|
120
|
+
|
|
121
|
+
return cls._from_base_config(config)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _from_base_config(cls, config: dict[str, Any]) -> "StringlyTypedConfig":
|
|
125
|
+
"""Create config from base configuration dictionary.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
config: Base configuration dictionary
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
StringlyTypedConfig instance
|
|
132
|
+
"""
|
|
133
|
+
# Merge user ignore patterns with defaults
|
|
134
|
+
user_ignore = config.get("ignore", [])
|
|
135
|
+
merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
|
|
136
|
+
|
|
137
|
+
return cls(
|
|
138
|
+
enabled=config.get("enabled", True),
|
|
139
|
+
min_occurrences=config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
|
|
140
|
+
min_values_for_enum=config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
|
|
141
|
+
max_values_for_enum=config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
|
|
142
|
+
require_cross_file=config.get("require_cross_file", True),
|
|
143
|
+
ignore=merged_ignore,
|
|
144
|
+
allowed_string_sets=config.get("allowed_string_sets", []),
|
|
145
|
+
exclude_variables=config.get("exclude_variables", []),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _from_merged_config(
|
|
150
|
+
cls, base_config: dict[str, Any], lang_config: dict[str, Any]
|
|
151
|
+
) -> "StringlyTypedConfig":
|
|
152
|
+
"""Create config with language-specific overrides merged.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
base_config: Base configuration dictionary
|
|
156
|
+
lang_config: Language-specific configuration overrides
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
StringlyTypedConfig instance with merged values
|
|
160
|
+
"""
|
|
161
|
+
# Merge user ignore patterns with defaults
|
|
162
|
+
user_ignore = lang_config.get("ignore", base_config.get("ignore", []))
|
|
163
|
+
merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
|
|
164
|
+
|
|
165
|
+
return cls(
|
|
166
|
+
enabled=lang_config.get("enabled", base_config.get("enabled", True)),
|
|
167
|
+
min_occurrences=lang_config.get(
|
|
168
|
+
"min_occurrences",
|
|
169
|
+
base_config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
|
|
170
|
+
),
|
|
171
|
+
min_values_for_enum=lang_config.get(
|
|
172
|
+
"min_values_for_enum",
|
|
173
|
+
base_config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
|
|
174
|
+
),
|
|
175
|
+
max_values_for_enum=lang_config.get(
|
|
176
|
+
"max_values_for_enum",
|
|
177
|
+
base_config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
|
|
178
|
+
),
|
|
179
|
+
require_cross_file=lang_config.get(
|
|
180
|
+
"require_cross_file", base_config.get("require_cross_file", True)
|
|
181
|
+
),
|
|
182
|
+
ignore=merged_ignore,
|
|
183
|
+
allowed_string_sets=lang_config.get(
|
|
184
|
+
"allowed_string_sets", base_config.get("allowed_string_sets", [])
|
|
185
|
+
),
|
|
186
|
+
exclude_variables=lang_config.get(
|
|
187
|
+
"exclude_variables", base_config.get("exclude_variables", [])
|
|
188
|
+
),
|
|
189
|
+
)
|