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