thailint 0.10.0__py3-none-any.whl → 0.12.0__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 (76) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +450 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +395 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +36 -0
  46. src/linters/stringly_typed/config.py +190 -0
  47. src/linters/stringly_typed/context_filter.py +451 -0
  48. src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  49. src/linters/stringly_typed/ignore_checker.py +102 -0
  50. src/linters/stringly_typed/ignore_utils.py +51 -0
  51. src/linters/stringly_typed/linter.py +344 -0
  52. src/linters/stringly_typed/python/__init__.py +33 -0
  53. src/linters/stringly_typed/python/analyzer.py +344 -0
  54. src/linters/stringly_typed/python/call_tracker.py +172 -0
  55. src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/linters/stringly_typed/storage.py +630 -0
  63. src/linters/stringly_typed/storage_initializer.py +45 -0
  64. src/linters/stringly_typed/typescript/__init__.py +28 -0
  65. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  66. src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  67. src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  68. src/linters/stringly_typed/violation_generator.py +376 -0
  69. src/orchestrator/core.py +241 -12
  70. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
  71. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
  72. thailint-0.12.0.dist-info/entry_points.txt +4 -0
  73. src/cli.py +0 -2141
  74. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  75. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
  76. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,255 @@
1
+ """
2
+ Purpose: Detects single-statement patterns in TypeScript/JavaScript code for DRY linter filtering
3
+
4
+ Scope: Tree-sitter AST analysis to identify single logical statements that should not be flagged
5
+
6
+ Overview: Provides sophisticated single-statement pattern detection to filter false positives in the
7
+ DRY linter for TypeScript/JavaScript code. Uses tree-sitter AST to identify when a code block
8
+ represents a single logical statement (decorators, call expressions, object literals, class fields,
9
+ JSX elements, interface definitions) that should not be flagged as duplicate code.
10
+
11
+ Dependencies: tree-sitter for TypeScript AST parsing
12
+
13
+ Exports: TypeScriptStatementDetector class
14
+
15
+ Interfaces: TypeScriptStatementDetector.is_single_statement(content, start_line, end_line) -> bool
16
+
17
+ Implementation: Tree-sitter AST walking with pattern matching for TypeScript constructs
18
+
19
+ SRP Exception: TypeScriptStatementDetector has 20 methods (exceeds max 8 methods)
20
+ Justification: Complex tree-sitter AST analysis algorithm for single-statement pattern detection.
21
+ Methods form tightly coupled algorithm pipeline: decorator detection, call expression analysis,
22
+ declaration patterns, JSX element handling, class body field definitions, and interface filtering.
23
+ Similar to parser or compiler pass architecture where algorithmic cohesion is critical. Splitting
24
+ would fragment the algorithm logic and make maintenance harder by separating interdependent
25
+ tree-sitter AST analysis steps. All methods contribute to single responsibility: accurately
26
+ detecting single-statement patterns to prevent false positives in TypeScript duplicate detection.
27
+ """
28
+
29
+ from collections.abc import Generator
30
+
31
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
32
+
33
+ if TREE_SITTER_AVAILABLE:
34
+ from tree_sitter import Node
35
+ else:
36
+ Node = None # type: ignore[assignment,misc] # pylint: disable=invalid-name
37
+
38
+
39
+ class TypeScriptStatementDetector: # thailint: ignore[srp.violation]
40
+ """Detects single-statement patterns in TypeScript/JavaScript for duplicate filtering.
41
+
42
+ SRP suppression: Complex tree-sitter AST analysis algorithm requires 20 methods to implement
43
+ sophisticated single-statement detection. See file header for justification.
44
+ """
45
+
46
+ def is_single_statement(self, content: str, start_line: int, end_line: int) -> bool:
47
+ """Check if a line range is a single logical statement.
48
+
49
+ Args:
50
+ content: TypeScript source code
51
+ start_line: Starting line number (1-indexed)
52
+ end_line: Ending line number (1-indexed)
53
+
54
+ Returns:
55
+ True if this range represents a single logical statement/expression
56
+ """
57
+ if not TREE_SITTER_AVAILABLE:
58
+ return False
59
+
60
+ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
61
+
62
+ analyzer = TypeScriptBaseAnalyzer()
63
+ root = analyzer.parse_typescript(content)
64
+ if not root:
65
+ return False
66
+
67
+ return self._check_overlapping_nodes(root, start_line, end_line)
68
+
69
+ def _check_overlapping_nodes(self, root: Node, start_line: int, end_line: int) -> bool:
70
+ """Check if any AST node overlaps and matches single-statement pattern."""
71
+ ts_start = start_line - 1 # Convert to 0-indexed
72
+ ts_end = end_line - 1
73
+
74
+ for node in self._walk_nodes(root):
75
+ if self._node_overlaps_and_matches(node, ts_start, ts_end):
76
+ return True
77
+ return False
78
+
79
+ def _walk_nodes(self, node: Node) -> Generator[Node, None, None]:
80
+ """Generator to walk all nodes in tree."""
81
+ yield node
82
+ for child in node.children:
83
+ yield from self._walk_nodes(child)
84
+
85
+ def _node_overlaps_and_matches(self, node: Node, ts_start: int, ts_end: int) -> bool:
86
+ """Check if node overlaps with range and matches single-statement pattern."""
87
+ node_start = node.start_point[0]
88
+ node_end = node.end_point[0]
89
+
90
+ overlaps = not (node_end < ts_start or node_start > ts_end)
91
+ if not overlaps:
92
+ return False
93
+
94
+ return self._is_single_statement_pattern(node, ts_start, ts_end)
95
+
96
+ def _is_single_statement_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
97
+ """Check if an AST node represents a single-statement pattern to filter."""
98
+ node_start = node.start_point[0]
99
+ node_end = node.end_point[0]
100
+ contains = (node_start <= ts_start) and (node_end >= ts_end)
101
+
102
+ matchers = [
103
+ self._matches_simple_container_pattern(node, contains),
104
+ self._matches_call_expression_pattern(node, ts_start, ts_end, contains),
105
+ self._matches_declaration_pattern(node, contains),
106
+ self._matches_jsx_pattern(node, contains),
107
+ self._matches_class_body_pattern(node, ts_start, ts_end),
108
+ ]
109
+ return any(matchers)
110
+
111
+ def _matches_simple_container_pattern(self, node: Node, contains: bool) -> bool:
112
+ """Check if node is a simple container pattern (decorator, object, etc.)."""
113
+ simple_types = (
114
+ "decorator",
115
+ "object",
116
+ "member_expression",
117
+ "as_expression",
118
+ "array_pattern",
119
+ )
120
+ return node.type in simple_types and contains
121
+
122
+ def _matches_call_expression_pattern(
123
+ self, node: Node, ts_start: int, ts_end: int, contains: bool
124
+ ) -> bool:
125
+ """Check if node is a call expression pattern."""
126
+ if node.type != "call_expression":
127
+ return False
128
+
129
+ node_start = node.start_point[0]
130
+ node_end = node.end_point[0]
131
+ is_multiline = node_start < node_end
132
+ if is_multiline and node_start <= ts_start <= node_end:
133
+ return True
134
+
135
+ return contains
136
+
137
+ def _matches_declaration_pattern(self, node: Node, contains: bool) -> bool:
138
+ """Check if node is a lexical declaration pattern."""
139
+ if node.type != "lexical_declaration" or not contains:
140
+ return False
141
+
142
+ if self._contains_function_body(node):
143
+ return False
144
+
145
+ return True
146
+
147
+ def _matches_jsx_pattern(self, node: Node, contains: bool) -> bool:
148
+ """Check if node is a JSX element pattern."""
149
+ jsx_types = ("jsx_opening_element", "jsx_self_closing_element")
150
+ return node.type in jsx_types and contains
151
+
152
+ def _matches_class_body_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
153
+ """Check if node is a class body field definition pattern."""
154
+ if node.type != "class_body":
155
+ return False
156
+
157
+ return self._is_in_class_field_area(node, ts_start, ts_end)
158
+
159
+ def _contains_function_body(self, node: Node) -> bool:
160
+ """Check if node contains an arrow function or function expression."""
161
+ for child in node.children:
162
+ if child.type in ("arrow_function", "function", "function_expression"):
163
+ return True
164
+ if self._contains_function_body(child):
165
+ return True
166
+ return False
167
+
168
+ def _find_first_method_line(self, class_body: Node) -> int | None:
169
+ """Find line number of first method in class body."""
170
+ for child in class_body.children:
171
+ if child.type in ("method_definition", "function_declaration"):
172
+ return child.start_point[0]
173
+ return None
174
+
175
+ def _is_in_class_field_area(self, class_body: Node, ts_start: int, ts_end: int) -> bool:
176
+ """Check if range is in class field definition area (before methods)."""
177
+ first_method_line = self._find_first_method_line(class_body)
178
+ class_start = class_body.start_point[0]
179
+ class_end = class_body.end_point[0]
180
+
181
+ if first_method_line is None:
182
+ return class_start <= ts_start and class_end >= ts_end
183
+
184
+ return class_start <= ts_start and ts_end < first_method_line
185
+
186
+ def should_include_block(self, content: str, start_line: int, end_line: int) -> bool:
187
+ """Check if block should be included (not overlapping interface definitions).
188
+
189
+ Args:
190
+ content: File content
191
+ start_line: Block start line
192
+ end_line: Block end line
193
+
194
+ Returns:
195
+ False if block overlaps interface definition, True otherwise
196
+ """
197
+ interface_ranges = self._find_interface_ranges(content)
198
+ return not self._overlaps_interface(start_line, end_line, interface_ranges)
199
+
200
+ def _find_interface_ranges(self, content: str) -> list[tuple[int, int]]:
201
+ """Find line ranges of interface/type definitions."""
202
+ ranges: list[tuple[int, int]] = []
203
+ lines = content.split("\n")
204
+ state = {"in_interface": False, "start_line": 0, "brace_count": 0}
205
+
206
+ for i, line in enumerate(lines, start=1):
207
+ stripped = line.strip()
208
+ self._process_line_for_interface(stripped, i, state, ranges)
209
+
210
+ return ranges
211
+
212
+ def _process_line_for_interface(
213
+ self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
214
+ ) -> None:
215
+ """Process single line for interface detection."""
216
+ if self._is_interface_start(stripped):
217
+ self._handle_interface_start(stripped, line_num, state, ranges)
218
+ return
219
+
220
+ if state["in_interface"]:
221
+ self._handle_interface_continuation(stripped, line_num, state, ranges)
222
+
223
+ def _is_interface_start(self, stripped: str) -> bool:
224
+ """Check if line starts interface/type definition."""
225
+ return stripped.startswith(("interface ", "type ")) and "{" in stripped
226
+
227
+ def _handle_interface_start(
228
+ self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
229
+ ) -> None:
230
+ """Handle start of interface definition."""
231
+ state["in_interface"] = True
232
+ state["start_line"] = line_num
233
+ state["brace_count"] = stripped.count("{") - stripped.count("}")
234
+
235
+ if state["brace_count"] == 0:
236
+ ranges.append((line_num, line_num))
237
+ state["in_interface"] = False
238
+
239
+ def _handle_interface_continuation(
240
+ self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
241
+ ) -> None:
242
+ """Handle continuation of interface definition."""
243
+ state["brace_count"] += stripped.count("{") - stripped.count("}")
244
+ if state["brace_count"] == 0:
245
+ ranges.append((state["start_line"], line_num))
246
+ state["in_interface"] = False
247
+
248
+ def _overlaps_interface(
249
+ self, start: int, end: int, interface_ranges: list[tuple[int, int]]
250
+ ) -> bool:
251
+ """Check if block overlaps with any interface range."""
252
+ for if_start, if_end in interface_ranges:
253
+ if start <= if_end and end >= if_start:
254
+ return True
255
+ return False
@@ -0,0 +1,66 @@
1
+ """
2
+ Purpose: Extract value representations from TypeScript AST nodes
3
+
4
+ Scope: Helper for TypeScript constant extraction to extract value strings
5
+
6
+ Overview: Provides utility methods to extract string representations from tree-sitter AST nodes
7
+ for TypeScript value types (numbers, strings, booleans, arrays, objects, call expressions).
8
+ Used by TypeScriptConstantExtractor to get value context for duplicate constant detection.
9
+
10
+ Dependencies: tree-sitter, tree-sitter-typescript, src.analyzers.typescript_base
11
+
12
+ Exports: TypeScriptValueExtractor class
13
+
14
+ Interfaces: TypeScriptValueExtractor.get_value_string(node, content) -> str | None
15
+
16
+ Implementation: Tree-sitter node traversal with type-specific string formatting
17
+ """
18
+
19
+ from typing import Any
20
+
21
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
22
+
23
+ if TREE_SITTER_AVAILABLE:
24
+ from tree_sitter import Node
25
+ else:
26
+ Node = Any # type: ignore[assignment,misc]
27
+
28
+
29
+ class TypeScriptValueExtractor:
30
+ """Extracts value representations from TypeScript AST nodes."""
31
+
32
+ # Types that return their literal text
33
+ LITERAL_TYPES = frozenset(("number", "string", "true", "false", "null", "identifier"))
34
+
35
+ # Types with fixed representations
36
+ FIXED_REPRESENTATIONS = {"array": "[...]", "object": "{...}"}
37
+
38
+ def get_node_text(self, node: Node, content: str) -> str:
39
+ """Get text content of a node."""
40
+ return content[node.start_byte : node.end_byte]
41
+
42
+ def get_value_string(self, node: Node, content: str) -> str | None:
43
+ """Get string representation of a value node."""
44
+ if node.type in self.LITERAL_TYPES:
45
+ return self.get_node_text(node, content)
46
+ if node.type in self.FIXED_REPRESENTATIONS:
47
+ return self.FIXED_REPRESENTATIONS[node.type]
48
+ if node.type == "call_expression":
49
+ return self._get_call_string(node, content)
50
+ return None
51
+
52
+ def _get_call_string(self, node: Node, content: str) -> str:
53
+ """Get string representation of a call expression.
54
+
55
+ Args:
56
+ node: call_expression node
57
+ content: Original source content
58
+
59
+ Returns:
60
+ String like "functionName(...)"
61
+ """
62
+ for child in node.children:
63
+ if child.type == "identifier":
64
+ func_name = self.get_node_text(child, content)
65
+ return f"{func_name}(...)"
66
+ return "call(...)"
@@ -28,7 +28,7 @@ from typing import Protocol
28
28
  from src.core.base import BaseLintContext, BaseLintRule
29
29
  from src.core.linter_utils import load_linter_config
30
30
  from src.core.types import Violation
31
- from src.linter_config.ignore import IgnoreDirectiveParser
31
+ from src.linter_config.ignore import get_ignore_parser
32
32
 
33
33
  from .atemporal_detector import AtemporalDetector
34
34
  from .bash_parser import BashHeaderParser
@@ -73,7 +73,7 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
73
73
  def __init__(self) -> None:
74
74
  """Initialize the file header rule."""
75
75
  self._violation_builder = ViolationBuilder(self.rule_id)
76
- self._ignore_parser = IgnoreDirectiveParser()
76
+ self._ignore_parser = get_ignore_parser()
77
77
 
78
78
  @property
79
79
  def rule_id(self) -> str:
@@ -137,9 +137,9 @@ class FilePlacementLinter:
137
137
  Returns:
138
138
  List of file paths to lint
139
139
  """
140
- from src.linter_config.ignore import IgnoreDirectiveParser
140
+ from src.linter_config.ignore import get_ignore_parser
141
141
 
142
- ignore_parser = IgnoreDirectiveParser(self.project_root)
142
+ ignore_parser = get_ignore_parser(self.project_root)
143
143
  pattern = "**/*" if recursive else "*"
144
144
 
145
145
  return [
@@ -18,14 +18,28 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
18
18
  """
19
19
 
20
20
  import re
21
+ from re import Pattern
21
22
 
22
23
 
23
24
  class PatternMatcher:
24
25
  """Handles regex pattern matching for file paths."""
25
26
 
26
27
  def __init__(self) -> None:
27
- """Initialize the pattern matcher."""
28
- pass # Stateless matcher for regex patterns
28
+ """Initialize the pattern matcher with compiled regex cache."""
29
+ self._compiled_patterns: dict[str, Pattern[str]] = {}
30
+
31
+ def _get_compiled(self, pattern: str) -> Pattern[str]:
32
+ """Get compiled regex pattern, caching for reuse.
33
+
34
+ Args:
35
+ pattern: Regex pattern string
36
+
37
+ Returns:
38
+ Compiled regex Pattern object
39
+ """
40
+ if pattern not in self._compiled_patterns:
41
+ self._compiled_patterns[pattern] = re.compile(pattern, re.IGNORECASE)
42
+ return self._compiled_patterns[pattern]
29
43
 
30
44
  def match_deny_patterns(
31
45
  self, path_str: str, deny_patterns: list[dict[str, str]]
@@ -40,8 +54,8 @@ class PatternMatcher:
40
54
  Tuple of (is_denied, reason)
41
55
  """
42
56
  for deny_item in deny_patterns:
43
- pattern = deny_item["pattern"]
44
- if re.search(pattern, path_str, re.IGNORECASE):
57
+ compiled = self._get_compiled(deny_item["pattern"])
58
+ if compiled.search(path_str):
45
59
  reason = deny_item.get("reason", "File not allowed in this location")
46
60
  return True, reason
47
61
  return False, None
@@ -56,4 +70,4 @@ class PatternMatcher:
56
70
  Returns:
57
71
  True if path matches any pattern
58
72
  """
59
- return any(re.search(pattern, path_str, re.IGNORECASE) for pattern in allow_patterns)
73
+ return any(self._get_compiled(pattern).search(path_str) for pattern in allow_patterns)
@@ -29,12 +29,14 @@ from pathlib import Path
29
29
  from src.core.base import BaseLintContext, MultiLanguageLintRule
30
30
  from src.core.linter_utils import load_linter_config
31
31
  from src.core.types import Violation
32
- from src.linter_config.ignore import IgnoreDirectiveParser
32
+ from src.core.violation_utils import get_violation_line, has_python_noqa
33
+ from src.linter_config.ignore import get_ignore_parser
33
34
 
34
35
  from .config import MagicNumberConfig
35
36
  from .context_analyzer import ContextAnalyzer
36
37
  from .python_analyzer import PythonMagicNumberAnalyzer
37
38
  from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
39
+ from .typescript_ignore_checker import TypeScriptIgnoreChecker
38
40
  from .violation_builder import ViolationBuilder
39
41
 
40
42
 
@@ -43,9 +45,10 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
43
45
 
44
46
  def __init__(self) -> None:
45
47
  """Initialize the magic numbers rule."""
46
- self._ignore_parser = IgnoreDirectiveParser()
48
+ self._ignore_parser = get_ignore_parser()
47
49
  self._violation_builder = ViolationBuilder(self.rule_id)
48
50
  self._context_analyzer = ContextAnalyzer()
51
+ self._typescript_ignore_checker = TypeScriptIgnoreChecker()
49
52
 
50
53
  @property
51
54
  def rule_id(self) -> str:
@@ -282,28 +285,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
282
285
  Returns:
283
286
  True if line has generic ignore directive
284
287
  """
285
- line_text = self._get_violation_line(violation, context)
288
+ line_text = get_violation_line(violation, context)
286
289
  if line_text is None:
287
290
  return False
288
291
 
289
292
  return self._has_generic_ignore_directive(line_text)
290
293
 
291
- def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
292
- """Get the line text for a violation."""
293
- if not context.file_content:
294
- return None
295
-
296
- lines = context.file_content.splitlines()
297
- if violation.line <= 0 or violation.line > len(lines):
298
- return None
299
-
300
- return lines[violation.line - 1].lower()
301
-
302
294
  def _has_generic_ignore_directive(self, line_text: str) -> bool:
303
295
  """Check if line has generic ignore directive."""
304
296
  if self._has_generic_thailint_ignore(line_text):
305
297
  return True
306
- return self._has_noqa_directive(line_text)
298
+ return has_python_noqa(line_text)
307
299
 
308
300
  def _has_generic_thailint_ignore(self, line_text: str) -> bool:
309
301
  """Check for generic thailint: ignore (no brackets)."""
@@ -312,10 +304,6 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
312
304
  after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
313
305
  return "[" not in after_ignore
314
306
 
315
- def _has_noqa_directive(self, line_text: str) -> bool:
316
- """Check for noqa-style comments."""
317
- return "# noqa" in line_text
318
-
319
307
  def _check_typescript(
320
308
  self, context: BaseLintContext, config: MagicNumberConfig
321
309
  ) -> list[Violation]:
@@ -466,51 +454,4 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
466
454
  Returns:
467
455
  True if should ignore
468
456
  """
469
- # Check standard ignore directives
470
- if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
471
- return True
472
-
473
- # Check TypeScript-style comments
474
- return self._check_typescript_ignore(violation, context)
475
-
476
- def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
477
- """Check for TypeScript-style ignore directives.
478
-
479
- Args:
480
- violation: Violation to check
481
- context: Lint context
482
-
483
- Returns:
484
- True if line has ignore directive
485
- """
486
- line_text = self._get_violation_line(violation, context)
487
- if line_text is None:
488
- return False
489
-
490
- # Check for // thailint: ignore or // noqa
491
- return self._has_typescript_ignore_directive(line_text)
492
-
493
- def _has_typescript_ignore_directive(self, line_text: str) -> bool:
494
- """Check if line has TypeScript-style ignore directive.
495
-
496
- Args:
497
- line_text: Line text to check
498
-
499
- Returns:
500
- True if has ignore directive
501
- """
502
- # Check for // thailint: ignore[magic-numbers]
503
- if "// thailint: ignore[magic-numbers]" in line_text:
504
- return True
505
-
506
- # Check for // thailint: ignore (generic)
507
- if "// thailint: ignore" in line_text:
508
- after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
509
- if "[" not in after_ignore:
510
- return True
511
-
512
- # Check for // noqa
513
- if "// noqa" in line_text:
514
- return True
515
-
516
- return False
457
+ return self._typescript_ignore_checker.should_ignore(violation, context)
@@ -0,0 +1,81 @@
1
+ """
2
+ Purpose: TypeScript-specific ignore directive checking for magic numbers linter
3
+
4
+ Scope: Ignore directive detection for TypeScript/JavaScript files
5
+
6
+ Overview: Provides ignore directive checking functionality specifically for TypeScript and JavaScript
7
+ files in the magic numbers linter. Handles both thailint-style and noqa-style ignore comments
8
+ using TypeScript comment syntax (// instead of #). Extracted from linter.py to reduce file
9
+ size and improve modularity.
10
+
11
+ Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, violation_utils
12
+
13
+ Exports: TypeScriptIgnoreChecker class
14
+
15
+ Interfaces: TypeScriptIgnoreChecker.should_ignore(violation, context) -> bool
16
+
17
+ Implementation: Comment parsing with TypeScript-specific syntax handling, uses shared utilities
18
+ """
19
+
20
+ from src.core.base import BaseLintContext
21
+ from src.core.types import Violation
22
+ from src.core.violation_utils import get_violation_line, has_typescript_noqa
23
+ from src.linter_config.ignore import get_ignore_parser
24
+
25
+
26
+ class TypeScriptIgnoreChecker:
27
+ """Checks for TypeScript-style ignore directives in magic numbers linter."""
28
+
29
+ def __init__(self) -> None:
30
+ """Initialize with standard ignore parser."""
31
+ self._ignore_parser = get_ignore_parser()
32
+
33
+ def should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
34
+ """Check if TypeScript violation should be ignored.
35
+
36
+ Args:
37
+ violation: Violation to check
38
+ context: Lint context
39
+
40
+ Returns:
41
+ True if should ignore
42
+ """
43
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
44
+ return True
45
+
46
+ return self._check_typescript_ignore(violation, context)
47
+
48
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
49
+ """Check for TypeScript-style ignore directives.
50
+
51
+ Args:
52
+ violation: Violation to check
53
+ context: Lint context
54
+
55
+ Returns:
56
+ True if line has ignore directive
57
+ """
58
+ line_text = get_violation_line(violation, context)
59
+ if line_text is None:
60
+ return False
61
+
62
+ return self._has_typescript_ignore_directive(line_text)
63
+
64
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
65
+ """Check if line has TypeScript-style ignore directive.
66
+
67
+ Args:
68
+ line_text: Line text to check
69
+
70
+ Returns:
71
+ True if has ignore directive
72
+ """
73
+ if "// thailint: ignore[magic-numbers]" in line_text:
74
+ return True
75
+
76
+ if "// thailint: ignore" in line_text:
77
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
78
+ if "[" not in after_ignore:
79
+ return True
80
+
81
+ return has_typescript_noqa(line_text)
@@ -24,7 +24,7 @@ from typing import Any
24
24
  from src.core.base import BaseLintContext, MultiLanguageLintRule
25
25
  from src.core.linter_utils import load_linter_config
26
26
  from src.core.types import Violation
27
- from src.linter_config.ignore import IgnoreDirectiveParser
27
+ from src.linter_config.ignore import get_ignore_parser
28
28
 
29
29
  from .config import NestingConfig
30
30
  from .python_analyzer import PythonNestingAnalyzer
@@ -37,8 +37,11 @@ class NestingDepthRule(MultiLanguageLintRule):
37
37
 
38
38
  def __init__(self) -> None:
39
39
  """Initialize the nesting depth rule."""
40
- self._ignore_parser = IgnoreDirectiveParser()
40
+ self._ignore_parser = get_ignore_parser()
41
41
  self._violation_builder = NestingViolationBuilder(self.rule_id)
42
+ # Singleton analyzers for performance (avoid recreating per-file)
43
+ self._python_analyzer = PythonNestingAnalyzer()
44
+ self._typescript_analyzer = TypeScriptNestingAnalyzer()
42
45
 
43
46
  @property
44
47
  def rule_id(self) -> str:
@@ -108,9 +111,8 @@ class NestingDepthRule(MultiLanguageLintRule):
108
111
  except SyntaxError as e:
109
112
  return [self._violation_builder.create_syntax_error_violation(e, context)]
110
113
 
111
- analyzer = PythonNestingAnalyzer()
112
- functions = analyzer.find_all_functions(tree)
113
- return self._process_python_functions(functions, analyzer, config, context)
114
+ functions = self._python_analyzer.find_all_functions(tree)
115
+ return self._process_python_functions(functions, self._python_analyzer, config, context)
114
116
 
115
117
  def _process_typescript_functions(
116
118
  self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
@@ -149,13 +151,14 @@ class NestingDepthRule(MultiLanguageLintRule):
149
151
  Returns:
150
152
  List of violations found in TypeScript code
151
153
  """
152
- analyzer = TypeScriptNestingAnalyzer()
153
- root_node = analyzer.parse_typescript(context.file_content or "")
154
+ root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
154
155
  if root_node is None:
155
156
  return []
156
157
 
157
- functions = analyzer.find_all_functions(root_node)
158
- return self._process_typescript_functions(functions, analyzer, config, context)
158
+ functions = self._typescript_analyzer.find_all_functions(root_node)
159
+ return self._process_typescript_functions(
160
+ functions, self._typescript_analyzer, config, context
161
+ )
159
162
 
160
163
  def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
161
164
  """Check if violation should be ignored based on inline directives.