thailint 0.1.5__py3-none-any.whl → 0.5.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 (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,139 @@
1
+ """
2
+ Purpose: Violation creation for nesting depth linter
3
+
4
+ Scope: Builds Violation objects for nesting depth violations
5
+
6
+ Overview: Provides violation building functionality for the nesting depth linter. Creates
7
+ violations for Python and TypeScript functions with excessive nesting, generates contextual
8
+ error messages with actual vs maximum depth, and provides actionable refactoring suggestions
9
+ (early returns, guard clauses, extract method). Handles syntax errors gracefully. Isolates
10
+ violation construction from analysis and checking logic.
11
+
12
+ Dependencies: ast, BaseLintContext, Violation, Severity, NestingConfig, src.core.violation_builder
13
+
14
+ Exports: NestingViolationBuilder
15
+
16
+ Interfaces: create_nesting_violation, create_typescript_nesting_violation, create_syntax_error_violation
17
+
18
+ Implementation: Formats messages with depth information, provides targeted refactoring suggestions,
19
+ extends BaseViolationBuilder for consistent violation construction
20
+ """
21
+
22
+ import ast
23
+ from typing import Any
24
+
25
+ from src.core.base import BaseLintContext
26
+ from src.core.types import Severity, Violation
27
+ from src.core.violation_builder import BaseViolationBuilder
28
+
29
+ from .config import NestingConfig
30
+
31
+
32
+ class NestingViolationBuilder(BaseViolationBuilder):
33
+ """Builds violations for nesting depth issues."""
34
+
35
+ def __init__(self, rule_id: str):
36
+ """Initialize violation builder.
37
+
38
+ Args:
39
+ rule_id: Rule identifier for violations
40
+ """
41
+ self.rule_id = rule_id
42
+
43
+ def create_syntax_error_violation(
44
+ self, error: SyntaxError, context: BaseLintContext
45
+ ) -> Violation:
46
+ """Create violation for syntax error.
47
+
48
+ Args:
49
+ error: SyntaxError exception
50
+ context: Lint context
51
+
52
+ Returns:
53
+ Syntax error violation
54
+ """
55
+ return self.build_from_params(
56
+ rule_id=self.rule_id,
57
+ file_path=str(context.file_path or ""),
58
+ line=error.lineno or 0,
59
+ column=error.offset or 0,
60
+ message=f"Syntax error: {error.msg}",
61
+ severity=Severity.ERROR,
62
+ suggestion="Fix syntax errors before checking nesting depth",
63
+ )
64
+
65
+ def create_nesting_violation(
66
+ self,
67
+ func: ast.FunctionDef | ast.AsyncFunctionDef,
68
+ max_depth: int,
69
+ config: NestingConfig,
70
+ context: BaseLintContext,
71
+ ) -> Violation:
72
+ """Create violation for excessive nesting in Python function.
73
+
74
+ Args:
75
+ func: Python function AST node
76
+ max_depth: Actual max nesting depth found
77
+ config: Nesting configuration
78
+ context: Lint context
79
+
80
+ Returns:
81
+ Nesting depth violation
82
+ """
83
+ return self.build_from_params(
84
+ rule_id=self.rule_id,
85
+ file_path=str(context.file_path or ""),
86
+ line=func.lineno,
87
+ column=func.col_offset,
88
+ message=f"Function '{func.name}' has excessive nesting depth ({max_depth})",
89
+ severity=Severity.ERROR,
90
+ suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
91
+ )
92
+
93
+ def create_typescript_nesting_violation(
94
+ self,
95
+ func_info: tuple[Any, str],
96
+ max_depth: int,
97
+ config: NestingConfig,
98
+ context: BaseLintContext,
99
+ ) -> Violation:
100
+ """Create violation for excessive nesting in TypeScript function.
101
+
102
+ Args:
103
+ func_info: Tuple of (func_node, func_name)
104
+ max_depth: Actual max nesting depth found
105
+ config: Nesting configuration
106
+ context: Lint context
107
+
108
+ Returns:
109
+ Nesting depth violation
110
+ """
111
+ func_node, func_name = func_info
112
+ line = func_node.start_point[0] + 1 # Convert to 1-indexed
113
+ column = func_node.start_point[1]
114
+
115
+ return self.build_from_params(
116
+ rule_id=self.rule_id,
117
+ file_path=str(context.file_path or ""),
118
+ line=line,
119
+ column=column,
120
+ message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
121
+ severity=Severity.ERROR,
122
+ suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
123
+ )
124
+
125
+ def _generate_suggestion(self, actual_depth: int, max_depth: int) -> str:
126
+ """Generate refactoring suggestion based on depth.
127
+
128
+ Args:
129
+ actual_depth: Actual nesting depth found
130
+ max_depth: Maximum allowed depth
131
+
132
+ Returns:
133
+ Suggestion string with refactoring advice
134
+ """
135
+ return (
136
+ f"Maximum nesting depth of {actual_depth} exceeds limit of {max_depth}. "
137
+ "Consider extracting nested logic to separate functions, using early returns, "
138
+ "or applying guard clauses to reduce nesting."
139
+ )
@@ -0,0 +1,53 @@
1
+ """
2
+ File: src/linters/print_statements/__init__.py
3
+
4
+ Purpose: Print statements linter package exports and convenience functions
5
+
6
+ Exports: PrintStatementRule class, PrintStatementConfig dataclass, lint() convenience function
7
+
8
+ Depends: .linter for PrintStatementRule, .config for PrintStatementConfig
9
+
10
+ Implements: lint(file_path, config) -> list[Violation] for simple linting operations
11
+
12
+ Related: src/linters/magic_numbers/__init__.py, src/core/base.py
13
+
14
+ Overview: Provides the public interface for the print statements linter package. Exports main
15
+ PrintStatementRule class for use by the orchestrator and PrintStatementConfig for configuration.
16
+ Includes lint() convenience function that provides a simple API for running the print statements
17
+ linter on a file without directly interacting with the orchestrator. This module serves as the
18
+ entry point for users of the print statements linter, hiding implementation details and exposing
19
+ only the essential components needed for linting operations.
20
+
21
+ Usage: from src.linters.print_statements import PrintStatementRule, lint
22
+ violations = lint("path/to/file.py")
23
+
24
+ Notes: Module-level exports with __all__ definition, convenience function wrapper
25
+ """
26
+
27
+ from .config import PrintStatementConfig
28
+ from .linter import PrintStatementRule
29
+
30
+ __all__ = ["PrintStatementRule", "PrintStatementConfig", "lint"]
31
+
32
+
33
+ def lint(file_path: str, config: dict | None = None) -> list:
34
+ """Convenience function for linting a file for print statements.
35
+
36
+ Args:
37
+ file_path: Path to the file to lint
38
+ config: Optional configuration dictionary
39
+
40
+ Returns:
41
+ List of violations found
42
+ """
43
+ from pathlib import Path
44
+
45
+ from src.orchestrator.core import FileLintContext
46
+
47
+ rule = PrintStatementRule()
48
+ context = FileLintContext(
49
+ path=Path(file_path),
50
+ lang="python",
51
+ )
52
+
53
+ return rule.check(context)
@@ -0,0 +1,83 @@
1
+ """
2
+ File: src/linters/print_statements/config.py
3
+
4
+ Purpose: Configuration schema for print statements linter
5
+
6
+ Exports: PrintStatementConfig dataclass
7
+
8
+ Depends: dataclasses, typing
9
+
10
+ Implements: PrintStatementConfig(enabled, ignore, allow_in_scripts, console_methods),
11
+ from_dict class method for loading configuration from dictionary
12
+
13
+ Related: src/linters/magic_numbers/config.py, src/core/types.py
14
+
15
+ Overview: Defines configuration schema for print statements linter. Provides PrintStatementConfig
16
+ dataclass with enabled flag, ignore patterns list, allow_in_scripts setting (default True to
17
+ allow print in __main__ blocks), and console_methods set (default includes log, warn, error,
18
+ debug, info) for TypeScript/JavaScript console method detection. Supports per-file and
19
+ per-directory config overrides through from_dict class method. Integrates with orchestrator's
20
+ configuration system to allow users to customize detection via .thailint.yaml configuration.
21
+
22
+ Usage: config = PrintStatementConfig.from_dict(yaml_config, language="python")
23
+
24
+ Notes: Dataclass with defaults matching common use cases, language-specific override support
25
+ """
26
+
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+
31
+ @dataclass
32
+ class PrintStatementConfig:
33
+ """Configuration for print statements linter."""
34
+
35
+ enabled: bool = True
36
+ ignore: list[str] = field(default_factory=list)
37
+ allow_in_scripts: bool = True
38
+ console_methods: set[str] = field(
39
+ default_factory=lambda: {"log", "warn", "error", "debug", "info"}
40
+ )
41
+
42
+ @classmethod
43
+ def from_dict(
44
+ cls, config: dict[str, Any], language: str | None = None
45
+ ) -> "PrintStatementConfig":
46
+ """Load configuration from dictionary with language-specific overrides.
47
+
48
+ Args:
49
+ config: Dictionary containing configuration values
50
+ language: Programming language (python, typescript, javascript)
51
+ for language-specific settings
52
+
53
+ Returns:
54
+ PrintStatementConfig instance with values from dictionary
55
+ """
56
+ # Get language-specific config if available
57
+ if language and language in config:
58
+ lang_config = config[language]
59
+ allow_in_scripts = lang_config.get(
60
+ "allow_in_scripts", config.get("allow_in_scripts", True)
61
+ )
62
+ console_methods = set(
63
+ lang_config.get(
64
+ "console_methods",
65
+ config.get("console_methods", ["log", "warn", "error", "debug", "info"]),
66
+ )
67
+ )
68
+ else:
69
+ allow_in_scripts = config.get("allow_in_scripts", True)
70
+ console_methods = set(
71
+ config.get("console_methods", ["log", "warn", "error", "debug", "info"])
72
+ )
73
+
74
+ ignore_patterns = config.get("ignore", [])
75
+ if not isinstance(ignore_patterns, list):
76
+ ignore_patterns = []
77
+
78
+ return cls(
79
+ enabled=config.get("enabled", True),
80
+ ignore=ignore_patterns,
81
+ allow_in_scripts=allow_in_scripts,
82
+ console_methods=console_methods,
83
+ )
@@ -0,0 +1,430 @@
1
+ """
2
+ File: src/linters/print_statements/linter.py
3
+
4
+ Purpose: Main print statements linter rule implementation
5
+
6
+ Exports: PrintStatementRule class
7
+
8
+ Depends: BaseLintContext, MultiLanguageLintRule, PythonPrintStatementAnalyzer,
9
+ TypeScriptPrintStatementAnalyzer, ViolationBuilder, PrintStatementConfig, IgnoreDirectiveParser
10
+
11
+ Implements: PrintStatementRule.check(context) -> list[Violation], properties for rule metadata
12
+
13
+ Related: src/linters/magic_numbers/linter.py, src/core/base.py
14
+
15
+ Overview: Implements print statements linter rule following BaseLintRule interface. Orchestrates
16
+ configuration loading, Python AST analysis for print() calls, TypeScript tree-sitter analysis
17
+ for console.* calls, and violation building through focused helper classes. Detects print and
18
+ console statements that should be replaced with proper logging. Supports configurable
19
+ allow_in_scripts option to permit print() in __main__ blocks and configurable console_methods
20
+ set for TypeScript/JavaScript. Handles ignore directives for suppressing specific violations.
21
+
22
+ Usage: rule = PrintStatementRule()
23
+ violations = rule.check(context)
24
+
25
+ Notes: Composition pattern with helper classes, AST-based analysis for Python, tree-sitter for TS/JS
26
+ """
27
+
28
+ import ast
29
+ from pathlib import Path
30
+
31
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
32
+ from src.core.linter_utils import load_linter_config
33
+ from src.core.types import Violation
34
+ from src.linter_config.ignore import IgnoreDirectiveParser
35
+
36
+ from .config import PrintStatementConfig
37
+ from .python_analyzer import PythonPrintStatementAnalyzer
38
+ from .typescript_analyzer import TypeScriptPrintStatementAnalyzer
39
+ from .violation_builder import ViolationBuilder
40
+
41
+
42
+ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
43
+ """Detects print/console statements that should be replaced with proper logging."""
44
+
45
+ def __init__(self) -> None:
46
+ """Initialize the print statements rule."""
47
+ self._ignore_parser = IgnoreDirectiveParser()
48
+ self._violation_builder = ViolationBuilder(self.rule_id)
49
+
50
+ @property
51
+ def rule_id(self) -> str:
52
+ """Unique identifier for this rule."""
53
+ return "print-statements.detected"
54
+
55
+ @property
56
+ def rule_name(self) -> str:
57
+ """Human-readable name for this rule."""
58
+ return "Print Statements"
59
+
60
+ @property
61
+ def description(self) -> str:
62
+ """Description of what this rule checks."""
63
+ return "Print/console statements should be replaced with proper logging"
64
+
65
+ def _load_config(self, context: BaseLintContext) -> PrintStatementConfig:
66
+ """Load configuration from context.
67
+
68
+ Args:
69
+ context: Lint context
70
+
71
+ Returns:
72
+ PrintStatementConfig instance
73
+ """
74
+ test_config = self._try_load_test_config(context)
75
+ if test_config is not None:
76
+ return test_config
77
+
78
+ prod_config = self._try_load_production_config(context)
79
+ if prod_config is not None:
80
+ return prod_config
81
+
82
+ return PrintStatementConfig()
83
+
84
+ def _try_load_test_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
85
+ """Try to load test-style configuration."""
86
+ if not hasattr(context, "config"):
87
+ return None
88
+ config_attr = context.config
89
+ if config_attr is None or not isinstance(config_attr, dict):
90
+ return None
91
+ return PrintStatementConfig.from_dict(config_attr, context.language)
92
+
93
+ def _try_load_production_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
94
+ """Try to load production configuration."""
95
+ if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
96
+ return None
97
+
98
+ metadata = context.metadata
99
+
100
+ if "print_statements" in metadata:
101
+ return load_linter_config(context, "print_statements", PrintStatementConfig)
102
+
103
+ if "print-statements" in metadata:
104
+ return load_linter_config(context, "print-statements", PrintStatementConfig)
105
+
106
+ return None
107
+
108
+ def _is_file_ignored(self, context: BaseLintContext, config: PrintStatementConfig) -> bool:
109
+ """Check if file matches ignore patterns.
110
+
111
+ Args:
112
+ context: Lint context
113
+ config: Print statements configuration
114
+
115
+ Returns:
116
+ True if file should be ignored
117
+ """
118
+ if not config.ignore:
119
+ return False
120
+
121
+ if not context.file_path:
122
+ return False
123
+
124
+ file_path = Path(context.file_path)
125
+ for pattern in config.ignore:
126
+ if self._matches_pattern(file_path, pattern):
127
+ return True
128
+ return False
129
+
130
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
131
+ """Check if file path matches a glob pattern.
132
+
133
+ Args:
134
+ file_path: Path to check
135
+ pattern: Glob pattern
136
+
137
+ Returns:
138
+ True if path matches pattern
139
+ """
140
+ if file_path.match(pattern):
141
+ return True
142
+ if pattern in str(file_path):
143
+ return True
144
+ return False
145
+
146
+ def _check_python(
147
+ self, context: BaseLintContext, config: PrintStatementConfig
148
+ ) -> list[Violation]:
149
+ """Check Python code for print() violations.
150
+
151
+ Args:
152
+ context: Lint context with Python file information
153
+ config: Print statements configuration
154
+
155
+ Returns:
156
+ List of violations found in Python code
157
+ """
158
+ if self._is_file_ignored(context, config):
159
+ return []
160
+
161
+ tree = self._parse_python_code(context.file_content)
162
+ if tree is None:
163
+ return []
164
+
165
+ analyzer = PythonPrintStatementAnalyzer()
166
+ print_calls = analyzer.find_print_calls(tree)
167
+ return self._collect_python_violations(print_calls, context, config, analyzer)
168
+
169
+ def _parse_python_code(self, code: str | None) -> ast.AST | None:
170
+ """Parse Python code into AST."""
171
+ try:
172
+ return ast.parse(code or "")
173
+ except SyntaxError:
174
+ return None
175
+
176
+ def _collect_python_violations(
177
+ self,
178
+ print_calls: list,
179
+ context: BaseLintContext,
180
+ config: PrintStatementConfig,
181
+ analyzer: PythonPrintStatementAnalyzer,
182
+ ) -> list[Violation]:
183
+ """Collect violations from Python print() calls.
184
+
185
+ Args:
186
+ print_calls: List of (node, parent, line_number) tuples
187
+ context: Lint context
188
+ config: Configuration
189
+ analyzer: Python analyzer instance
190
+
191
+ Returns:
192
+ List of violations
193
+ """
194
+ violations = []
195
+ for node, _parent, line_number in print_calls:
196
+ violation = self._try_create_python_violation(
197
+ node, line_number, context, config, analyzer
198
+ )
199
+ if violation is not None:
200
+ violations.append(violation)
201
+ return violations
202
+
203
+ def _try_create_python_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
204
+ self,
205
+ node: ast.Call,
206
+ line_number: int,
207
+ context: BaseLintContext,
208
+ config: PrintStatementConfig,
209
+ analyzer: PythonPrintStatementAnalyzer,
210
+ ) -> Violation | None:
211
+ """Try to create a violation for a Python print() call.
212
+
213
+ Args:
214
+ node: AST Call node
215
+ line_number: Line number
216
+ context: Lint context
217
+ config: Configuration
218
+ analyzer: Python analyzer
219
+
220
+ Returns:
221
+ Violation or None if should not flag
222
+ """
223
+ # Check if in __main__ block and allow_in_scripts is enabled
224
+ if config.allow_in_scripts and analyzer.is_in_main_block(node):
225
+ return None
226
+
227
+ violation = self._violation_builder.create_python_violation(
228
+ node, line_number, context.file_path
229
+ )
230
+
231
+ if self._should_ignore(violation, context):
232
+ return None
233
+
234
+ return violation
235
+
236
+ def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
237
+ """Check if violation should be ignored based on inline directives.
238
+
239
+ Args:
240
+ violation: Violation to check
241
+ context: Lint context with file content
242
+
243
+ Returns:
244
+ True if violation should be ignored
245
+ """
246
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
247
+ return True
248
+ return self._check_generic_ignore(violation, context)
249
+
250
+ def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
251
+ """Check for generic ignore directives.
252
+
253
+ Args:
254
+ violation: Violation to check
255
+ context: Lint context
256
+
257
+ Returns:
258
+ True if line has generic ignore directive
259
+ """
260
+ line_text = self._get_violation_line(violation, context)
261
+ if line_text is None:
262
+ return False
263
+ return self._has_generic_ignore_directive(line_text)
264
+
265
+ def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
266
+ """Get the line text for a violation."""
267
+ if not context.file_content:
268
+ return None
269
+
270
+ lines = context.file_content.splitlines()
271
+ if violation.line <= 0 or violation.line > len(lines):
272
+ return None
273
+
274
+ return lines[violation.line - 1].lower()
275
+
276
+ def _has_generic_ignore_directive(self, line_text: str) -> bool:
277
+ """Check if line has generic ignore directive."""
278
+ if self._has_generic_thailint_ignore(line_text):
279
+ return True
280
+ return self._has_noqa_directive(line_text)
281
+
282
+ def _has_generic_thailint_ignore(self, line_text: str) -> bool:
283
+ """Check for generic thailint: ignore (no brackets)."""
284
+ if "# thailint: ignore" not in line_text:
285
+ return False
286
+ after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
287
+ return "[" not in after_ignore
288
+
289
+ def _has_noqa_directive(self, line_text: str) -> bool:
290
+ """Check for noqa-style comments."""
291
+ return "# noqa" in line_text
292
+
293
+ def _check_typescript(
294
+ self, context: BaseLintContext, config: PrintStatementConfig
295
+ ) -> list[Violation]:
296
+ """Check TypeScript/JavaScript code for console.* violations.
297
+
298
+ Args:
299
+ context: Lint context with TypeScript/JavaScript file information
300
+ config: Print statements configuration
301
+
302
+ Returns:
303
+ List of violations found in TypeScript/JavaScript code
304
+ """
305
+ if self._is_file_ignored(context, config):
306
+ return []
307
+
308
+ analyzer = TypeScriptPrintStatementAnalyzer()
309
+ root_node = analyzer.parse_typescript(context.file_content or "")
310
+ if root_node is None:
311
+ return []
312
+
313
+ console_calls = analyzer.find_console_calls(root_node, config.console_methods)
314
+ return self._collect_typescript_violations(console_calls, context)
315
+
316
+ def _collect_typescript_violations(
317
+ self,
318
+ console_calls: list,
319
+ context: BaseLintContext,
320
+ ) -> list[Violation]:
321
+ """Collect violations from TypeScript console.* calls.
322
+
323
+ Args:
324
+ console_calls: List of (node, method_name, line_number) tuples
325
+ context: Lint context
326
+
327
+ Returns:
328
+ List of violations
329
+ """
330
+ violations = []
331
+ for _node, method_name, line_number in console_calls:
332
+ violation = self._try_create_typescript_violation(method_name, line_number, context)
333
+ if violation is not None:
334
+ violations.append(violation)
335
+ return violations
336
+
337
+ def _try_create_typescript_violation(
338
+ self,
339
+ method_name: str,
340
+ line_number: int,
341
+ context: BaseLintContext,
342
+ ) -> Violation | None:
343
+ """Try to create a violation for a TypeScript console.* call.
344
+
345
+ Args:
346
+ method_name: Console method name (log, warn, etc.)
347
+ line_number: Line number
348
+ context: Lint context
349
+
350
+ Returns:
351
+ Violation or None if should not flag
352
+ """
353
+ # Check if test file (skip test files)
354
+ if self._is_test_file(context.file_path):
355
+ return None
356
+
357
+ violation = self._violation_builder.create_typescript_violation(
358
+ method_name, line_number, context.file_path
359
+ )
360
+
361
+ if self._should_ignore_typescript(violation, context):
362
+ return None
363
+
364
+ return violation
365
+
366
+ def _is_test_file(self, file_path: object) -> bool:
367
+ """Check if file is a test file.
368
+
369
+ Args:
370
+ file_path: Path to check
371
+
372
+ Returns:
373
+ True if test file
374
+ """
375
+ path_str = str(file_path)
376
+ return any(
377
+ pattern in path_str
378
+ for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
379
+ )
380
+
381
+ def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
382
+ """Check if TypeScript violation should be ignored.
383
+
384
+ Args:
385
+ violation: Violation to check
386
+ context: Lint context
387
+
388
+ Returns:
389
+ True if should ignore
390
+ """
391
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
392
+ return True
393
+ return self._check_typescript_ignore(violation, context)
394
+
395
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
396
+ """Check for TypeScript-style ignore directives.
397
+
398
+ Args:
399
+ violation: Violation to check
400
+ context: Lint context
401
+
402
+ Returns:
403
+ True if line has ignore directive
404
+ """
405
+ line_text = self._get_violation_line(violation, context)
406
+ if line_text is None:
407
+ return False
408
+ return self._has_typescript_ignore_directive(line_text)
409
+
410
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
411
+ """Check if line has TypeScript-style ignore directive.
412
+
413
+ Args:
414
+ line_text: Line text to check
415
+
416
+ Returns:
417
+ True if has ignore directive
418
+ """
419
+ if "// thailint: ignore[print-statements]" in line_text:
420
+ return True
421
+
422
+ if "// thailint: ignore" in line_text:
423
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
424
+ if "[" not in after_ignore:
425
+ return True
426
+
427
+ if "// noqa" in line_text:
428
+ return True
429
+
430
+ return False