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.
@@ -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