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,215 @@
1
+ """
2
+ Purpose: Coordinate LBYL pattern detection and violation generation
3
+
4
+ Scope: Orchestrates multiple pattern detectors and converts results to violations
5
+
6
+ Overview: Provides PythonLBYLAnalyzer class that coordinates all LBYL pattern detectors
7
+ and converts detected patterns into Violation objects. Handles AST parsing with
8
+ graceful syntax error handling, runs enabled detectors based on configuration toggles,
9
+ and aggregates violations from all detectors. Serves as the main analysis engine
10
+ called by LBYLRule.
11
+
12
+ Dependencies: ast module, LBYLConfig, pattern detectors, violation_builder functions
13
+
14
+ Exports: PythonLBYLAnalyzer
15
+
16
+ Interfaces: analyze(code: str, file_path: str, config: LBYLConfig) -> list[Violation]
17
+
18
+ Implementation: Detector coordination with config-driven pattern selection using
19
+ generic detector runner for code reuse
20
+ """
21
+
22
+ import ast
23
+ from collections.abc import Callable
24
+ from typing import Any, TypeVar
25
+
26
+ from src.core.types import Violation
27
+
28
+ from .config import LBYLConfig
29
+ from .pattern_detectors.base import BaseLBYLDetector, LBYLPattern
30
+ from .pattern_detectors.dict_key_detector import DictKeyDetector, DictKeyPattern
31
+ from .pattern_detectors.division_check_detector import (
32
+ DivisionCheckDetector,
33
+ DivisionCheckPattern,
34
+ )
35
+ from .pattern_detectors.file_exists_detector import FileExistsDetector, FileExistsPattern
36
+ from .pattern_detectors.hasattr_detector import HasattrDetector, HasattrPattern
37
+ from .pattern_detectors.isinstance_detector import IsinstanceDetector, IsinstancePattern
38
+ from .pattern_detectors.len_check_detector import LenCheckDetector, LenCheckPattern
39
+ from .pattern_detectors.none_check_detector import NoneCheckDetector, NoneCheckPattern
40
+ from .pattern_detectors.string_validator_detector import (
41
+ StringValidatorDetector,
42
+ StringValidatorPattern,
43
+ )
44
+ from .violation_builder import (
45
+ build_dict_key_violation,
46
+ build_division_check_violation,
47
+ build_file_exists_violation,
48
+ build_hasattr_violation,
49
+ build_isinstance_violation,
50
+ build_len_check_violation,
51
+ build_none_check_violation,
52
+ build_string_validator_violation,
53
+ )
54
+
55
+ PatternT = TypeVar("PatternT", bound=LBYLPattern)
56
+
57
+
58
+ def _parse_python_code(code: str) -> ast.Module | None:
59
+ """Parse Python code into AST, returning None if empty or invalid."""
60
+ if not code or not code.strip():
61
+ return None
62
+ try:
63
+ return ast.parse(code)
64
+ except SyntaxError:
65
+ return None
66
+
67
+
68
+ def _run_detector(
69
+ detector: BaseLBYLDetector[PatternT],
70
+ tree: ast.Module,
71
+ file_path: str,
72
+ converter: Callable[[PatternT, str], Violation],
73
+ pattern_type: type[PatternT],
74
+ ) -> list[Violation]:
75
+ """Run a detector and convert patterns to violations."""
76
+ return [
77
+ converter(p, file_path) for p in detector.find_patterns(tree) if isinstance(p, pattern_type)
78
+ ]
79
+
80
+
81
+ def _build_dict_key(pattern: DictKeyPattern, file_path: str) -> Violation:
82
+ """Convert DictKeyPattern to Violation."""
83
+ return build_dict_key_violation(
84
+ file_path=file_path,
85
+ line=pattern.line_number,
86
+ column=pattern.column,
87
+ dict_name=pattern.dict_name,
88
+ key_expression=pattern.key_expression,
89
+ )
90
+
91
+
92
+ def _build_division_check(pattern: DivisionCheckPattern, file_path: str) -> Violation:
93
+ """Convert DivisionCheckPattern to Violation."""
94
+ return build_division_check_violation(
95
+ file_path=file_path,
96
+ line=pattern.line_number,
97
+ column=pattern.column,
98
+ divisor_name=pattern.divisor_name,
99
+ operation=pattern.operation,
100
+ )
101
+
102
+
103
+ def _build_file_exists(pattern: FileExistsPattern, file_path: str) -> Violation:
104
+ """Convert FileExistsPattern to Violation."""
105
+ return build_file_exists_violation(
106
+ file_path=file_path,
107
+ line=pattern.line_number,
108
+ column=pattern.column,
109
+ path_expression=pattern.file_path_expression,
110
+ check_type=pattern.check_type,
111
+ )
112
+
113
+
114
+ def _build_hasattr(pattern: HasattrPattern, file_path: str) -> Violation:
115
+ """Convert HasattrPattern to Violation."""
116
+ return build_hasattr_violation(
117
+ file_path=file_path,
118
+ line=pattern.line_number,
119
+ column=pattern.column,
120
+ object_name=pattern.object_name,
121
+ attribute_name=pattern.attribute_name,
122
+ )
123
+
124
+
125
+ def _build_isinstance(pattern: IsinstancePattern, file_path: str) -> Violation:
126
+ """Convert IsinstancePattern to Violation."""
127
+ return build_isinstance_violation(
128
+ file_path=file_path,
129
+ line=pattern.line_number,
130
+ column=pattern.column,
131
+ object_name=pattern.object_name,
132
+ type_name=pattern.type_name,
133
+ )
134
+
135
+
136
+ def _build_len_check(pattern: LenCheckPattern, file_path: str) -> Violation:
137
+ """Convert LenCheckPattern to Violation."""
138
+ return build_len_check_violation(
139
+ file_path=file_path,
140
+ line=pattern.line_number,
141
+ column=pattern.column,
142
+ collection_name=pattern.collection_name,
143
+ index_expression=pattern.index_expression,
144
+ )
145
+
146
+
147
+ def _build_none_check(pattern: NoneCheckPattern, file_path: str) -> Violation:
148
+ """Convert NoneCheckPattern to Violation."""
149
+ return build_none_check_violation(
150
+ file_path=file_path,
151
+ line=pattern.line_number,
152
+ column=pattern.column,
153
+ variable_name=pattern.variable_name,
154
+ )
155
+
156
+
157
+ def _build_string_validator(pattern: StringValidatorPattern, file_path: str) -> Violation:
158
+ """Convert StringValidatorPattern to Violation."""
159
+ return build_string_validator_violation(
160
+ file_path=file_path,
161
+ line=pattern.line_number,
162
+ column=pattern.column,
163
+ string_name=pattern.string_name,
164
+ validator_method=pattern.validator_method,
165
+ conversion_func=pattern.conversion_func,
166
+ )
167
+
168
+
169
+ class PythonLBYLAnalyzer:
170
+ """Coordinates LBYL pattern detection for Python code."""
171
+
172
+ def __init__(self) -> None:
173
+ """Initialize the analyzer with pattern detectors."""
174
+ # Each tuple: (detector, converter, pattern_type)
175
+ self._detector_configs: list[
176
+ tuple[BaseLBYLDetector[Any], Callable[..., Violation], type]
177
+ ] = [
178
+ (DictKeyDetector(), _build_dict_key, DictKeyPattern),
179
+ (DivisionCheckDetector(), _build_division_check, DivisionCheckPattern),
180
+ (FileExistsDetector(), _build_file_exists, FileExistsPattern),
181
+ (HasattrDetector(), _build_hasattr, HasattrPattern),
182
+ (IsinstanceDetector(), _build_isinstance, IsinstancePattern),
183
+ (LenCheckDetector(), _build_len_check, LenCheckPattern),
184
+ (NoneCheckDetector(), _build_none_check, NoneCheckPattern),
185
+ (StringValidatorDetector(), _build_string_validator, StringValidatorPattern),
186
+ ]
187
+
188
+ def analyze(self, code: str, file_path: str, config: LBYLConfig) -> list[Violation]:
189
+ """Analyze Python code for LBYL patterns."""
190
+ tree = _parse_python_code(code)
191
+ if tree is None:
192
+ return []
193
+ return self._run_enabled_detectors(tree, file_path, config)
194
+
195
+ def _run_enabled_detectors(
196
+ self, tree: ast.Module, file_path: str, config: LBYLConfig
197
+ ) -> list[Violation]:
198
+ """Run all enabled pattern detectors and collect violations."""
199
+ # Map detector types to their config flags
200
+ enabled_flags = {
201
+ DictKeyDetector: config.detect_dict_key,
202
+ DivisionCheckDetector: config.detect_division_check,
203
+ FileExistsDetector: config.detect_file_exists,
204
+ HasattrDetector: config.detect_hasattr,
205
+ IsinstanceDetector: config.detect_isinstance,
206
+ LenCheckDetector: config.detect_len_check,
207
+ NoneCheckDetector: config.detect_none_check,
208
+ StringValidatorDetector: config.detect_string_validation,
209
+ }
210
+
211
+ violations: list[Violation] = []
212
+ for detector, converter, pattern_type in self._detector_configs:
213
+ if enabled_flags.get(type(detector), False):
214
+ violations.extend(_run_detector(detector, tree, file_path, converter, pattern_type))
215
+ return violations
@@ -0,0 +1,354 @@
1
+ """
2
+ Purpose: Build Violation objects with EAFP suggestions for LBYL patterns
3
+
4
+ Scope: Creates violations for detected LBYL anti-patterns with fix suggestions
5
+
6
+ Overview: Provides module-level functions that create Violation objects for LBYL
7
+ anti-patterns detected in Python code. Each violation includes the rule ID, location,
8
+ descriptive message, and an EAFP suggestion showing how to refactor the code using
9
+ try/except. Supports dict key check, hasattr, isinstance, file exists, len check,
10
+ None check, string validator, and division zero-check patterns.
11
+
12
+ Dependencies: src.core.types for Violation, src.core.base for BaseLintContext
13
+
14
+ Exports: build_dict_key_violation, build_hasattr_violation, build_isinstance_violation,
15
+ build_file_exists_violation, build_len_check_violation, create_syntax_error_violation,
16
+ build_none_check_violation, build_string_validator_violation, build_division_check_violation
17
+
18
+ Interfaces: Module functions for building LBYL violations
19
+
20
+ Implementation: Factory functions for each violation type with descriptive suggestions
21
+
22
+ Suppressions:
23
+ too-many-arguments, too-many-positional-arguments: build_string_validator_violation
24
+ requires 6 parameters (file_path, line, column, string_name, validator_method,
25
+ conversion_func) to create accurate violation messages and suggestions
26
+ """
27
+
28
+ from src.core.base import BaseLintContext
29
+ from src.core.types import Violation
30
+
31
+
32
+ def build_dict_key_violation(
33
+ file_path: str,
34
+ line: int,
35
+ column: int,
36
+ dict_name: str,
37
+ key_expression: str,
38
+ ) -> Violation:
39
+ """Build a violation for dict key LBYL pattern.
40
+
41
+ Args:
42
+ file_path: Path to the file containing the violation
43
+ line: Line number (1-indexed)
44
+ column: Column number (0-indexed)
45
+ dict_name: Name of the dict being checked
46
+ key_expression: Expression used as key
47
+
48
+ Returns:
49
+ Violation object with EAFP suggestion
50
+ """
51
+ message = (
52
+ f"LBYL pattern: 'if {key_expression} in {dict_name}' followed by "
53
+ f"'{dict_name}[{key_expression}]'"
54
+ )
55
+
56
+ suggestion = (
57
+ f"Use EAFP: 'with suppress(KeyError): value = {dict_name}[{key_expression}]' "
58
+ f"or 'try: ... except KeyError: ...'"
59
+ )
60
+
61
+ return Violation(
62
+ rule_id="lbyl.dict-key-check",
63
+ file_path=file_path,
64
+ line=line,
65
+ column=column,
66
+ message=message,
67
+ suggestion=suggestion,
68
+ )
69
+
70
+
71
+ def build_hasattr_violation(
72
+ file_path: str,
73
+ line: int,
74
+ column: int,
75
+ object_name: str,
76
+ attribute_name: str,
77
+ ) -> Violation:
78
+ """Build a violation for hasattr LBYL pattern.
79
+
80
+ Args:
81
+ file_path: Path to the file containing the violation
82
+ line: Line number (1-indexed)
83
+ column: Column number (0-indexed)
84
+ object_name: Name of the object being checked
85
+ attribute_name: Name of the attribute being checked
86
+
87
+ Returns:
88
+ Violation object with EAFP suggestion
89
+ """
90
+ message = (
91
+ f"LBYL pattern: 'if hasattr({object_name}, '{attribute_name}')' followed by "
92
+ f"'{object_name}.{attribute_name}'"
93
+ )
94
+
95
+ suggestion = f"Use EAFP: 'try: {object_name}.{attribute_name} except AttributeError: ...'"
96
+
97
+ return Violation(
98
+ rule_id="lbyl.hasattr-check",
99
+ file_path=file_path,
100
+ line=line,
101
+ column=column,
102
+ message=message,
103
+ suggestion=suggestion,
104
+ )
105
+
106
+
107
+ def build_isinstance_violation(
108
+ file_path: str,
109
+ line: int,
110
+ column: int,
111
+ object_name: str,
112
+ type_name: str,
113
+ ) -> Violation:
114
+ """Build a violation for isinstance LBYL pattern.
115
+
116
+ Args:
117
+ file_path: Path to the file containing the violation
118
+ line: Line number (1-indexed)
119
+ column: Column number (0-indexed)
120
+ object_name: Name of the object being checked
121
+ type_name: Name of the type being checked against
122
+
123
+ Returns:
124
+ Violation object with EAFP suggestion
125
+ """
126
+ message = (
127
+ f"LBYL pattern: 'if isinstance({object_name}, {type_name})' before type-specific operation"
128
+ )
129
+
130
+ suggestion = (
131
+ "Consider EAFP: 'try: ... except (TypeError, AttributeError): ...' "
132
+ "instead of isinstance check"
133
+ )
134
+
135
+ return Violation(
136
+ rule_id="lbyl.isinstance-check",
137
+ file_path=file_path,
138
+ line=line,
139
+ column=column,
140
+ message=message,
141
+ suggestion=suggestion,
142
+ )
143
+
144
+
145
+ def create_syntax_error_violation(error: SyntaxError, context: BaseLintContext) -> Violation:
146
+ """Create a violation for a syntax error.
147
+
148
+ Args:
149
+ error: The SyntaxError from parsing
150
+ context: Lint context with file information
151
+
152
+ Returns:
153
+ Violation indicating syntax error
154
+ """
155
+ file_path = str(context.file_path) if context.file_path else "unknown"
156
+ line = error.lineno or 1
157
+ column = error.offset or 0
158
+
159
+ return Violation(
160
+ rule_id="lbyl.syntax-error",
161
+ file_path=file_path,
162
+ line=line,
163
+ column=column,
164
+ message=f"Syntax error: {error.msg}",
165
+ suggestion="Fix the syntax error before running LBYL analysis",
166
+ )
167
+
168
+
169
+ def build_file_exists_violation(
170
+ file_path: str,
171
+ line: int,
172
+ column: int,
173
+ path_expression: str,
174
+ check_type: str,
175
+ ) -> Violation:
176
+ """Build a violation for file exists LBYL pattern.
177
+
178
+ Args:
179
+ file_path: Path to the file containing the violation
180
+ line: Line number (1-indexed)
181
+ column: Column number (0-indexed)
182
+ path_expression: Expression representing the file path being checked
183
+ check_type: Type of check ("os.path.exists", "Path.exists", "exists")
184
+
185
+ Returns:
186
+ Violation object with EAFP suggestion
187
+ """
188
+ message = (
189
+ f"LBYL pattern: 'if {check_type}({path_expression})' followed by "
190
+ f"file operation on '{path_expression}'"
191
+ )
192
+
193
+ suggestion = (
194
+ f"Use EAFP: 'try: with open({path_expression}) as f: ... except FileNotFoundError: ...'"
195
+ )
196
+
197
+ return Violation(
198
+ rule_id="lbyl.file-exists-check",
199
+ file_path=file_path,
200
+ line=line,
201
+ column=column,
202
+ message=message,
203
+ suggestion=suggestion,
204
+ )
205
+
206
+
207
+ def build_len_check_violation(
208
+ file_path: str,
209
+ line: int,
210
+ column: int,
211
+ collection_name: str,
212
+ index_expression: str,
213
+ ) -> Violation:
214
+ """Build a violation for len check LBYL pattern.
215
+
216
+ Args:
217
+ file_path: Path to the file containing the violation
218
+ line: Line number (1-indexed)
219
+ column: Column number (0-indexed)
220
+ collection_name: Name of the collection being checked
221
+ index_expression: Expression used as index
222
+
223
+ Returns:
224
+ Violation object with EAFP suggestion
225
+ """
226
+ message = (
227
+ f"LBYL pattern: 'if len({collection_name}) > {index_expression}' followed by "
228
+ f"'{collection_name}[...]'"
229
+ )
230
+
231
+ suggestion = (
232
+ f"Use EAFP: 'try: value = {collection_name}[{index_expression}] except IndexError: ...'"
233
+ )
234
+
235
+ return Violation(
236
+ rule_id="lbyl.len-check",
237
+ file_path=file_path,
238
+ line=line,
239
+ column=column,
240
+ message=message,
241
+ suggestion=suggestion,
242
+ )
243
+
244
+
245
+ def build_none_check_violation(
246
+ file_path: str,
247
+ line: int,
248
+ column: int,
249
+ variable_name: str,
250
+ ) -> Violation:
251
+ """Build a violation for None check LBYL pattern.
252
+
253
+ Args:
254
+ file_path: Path to the file containing the violation
255
+ line: Line number (1-indexed)
256
+ column: Column number (0-indexed)
257
+ variable_name: Name of the variable being checked for None
258
+
259
+ Returns:
260
+ Violation object with EAFP suggestion
261
+ """
262
+ message = (
263
+ f"LBYL pattern: 'if {variable_name} is not None' followed by '{variable_name}.<method>()'"
264
+ )
265
+
266
+ suggestion = (
267
+ f"Use EAFP: 'try: {variable_name}.<method>() except AttributeError: ...' "
268
+ f"or check if None is a valid state to handle differently"
269
+ )
270
+
271
+ return Violation(
272
+ rule_id="lbyl.none-check",
273
+ file_path=file_path,
274
+ line=line,
275
+ column=column,
276
+ message=message,
277
+ suggestion=suggestion,
278
+ )
279
+
280
+
281
+ def build_string_validator_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
282
+ file_path: str,
283
+ line: int,
284
+ column: int,
285
+ string_name: str,
286
+ validator_method: str,
287
+ conversion_func: str,
288
+ ) -> Violation:
289
+ """Build a violation for string validator LBYL pattern.
290
+
291
+ Args:
292
+ file_path: Path to the file containing the violation
293
+ line: Line number (1-indexed)
294
+ column: Column number (0-indexed)
295
+ string_name: Name of the string being validated
296
+ validator_method: Validation method used (isnumeric, isdigit, etc.)
297
+ conversion_func: Conversion function used (int, float)
298
+
299
+ Returns:
300
+ Violation object with EAFP suggestion
301
+ """
302
+ message = (
303
+ f"LBYL pattern: 'if {string_name}.{validator_method}()' followed by "
304
+ f"'{conversion_func}({string_name})'"
305
+ )
306
+
307
+ suggestion = f"Use EAFP: 'try: value = {conversion_func}({string_name}) except ValueError: ...'"
308
+
309
+ return Violation(
310
+ rule_id="lbyl.string-validator",
311
+ file_path=file_path,
312
+ line=line,
313
+ column=column,
314
+ message=message,
315
+ suggestion=suggestion,
316
+ )
317
+
318
+
319
+ def build_division_check_violation(
320
+ file_path: str,
321
+ line: int,
322
+ column: int,
323
+ divisor_name: str,
324
+ operation: str,
325
+ ) -> Violation:
326
+ """Build a violation for division zero-check LBYL pattern.
327
+
328
+ Args:
329
+ file_path: Path to the file containing the violation
330
+ line: Line number (1-indexed)
331
+ column: Column number (0-indexed)
332
+ divisor_name: Name of the divisor being checked for zero
333
+ operation: Division operation used (/, //, %, /=, //=, %=)
334
+
335
+ Returns:
336
+ Violation object with EAFP suggestion
337
+ """
338
+ message = (
339
+ f"LBYL pattern: 'if {divisor_name} != 0' followed by "
340
+ f"'{operation}' operation with '{divisor_name}'"
341
+ )
342
+
343
+ suggestion = (
344
+ f"Use EAFP: 'try: result = ... {operation} {divisor_name} except ZeroDivisionError: ...'"
345
+ )
346
+
347
+ return Violation(
348
+ rule_id="lbyl.division-check",
349
+ file_path=file_path,
350
+ line=line,
351
+ column=column,
352
+ message=message,
353
+ suggestion=suggestion,
354
+ )
@@ -0,0 +1,48 @@
1
+ """
2
+ Purpose: Magic numbers linter package exports and convenience functions
3
+
4
+ Scope: Public API for magic numbers linter module
5
+
6
+ Overview: Provides the public interface for the magic numbers linter package. Exports main
7
+ MagicNumberRule class for use by the orchestrator and MagicNumberConfig for configuration.
8
+ Includes lint() convenience function that provides a simple API for running the magic numbers
9
+ linter on a file or directory without directly interacting with the orchestrator. This module
10
+ serves as the entry point for users of the magic numbers linter, hiding implementation details
11
+ and exposing only the essential components needed for linting operations.
12
+
13
+ Dependencies: .linter for MagicNumberRule, .config for MagicNumberConfig
14
+
15
+ Exports: MagicNumberRule class, MagicNumberConfig dataclass, lint() convenience function
16
+
17
+ Interfaces: lint(path, config) -> list[Violation] for simple linting operations
18
+
19
+ Implementation: Module-level exports with __all__ definition, convenience function wrapper
20
+ """
21
+
22
+ from .config import MagicNumberConfig
23
+ from .linter import MagicNumberRule
24
+
25
+ __all__ = ["MagicNumberRule", "MagicNumberConfig", "lint"]
26
+
27
+
28
+ def lint(file_path: str, config: dict | None = None) -> list:
29
+ """Convenience function for linting a file for magic numbers.
30
+
31
+ Args:
32
+ file_path: Path to the file to lint
33
+ config: Optional configuration dictionary
34
+
35
+ Returns:
36
+ List of violations found
37
+ """
38
+ from pathlib import Path
39
+
40
+ from src.orchestrator.core import FileLintContext
41
+
42
+ rule = MagicNumberRule()
43
+ context = FileLintContext(
44
+ path=Path(file_path),
45
+ lang="python",
46
+ )
47
+
48
+ return rule.check(context)
@@ -0,0 +1,82 @@
1
+ """
2
+ Purpose: Configuration schema for magic numbers linter
3
+
4
+ Scope: MagicNumberConfig dataclass with allowed_numbers and max_small_integer settings
5
+
6
+ Overview: Defines configuration schema for magic numbers linter. Provides MagicNumberConfig dataclass
7
+ with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000)
8
+ and max_small_integer threshold (default 10) for range() contexts. Supports per-file and per-directory
9
+ config overrides through from_dict class method. Validates that configuration values are appropriate
10
+ types. Integrates with orchestrator's configuration system to allow users to customize allowed numbers
11
+ via .thailint.yaml configuration files.
12
+
13
+ Dependencies: dataclasses for class definition, typing for type hints
14
+
15
+ Exports: MagicNumberConfig dataclass
16
+
17
+ Interfaces: MagicNumberConfig(allowed_numbers: set, max_small_integer: int, enabled: bool),
18
+ from_dict class method for loading configuration from dictionary
19
+
20
+ Implementation: Dataclass with validation and defaults, matches reference implementation patterns
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+
27
+ @dataclass
28
+ class MagicNumberConfig:
29
+ """Configuration for magic numbers linter."""
30
+
31
+ enabled: bool = True
32
+ allowed_numbers: set[int | float] = field(
33
+ default_factory=lambda: {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}
34
+ )
35
+ max_small_integer: int = 10
36
+ ignore: list[str] = field(default_factory=list)
37
+
38
+ def __post_init__(self) -> None:
39
+ """Validate configuration values."""
40
+ if self.max_small_integer <= 0:
41
+ raise ValueError(f"max_small_integer must be positive, got {self.max_small_integer}")
42
+
43
+ @classmethod
44
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "MagicNumberConfig":
45
+ """Load configuration from dictionary with language-specific overrides.
46
+
47
+ Args:
48
+ config: Dictionary containing configuration values
49
+ language: Programming language (python, typescript, javascript)
50
+ for language-specific settings
51
+
52
+ Returns:
53
+ MagicNumberConfig instance with values from dictionary
54
+ """
55
+ # Get language-specific config if available
56
+ if language and language in config:
57
+ lang_config = config[language]
58
+ allowed_numbers = set(
59
+ lang_config.get(
60
+ "allowed_numbers",
61
+ config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}),
62
+ )
63
+ )
64
+ max_small_integer = lang_config.get(
65
+ "max_small_integer", config.get("max_small_integer", 10)
66
+ )
67
+ else:
68
+ allowed_numbers = set(
69
+ config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000})
70
+ )
71
+ max_small_integer = config.get("max_small_integer", 10)
72
+
73
+ ignore_patterns = config.get("ignore", [])
74
+ if not isinstance(ignore_patterns, list):
75
+ ignore_patterns = []
76
+
77
+ return cls(
78
+ enabled=config.get("enabled", True),
79
+ allowed_numbers=allowed_numbers,
80
+ max_small_integer=max_small_integer,
81
+ ignore=ignore_patterns,
82
+ )