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.
Files changed (214) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {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
+ )