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,249 @@
1
+ """
2
+ Purpose: Analyzes contexts to determine if numeric literals are acceptable
3
+
4
+ Scope: Context detection for magic number acceptable usage patterns
5
+
6
+ Overview: Provides module-level functions that determine whether a numeric literal is in an acceptable
7
+ context where it should not be flagged as a magic number. Detects acceptable contexts including
8
+ constant definitions (UPPERCASE names), small integers in range() or enumerate() calls, test files,
9
+ and configuration contexts. Uses AST node analysis to inspect parent nodes and determine the usage
10
+ pattern of numeric literals. Helps reduce false positives by distinguishing between legitimate
11
+ numeric literals and true magic numbers that should be extracted to constants.
12
+
13
+ Dependencies: ast module for AST node types, pathlib for Path handling
14
+
15
+ Exports: is_acceptable_context function and helper functions
16
+
17
+ Interfaces: is_acceptable_context(node, parent, file_path, config) -> bool
18
+
19
+ Implementation: AST parent node inspection, pattern matching for acceptable contexts, configurable
20
+ max_small_integer threshold for range detection
21
+
22
+ Suppressions:
23
+ - B101: Assert used for internal invariant checking, not security validation
24
+ """
25
+
26
+ import ast
27
+ from pathlib import Path
28
+
29
+
30
+ def is_acceptable_context(
31
+ node: ast.Constant,
32
+ parent: ast.AST | None,
33
+ file_path: Path | None,
34
+ config: dict,
35
+ ) -> bool:
36
+ """Check if a numeric literal is in an acceptable context.
37
+
38
+ Args:
39
+ node: The numeric constant node
40
+ parent: The parent node in the AST
41
+ file_path: Path to the file being analyzed
42
+ config: Configuration with allowed_numbers and max_small_integer
43
+
44
+ Returns:
45
+ True if the context is acceptable and should not be flagged
46
+ """
47
+ # File-level and definition checks
48
+ if is_test_file(file_path) or is_constant_definition(node, parent):
49
+ return True
50
+
51
+ # Usage pattern checks
52
+ return _is_acceptable_usage_pattern(node, parent, config)
53
+
54
+
55
+ def _is_acceptable_usage_pattern(node: ast.Constant, parent: ast.AST | None, config: dict) -> bool:
56
+ """Check if numeric literal is in acceptable usage pattern.
57
+
58
+ Args:
59
+ node: The numeric constant node
60
+ parent: The parent node in the AST
61
+ config: Configuration with max_small_integer threshold
62
+
63
+ Returns:
64
+ True if usage pattern is acceptable
65
+ """
66
+ if is_small_integer_in_range(node, parent, config):
67
+ return True
68
+
69
+ if is_small_integer_in_enumerate(node, parent, config):
70
+ return True
71
+
72
+ return is_string_repetition(node, parent)
73
+
74
+
75
+ def is_test_file(file_path: Path | None) -> bool:
76
+ """Check if the file is a test file.
77
+
78
+ Args:
79
+ file_path: Path to the file
80
+
81
+ Returns:
82
+ True if the file is a test file (matches test_*.py pattern)
83
+ """
84
+ if not file_path:
85
+ return False
86
+ return file_path.name.startswith("test_") or "_test.py" in file_path.name
87
+
88
+
89
+ def is_constant_definition(node: ast.Constant, parent: ast.AST | None) -> bool:
90
+ """Check if the number is part of an UPPERCASE constant definition.
91
+
92
+ Args:
93
+ node: The numeric constant node
94
+ parent: The parent node in the AST
95
+
96
+ Returns:
97
+ True if this is a constant definition
98
+ """
99
+ if not _is_assignment_node(parent):
100
+ return False
101
+
102
+ # Type narrowing: parent is ast.Assign after the check above
103
+ assert isinstance(parent, ast.Assign) # nosec B101
104
+ return _has_constant_target(parent)
105
+
106
+
107
+ def _is_assignment_node(parent: ast.AST | None) -> bool:
108
+ """Check if parent is an assignment node."""
109
+ return parent is not None and isinstance(parent, ast.Assign)
110
+
111
+
112
+ def _has_constant_target(parent: ast.Assign) -> bool:
113
+ """Check if assignment has uppercase constant target."""
114
+ return any(
115
+ isinstance(target, ast.Name) and _is_constant_name(target.id) for target in parent.targets
116
+ )
117
+
118
+
119
+ def _is_constant_name(name: str) -> bool:
120
+ """Check if a name follows constant naming convention.
121
+
122
+ Args:
123
+ name: Variable name to check
124
+
125
+ Returns:
126
+ True if the name is UPPERCASE (constant convention)
127
+ """
128
+ return name.isupper() and len(name) > 1
129
+
130
+
131
+ def is_small_integer_in_range(node: ast.Constant, parent: ast.AST | None, config: dict) -> bool:
132
+ """Check if this is a small integer used in range() call.
133
+
134
+ Args:
135
+ node: The numeric constant node
136
+ parent: The parent node in the AST
137
+ config: Configuration with max_small_integer threshold
138
+
139
+ Returns:
140
+ True if this is a small integer in range()
141
+ """
142
+ if not isinstance(node.value, int):
143
+ return False
144
+
145
+ max_small_int = config.get("max_small_integer", 10)
146
+ if not 0 <= node.value <= max_small_int:
147
+ return False
148
+
149
+ return _is_in_range_call(parent)
150
+
151
+
152
+ def is_small_integer_in_enumerate(node: ast.Constant, parent: ast.AST | None, config: dict) -> bool:
153
+ """Check if this is a small integer used in enumerate() call.
154
+
155
+ Args:
156
+ node: The numeric constant node
157
+ parent: The parent node in the AST
158
+ config: Configuration with max_small_integer threshold
159
+
160
+ Returns:
161
+ True if this is a small integer in enumerate()
162
+ """
163
+ if not isinstance(node.value, int):
164
+ return False
165
+
166
+ max_small_int = config.get("max_small_integer", 10)
167
+ if not 0 <= node.value <= max_small_int:
168
+ return False
169
+
170
+ return _is_in_enumerate_call(parent)
171
+
172
+
173
+ def _is_in_range_call(parent: ast.AST | None) -> bool:
174
+ """Check if the parent is a range() call.
175
+
176
+ Args:
177
+ parent: The parent node
178
+
179
+ Returns:
180
+ True if parent is range() call
181
+ """
182
+ return (
183
+ isinstance(parent, ast.Call)
184
+ and isinstance(parent.func, ast.Name)
185
+ and parent.func.id == "range"
186
+ )
187
+
188
+
189
+ def _is_in_enumerate_call(parent: ast.AST | None) -> bool:
190
+ """Check if the parent is an enumerate() call.
191
+
192
+ Args:
193
+ parent: The parent node
194
+
195
+ Returns:
196
+ True if parent is enumerate() call
197
+ """
198
+ return (
199
+ isinstance(parent, ast.Call)
200
+ and isinstance(parent.func, ast.Name)
201
+ and parent.func.id == "enumerate"
202
+ )
203
+
204
+
205
+ def is_string_repetition(node: ast.Constant, parent: ast.AST | None) -> bool:
206
+ """Check if this number is used in string repetition (e.g., "-" * 40).
207
+
208
+ Args:
209
+ node: The numeric constant node
210
+ parent: The parent node in the AST
211
+
212
+ Returns:
213
+ True if this is a string repetition pattern
214
+ """
215
+ if not isinstance(node.value, int):
216
+ return False
217
+
218
+ if not isinstance(parent, ast.BinOp):
219
+ return False
220
+
221
+ if not isinstance(parent.op, ast.Mult):
222
+ return False
223
+
224
+ # Check if either operand is a string constant
225
+ return _has_string_operand(parent)
226
+
227
+
228
+ def _has_string_operand(binop: ast.BinOp) -> bool:
229
+ """Check if binary operation has a string operand.
230
+
231
+ Args:
232
+ binop: Binary operation node
233
+
234
+ Returns:
235
+ True if either left or right operand is a string constant
236
+ """
237
+ return _is_string_constant(binop.left) or _is_string_constant(binop.right)
238
+
239
+
240
+ def _is_string_constant(node: ast.AST) -> bool:
241
+ """Check if a node is a string constant.
242
+
243
+ Args:
244
+ node: AST node to check
245
+
246
+ Returns:
247
+ True if node is a Constant with string value
248
+ """
249
+ return isinstance(node, ast.Constant) and isinstance(node.value, str)
@@ -0,0 +1,462 @@
1
+ """
2
+ Purpose: Main magic numbers linter rule implementation
3
+
4
+ Scope: MagicNumberRule class implementing BaseLintRule interface
5
+
6
+ Overview: Implements magic numbers linter rule following BaseLintRule interface. Orchestrates
7
+ configuration loading, Python AST analysis, context detection, and violation building through
8
+ focused helper classes. Detects numeric literals that should be extracted to named constants.
9
+ Supports configurable allowed_numbers set and max_small_integer threshold. Handles ignore
10
+ directives for suppressing specific violations. Main rule class acts as coordinator for magic
11
+ number checking workflow across Python code files. Method count (17) exceeds SRP limit (8)
12
+ because refactoring for A-grade complexity requires extracting helper methods. Class maintains
13
+ single responsibility of magic number detection - all methods support this core purpose.
14
+
15
+ Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer, is_acceptable_context,
16
+ ViolationBuilder, MagicNumberConfig, IgnoreDirectiveParser
17
+
18
+ Exports: MagicNumberRule class
19
+
20
+ Interfaces: MagicNumberRule.check(context) -> list[Violation], properties for rule metadata
21
+
22
+ Implementation: Composition pattern with helper classes, AST-based analysis with configurable
23
+ allowed numbers and context detection
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: TypeScript violation creation with related params
27
+ - srp: Rule class coordinates analyzers and violation builders. Method count exceeds limit
28
+ due to complexity refactoring. All methods support magic number detection.
29
+ """
30
+
31
+ import ast
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
36
+ from src.core.linter_utils import load_linter_config
37
+ from src.core.types import Violation
38
+ from src.core.violation_utils import get_violation_line, has_python_noqa
39
+ from src.linter_config.ignore import get_ignore_parser
40
+
41
+ from .config import MagicNumberConfig
42
+ from .context_analyzer import is_acceptable_context
43
+ from .python_analyzer import PythonMagicNumberAnalyzer
44
+ from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
45
+ from .typescript_ignore_checker import TypeScriptIgnoreChecker
46
+ from .violation_builder import ViolationBuilder
47
+
48
+
49
+ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
50
+ """Detects magic numbers that should be replaced with named constants."""
51
+
52
+ def __init__(self) -> None:
53
+ """Initialize the magic numbers rule."""
54
+ self._ignore_parser = get_ignore_parser()
55
+ self._violation_builder = ViolationBuilder(self.rule_id)
56
+ self._typescript_ignore_checker = TypeScriptIgnoreChecker()
57
+
58
+ @property
59
+ def rule_id(self) -> str:
60
+ """Unique identifier for this rule."""
61
+ return "magic-numbers.numeric-literal"
62
+
63
+ @property
64
+ def rule_name(self) -> str:
65
+ """Human-readable name for this rule."""
66
+ return "Magic Numbers"
67
+
68
+ @property
69
+ def description(self) -> str:
70
+ """Description of what this rule checks."""
71
+ return "Numeric literals should be replaced with named constants for better maintainability"
72
+
73
+ def _load_config(self, context: BaseLintContext) -> MagicNumberConfig:
74
+ """Load configuration from context.
75
+
76
+ Args:
77
+ context: Lint context
78
+
79
+ Returns:
80
+ MagicNumberConfig instance
81
+ """
82
+ # Try test-style config first
83
+ test_config = self._try_load_test_config(context)
84
+ if test_config is not None:
85
+ return test_config
86
+
87
+ # Try production config
88
+ prod_config = self._try_load_production_config(context)
89
+ if prod_config is not None:
90
+ return prod_config
91
+
92
+ # Use defaults
93
+ return MagicNumberConfig()
94
+
95
+ def _try_load_test_config(self, context: BaseLintContext) -> MagicNumberConfig | None:
96
+ """Try to load test-style configuration."""
97
+ if not hasattr(context, "config"):
98
+ return None
99
+ config_attr = context.config
100
+ if config_attr is None or not isinstance(config_attr, dict):
101
+ return None
102
+ return MagicNumberConfig.from_dict(config_attr, context.language)
103
+
104
+ def _try_load_production_config(self, context: BaseLintContext) -> MagicNumberConfig | None:
105
+ """Try to load production configuration."""
106
+ if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
107
+ return None
108
+
109
+ # Try both hyphenated and underscored keys for backward compatibility
110
+ # The config parser normalizes keys when loading from YAML, but
111
+ # direct metadata injection (tests) may use either format
112
+ metadata = context.metadata
113
+
114
+ # Try underscore version first (normalized format)
115
+ if "magic_numbers" in metadata:
116
+ return load_linter_config(context, "magic_numbers", MagicNumberConfig)
117
+
118
+ # Fallback to hyphenated version (for direct test injection)
119
+ if "magic-numbers" in metadata:
120
+ return load_linter_config(context, "magic-numbers", MagicNumberConfig)
121
+
122
+ # No config found, return None to use defaults
123
+ return None
124
+
125
+ def _is_file_ignored(self, context: BaseLintContext, config: MagicNumberConfig) -> bool:
126
+ """Check if file matches ignore patterns.
127
+
128
+ Args:
129
+ context: Lint context
130
+ config: Magic numbers configuration
131
+
132
+ Returns:
133
+ True if file should be ignored
134
+ """
135
+ if not config.ignore:
136
+ return False
137
+
138
+ if not context.file_path:
139
+ return False
140
+
141
+ file_path = Path(context.file_path)
142
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
143
+
144
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
145
+ """Check if file path matches a glob pattern.
146
+
147
+ Args:
148
+ file_path: Path to check
149
+ pattern: Glob pattern (e.g., "test/**", "**/test_*.py", "specific/file.py")
150
+
151
+ Returns:
152
+ True if path matches pattern
153
+ """
154
+ # Try glob pattern matching first (handles **, *, etc.)
155
+ if file_path.match(pattern):
156
+ return True
157
+
158
+ # Also check if pattern is a substring (for partial path matching)
159
+ if pattern in str(file_path):
160
+ return True
161
+
162
+ return False
163
+
164
+ def _check_python(self, context: BaseLintContext, config: MagicNumberConfig) -> list[Violation]:
165
+ """Check Python code for magic number violations.
166
+
167
+ Args:
168
+ context: Lint context with Python file information
169
+ config: Magic numbers configuration
170
+
171
+ Returns:
172
+ List of violations found in Python code
173
+ """
174
+ if self._is_file_ignored(context, config):
175
+ return []
176
+
177
+ tree = self._parse_python_code(context.file_content)
178
+ if tree is None:
179
+ return []
180
+
181
+ numeric_literals = self._find_numeric_literals(tree)
182
+ return self._collect_violations(numeric_literals, context, config)
183
+
184
+ def _parse_python_code(self, code: str | None) -> ast.AST | None:
185
+ """Parse Python code into AST."""
186
+ try:
187
+ return ast.parse(code or "")
188
+ except SyntaxError:
189
+ return None
190
+
191
+ def _find_numeric_literals(self, tree: ast.AST) -> list:
192
+ """Find all numeric literals in AST."""
193
+ analyzer = PythonMagicNumberAnalyzer()
194
+ return analyzer.find_numeric_literals(tree)
195
+
196
+ def _collect_violations(
197
+ self, numeric_literals: list, context: BaseLintContext, config: MagicNumberConfig
198
+ ) -> list[Violation]:
199
+ """Collect violations from numeric literals."""
200
+ violations = []
201
+ for literal_info in numeric_literals:
202
+ violation = self._try_create_violation(literal_info, context, config)
203
+ if violation is not None:
204
+ violations.append(violation)
205
+ return violations
206
+
207
+ def _try_create_violation(
208
+ self, literal_info: tuple, context: BaseLintContext, config: MagicNumberConfig
209
+ ) -> Violation | None:
210
+ """Try to create a violation for a numeric literal.
211
+
212
+ Args:
213
+ literal_info: Tuple of (node, parent, value, line_number)
214
+ context: Lint context
215
+ config: Configuration
216
+ """
217
+ node, parent, value, line_number = literal_info
218
+ if not self._should_flag_number(value, (node, parent), config, context):
219
+ return None
220
+
221
+ violation = self._violation_builder.create_violation(
222
+ node, value, line_number, context.file_path
223
+ )
224
+ if self._should_ignore(violation, context):
225
+ return None
226
+
227
+ return violation
228
+
229
+ def _should_flag_number(
230
+ self,
231
+ value: int | float,
232
+ node_info: tuple[ast.Constant, ast.AST | None],
233
+ config: MagicNumberConfig,
234
+ context: BaseLintContext,
235
+ ) -> bool:
236
+ """Determine if a number should be flagged as a magic number.
237
+
238
+ Args:
239
+ value: The numeric value
240
+ node_info: Tuple of (node, parent) AST nodes
241
+ config: Configuration
242
+ context: Lint context
243
+
244
+ Returns:
245
+ True if the number should be flagged
246
+ """
247
+ if value in config.allowed_numbers:
248
+ return False
249
+
250
+ node, parent = node_info
251
+ config_dict = {
252
+ "max_small_integer": config.max_small_integer,
253
+ "allowed_numbers": config.allowed_numbers,
254
+ }
255
+
256
+ if is_acceptable_context(node, parent, context.file_path, config_dict):
257
+ return False
258
+
259
+ return True
260
+
261
+ def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
262
+ """Check if violation should be ignored based on inline directives.
263
+
264
+ Args:
265
+ violation: Violation to check
266
+ context: Lint context with file content
267
+
268
+ Returns:
269
+ True if violation should be ignored
270
+ """
271
+ # Check using standard ignore parser
272
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
273
+ return True
274
+
275
+ # Workaround for generic ignore directives
276
+ return self._check_generic_ignore(violation, context)
277
+
278
+ def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
279
+ """Check for generic ignore directives (workaround for parser limitation).
280
+
281
+ Args:
282
+ violation: Violation to check
283
+ context: Lint context
284
+
285
+ Returns:
286
+ True if line has generic ignore directive
287
+ """
288
+ line_text = get_violation_line(violation, context)
289
+ if line_text is None:
290
+ return False
291
+
292
+ return self._has_generic_ignore_directive(line_text)
293
+
294
+ def _has_generic_ignore_directive(self, line_text: str) -> bool:
295
+ """Check if line has generic ignore directive."""
296
+ if self._has_generic_thailint_ignore(line_text):
297
+ return True
298
+ return has_python_noqa(line_text)
299
+
300
+ def _has_generic_thailint_ignore(self, line_text: str) -> bool:
301
+ """Check for generic thailint: ignore (no brackets)."""
302
+ if "# thailint: ignore" not in line_text:
303
+ return False
304
+ after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
305
+ return "[" not in after_ignore
306
+
307
+ def _check_typescript(
308
+ self, context: BaseLintContext, config: MagicNumberConfig
309
+ ) -> list[Violation]:
310
+ """Check TypeScript/JavaScript code for magic number violations.
311
+
312
+ Args:
313
+ context: Lint context with TypeScript/JavaScript file information
314
+ config: Magic numbers configuration
315
+
316
+ Returns:
317
+ List of violations found in TypeScript/JavaScript code
318
+ """
319
+ if self._is_file_ignored(context, config):
320
+ return []
321
+
322
+ analyzer = TypeScriptMagicNumberAnalyzer()
323
+ root_node = analyzer.parse_typescript(context.file_content or "")
324
+ if root_node is None:
325
+ return []
326
+
327
+ numeric_literals = analyzer.find_numeric_literals(root_node)
328
+ return self._collect_typescript_violations(numeric_literals, context, config, analyzer)
329
+
330
+ def _collect_typescript_violations(
331
+ self,
332
+ numeric_literals: list,
333
+ context: BaseLintContext,
334
+ config: MagicNumberConfig,
335
+ analyzer: TypeScriptMagicNumberAnalyzer,
336
+ ) -> list[Violation]:
337
+ """Collect violations from TypeScript numeric literals.
338
+
339
+ Args:
340
+ numeric_literals: List of (node, value, line_number) tuples
341
+ context: Lint context
342
+ config: Configuration
343
+ analyzer: TypeScript analyzer instance
344
+
345
+ Returns:
346
+ List of violations
347
+ """
348
+ violations = []
349
+ for node, value, line_number in numeric_literals:
350
+ violation = self._try_create_typescript_violation(
351
+ node, value, line_number, context, config, analyzer
352
+ )
353
+ if violation is not None:
354
+ violations.append(violation)
355
+ return violations
356
+
357
+ def _try_create_typescript_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
358
+ self,
359
+ node: object,
360
+ value: float | int,
361
+ line_number: int,
362
+ context: BaseLintContext,
363
+ config: MagicNumberConfig,
364
+ analyzer: TypeScriptMagicNumberAnalyzer,
365
+ ) -> Violation | None:
366
+ """Try to create a violation for a TypeScript numeric literal.
367
+
368
+ Args:
369
+ node: Tree-sitter node
370
+ value: Numeric value
371
+ line_number: Line number of literal
372
+ context: Lint context
373
+ config: Configuration
374
+ analyzer: TypeScript analyzer
375
+
376
+ Returns:
377
+ Violation or None if should not flag
378
+ """
379
+ if not self._should_flag_typescript_number(node, value, context, config, analyzer):
380
+ return None
381
+
382
+ violation = self._violation_builder.create_typescript_violation(
383
+ value, line_number, context.file_path
384
+ )
385
+ if self._should_ignore_typescript(violation, context):
386
+ return None
387
+
388
+ return violation
389
+
390
+ def _should_flag_typescript_number( # pylint: disable=too-many-arguments,too-many-positional-arguments
391
+ self,
392
+ node: object,
393
+ value: float | int,
394
+ context: BaseLintContext,
395
+ config: MagicNumberConfig,
396
+ analyzer: TypeScriptMagicNumberAnalyzer,
397
+ ) -> bool:
398
+ """Determine if a TypeScript number should be flagged.
399
+
400
+ Args:
401
+ node: Tree-sitter node
402
+ value: Numeric value
403
+ context: Lint context
404
+ config: Configuration
405
+ analyzer: TypeScript analyzer
406
+
407
+ Returns:
408
+ True if should flag as magic number
409
+ """
410
+ # Early return for allowed contexts
411
+ if self._is_typescript_allowed_context(value, context, config):
412
+ return False
413
+
414
+ # Check TypeScript-specific contexts
415
+ return not self._is_typescript_special_context(node, analyzer, context)
416
+
417
+ def _is_typescript_allowed_context(
418
+ self, value: float | int, context: BaseLintContext, config: MagicNumberConfig
419
+ ) -> bool:
420
+ """Check if number is in allowed context."""
421
+ return value in config.allowed_numbers or self._is_test_file(context.file_path)
422
+
423
+ def _is_typescript_special_context(
424
+ self, node: Any, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
425
+ ) -> bool:
426
+ """Check if in TypeScript-specific special context.
427
+
428
+ Args:
429
+ node: Tree-sitter Node (typed as Any due to optional dependency)
430
+ analyzer: TypeScript analyzer
431
+ context: Lint context
432
+ """
433
+ in_enum = analyzer.is_enum_context(node)
434
+ in_const_def = analyzer.is_constant_definition(node, context.file_content or "")
435
+ return in_enum or in_const_def
436
+
437
+ def _is_test_file(self, file_path: object) -> bool:
438
+ """Check if file is a test file.
439
+
440
+ Args:
441
+ file_path: Path to check
442
+
443
+ Returns:
444
+ True if test file
445
+ """
446
+ path_str = str(file_path)
447
+ return any(
448
+ pattern in path_str
449
+ for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
450
+ )
451
+
452
+ def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
453
+ """Check if TypeScript violation should be ignored.
454
+
455
+ Args:
456
+ violation: Violation to check
457
+ context: Lint context
458
+
459
+ Returns:
460
+ True if should ignore
461
+ """
462
+ return self._typescript_ignore_checker.should_ignore(violation, context)