thailint 0.12.0__py3-none-any.whl → 0.14.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 (135) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +9 -0
  8. src/cli/linters/code_patterns.py +107 -257
  9. src/cli/linters/code_smells.py +48 -165
  10. src/cli/linters/documentation.py +21 -95
  11. src/cli/linters/performance.py +274 -0
  12. src/cli/linters/shared.py +232 -6
  13. src/cli/linters/structure.py +26 -21
  14. src/cli/linters/structure_quality.py +28 -21
  15. src/cli_main.py +3 -0
  16. src/config.py +2 -1
  17. src/core/base.py +3 -2
  18. src/core/cli_utils.py +3 -1
  19. src/core/config_parser.py +5 -2
  20. src/core/constants.py +54 -0
  21. src/core/linter_utils.py +95 -6
  22. src/core/rule_discovery.py +5 -1
  23. src/core/violation_builder.py +3 -0
  24. src/linter_config/directive_markers.py +109 -0
  25. src/linter_config/ignore.py +225 -383
  26. src/linter_config/pattern_utils.py +65 -0
  27. src/linter_config/rule_matcher.py +89 -0
  28. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  29. src/linters/collection_pipeline/ast_utils.py +40 -0
  30. src/linters/collection_pipeline/config.py +12 -0
  31. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  32. src/linters/collection_pipeline/detector.py +262 -32
  33. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  34. src/linters/collection_pipeline/linter.py +18 -35
  35. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  36. src/linters/dry/base_token_analyzer.py +16 -9
  37. src/linters/dry/block_filter.py +7 -4
  38. src/linters/dry/cache.py +7 -2
  39. src/linters/dry/config.py +7 -1
  40. src/linters/dry/constant_matcher.py +34 -25
  41. src/linters/dry/file_analyzer.py +4 -2
  42. src/linters/dry/inline_ignore.py +7 -16
  43. src/linters/dry/linter.py +48 -25
  44. src/linters/dry/python_analyzer.py +18 -10
  45. src/linters/dry/python_constant_extractor.py +51 -52
  46. src/linters/dry/single_statement_detector.py +14 -12
  47. src/linters/dry/token_hasher.py +115 -115
  48. src/linters/dry/typescript_analyzer.py +11 -6
  49. src/linters/dry/typescript_constant_extractor.py +4 -0
  50. src/linters/dry/typescript_statement_detector.py +208 -208
  51. src/linters/dry/typescript_value_extractor.py +3 -0
  52. src/linters/dry/violation_filter.py +1 -4
  53. src/linters/dry/violation_generator.py +1 -4
  54. src/linters/file_header/atemporal_detector.py +58 -40
  55. src/linters/file_header/base_parser.py +4 -0
  56. src/linters/file_header/bash_parser.py +4 -0
  57. src/linters/file_header/config.py +14 -0
  58. src/linters/file_header/field_validator.py +5 -8
  59. src/linters/file_header/linter.py +19 -12
  60. src/linters/file_header/markdown_parser.py +6 -0
  61. src/linters/file_placement/config_loader.py +3 -1
  62. src/linters/file_placement/linter.py +22 -8
  63. src/linters/file_placement/pattern_matcher.py +21 -4
  64. src/linters/file_placement/pattern_validator.py +21 -7
  65. src/linters/file_placement/rule_checker.py +2 -2
  66. src/linters/lazy_ignores/__init__.py +43 -0
  67. src/linters/lazy_ignores/config.py +66 -0
  68. src/linters/lazy_ignores/directive_utils.py +121 -0
  69. src/linters/lazy_ignores/header_parser.py +177 -0
  70. src/linters/lazy_ignores/linter.py +158 -0
  71. src/linters/lazy_ignores/matcher.py +135 -0
  72. src/linters/lazy_ignores/python_analyzer.py +205 -0
  73. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  74. src/linters/lazy_ignores/skip_detector.py +298 -0
  75. src/linters/lazy_ignores/types.py +69 -0
  76. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  77. src/linters/lazy_ignores/violation_builder.py +131 -0
  78. src/linters/lbyl/__init__.py +29 -0
  79. src/linters/lbyl/config.py +63 -0
  80. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  81. src/linters/lbyl/pattern_detectors/base.py +46 -0
  82. src/linters/magic_numbers/context_analyzer.py +227 -229
  83. src/linters/magic_numbers/linter.py +20 -15
  84. src/linters/magic_numbers/python_analyzer.py +4 -16
  85. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  86. src/linters/method_property/config.py +4 -1
  87. src/linters/method_property/linter.py +5 -10
  88. src/linters/method_property/python_analyzer.py +5 -4
  89. src/linters/method_property/violation_builder.py +3 -0
  90. src/linters/nesting/linter.py +11 -6
  91. src/linters/nesting/typescript_analyzer.py +6 -12
  92. src/linters/nesting/typescript_function_extractor.py +0 -4
  93. src/linters/nesting/violation_builder.py +1 -0
  94. src/linters/performance/__init__.py +91 -0
  95. src/linters/performance/config.py +43 -0
  96. src/linters/performance/constants.py +49 -0
  97. src/linters/performance/linter.py +149 -0
  98. src/linters/performance/python_analyzer.py +365 -0
  99. src/linters/performance/regex_analyzer.py +312 -0
  100. src/linters/performance/regex_linter.py +139 -0
  101. src/linters/performance/typescript_analyzer.py +236 -0
  102. src/linters/performance/violation_builder.py +160 -0
  103. src/linters/print_statements/linter.py +6 -4
  104. src/linters/print_statements/python_analyzer.py +85 -81
  105. src/linters/print_statements/typescript_analyzer.py +6 -15
  106. src/linters/srp/heuristics.py +4 -4
  107. src/linters/srp/linter.py +12 -12
  108. src/linters/srp/violation_builder.py +0 -4
  109. src/linters/stateless_class/linter.py +30 -36
  110. src/linters/stateless_class/python_analyzer.py +11 -20
  111. src/linters/stringly_typed/config.py +4 -5
  112. src/linters/stringly_typed/context_filter.py +410 -410
  113. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  114. src/linters/stringly_typed/linter.py +48 -16
  115. src/linters/stringly_typed/python/analyzer.py +5 -1
  116. src/linters/stringly_typed/python/call_tracker.py +8 -5
  117. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  118. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  119. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  120. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  121. src/linters/stringly_typed/python/validation_detector.py +3 -0
  122. src/linters/stringly_typed/storage.py +14 -14
  123. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  124. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  125. src/linters/stringly_typed/violation_generator.py +288 -259
  126. src/orchestrator/core.py +13 -4
  127. src/templates/thailint_config_template.yaml +196 -0
  128. src/utils/project_root.py +3 -0
  129. thailint-0.14.0.dist-info/METADATA +185 -0
  130. thailint-0.14.0.dist-info/RECORD +199 -0
  131. thailint-0.12.0.dist-info/METADATA +0 -1667
  132. thailint-0.12.0.dist-info/RECORD +0 -164
  133. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  134. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  135. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,180 @@
1
+ """
2
+ Purpose: Pure utility functions for rule ID parsing and matching
3
+
4
+ Scope: String parsing utilities for comma-separated rule lists and type:ignore formats
5
+
6
+ Overview: Provides pure functions for parsing and matching rule IDs in various formats
7
+ used by the lazy-ignores linter. Handles comma-separated rule lists (e.g.,
8
+ "too-many-arguments,too-many-positional-arguments") and type:ignore bracket
9
+ formats (e.g., "type:ignore[arg-type,return-value]"). Functions are stateless
10
+ and can be easily tested in isolation.
11
+
12
+ Dependencies: None (pure Python string operations)
13
+
14
+ Exports: extract_type_ignore_bracket, split_comma_list, rule_in_comma_list,
15
+ rule_in_type_ignore_bracket, any_part_in_set, comma_list_has_used_rule,
16
+ type_ignore_bracket_has_used_rule
17
+
18
+ Interfaces: All functions take strings/sets and return strings/bools
19
+
20
+ Implementation: String parsing with early returns for invalid formats
21
+ """
22
+
23
+ TYPE_IGNORE_PREFIX = "type:ignore["
24
+
25
+
26
+ def extract_type_ignore_bracket(suppression_key: str) -> str | None:
27
+ """Extract content from type:ignore[...] format.
28
+
29
+ Args:
30
+ suppression_key: String that may be in type:ignore[rules] format
31
+
32
+ Returns:
33
+ Content between brackets, or None if not valid format
34
+ """
35
+ if not suppression_key.startswith(TYPE_IGNORE_PREFIX):
36
+ return None
37
+ if not suppression_key.endswith("]"):
38
+ return None
39
+ return suppression_key[len(TYPE_IGNORE_PREFIX) : -1]
40
+
41
+
42
+ def split_comma_list(content: str) -> list[str]:
43
+ """Split comma-separated string into stripped parts.
44
+
45
+ Args:
46
+ content: Comma-separated string
47
+
48
+ Returns:
49
+ List of stripped parts, or empty list if no commas
50
+ """
51
+ if "," not in content:
52
+ return []
53
+ return [p.strip() for p in content.split(",")]
54
+
55
+
56
+ def rule_in_comma_list(rule_id: str, suppression_key: str) -> bool:
57
+ """Check if rule_id is in a plain comma-separated list.
58
+
59
+ Args:
60
+ rule_id: Normalized rule ID to find
61
+ suppression_key: String that may contain comma-separated rules
62
+
63
+ Returns:
64
+ True if rule_id is found in the comma-separated parts
65
+ """
66
+ parts = split_comma_list(suppression_key)
67
+ return rule_id in parts
68
+
69
+
70
+ def rule_in_type_ignore_bracket(rule_id: str, suppression_key: str) -> bool:
71
+ """Check if rule_id is in type:ignore[rule1,rule2] format.
72
+
73
+ Args:
74
+ rule_id: Normalized rule ID to find
75
+ suppression_key: String that may be in type:ignore[rules] format
76
+
77
+ Returns:
78
+ True if rule_id is found in the bracket content
79
+ """
80
+ bracket_content = extract_type_ignore_bracket(suppression_key)
81
+ if bracket_content is None:
82
+ return False
83
+ parts = split_comma_list(bracket_content)
84
+ return rule_id in parts
85
+
86
+
87
+ def any_part_in_set(content: str, rule_ids: set[str]) -> bool:
88
+ """Check if any comma-separated part of content is in rule_ids.
89
+
90
+ Args:
91
+ content: Comma-separated string
92
+ rule_ids: Set of rule IDs to check against
93
+
94
+ Returns:
95
+ True if any part is in the set
96
+ """
97
+ parts = split_comma_list(content)
98
+ return any(p in rule_ids for p in parts)
99
+
100
+
101
+ def comma_list_has_used_rule(suppression_key: str, used_rule_ids: set[str]) -> bool:
102
+ """Check if any rule in a comma-separated suppression is used.
103
+
104
+ Args:
105
+ suppression_key: Comma-separated suppression key
106
+ used_rule_ids: Set of rule IDs used in code
107
+
108
+ Returns:
109
+ True if any comma-separated rule is in used_rule_ids
110
+ """
111
+ parts = split_comma_list(suppression_key)
112
+ return any(p in used_rule_ids for p in parts)
113
+
114
+
115
+ def type_ignore_bracket_has_used_rule(suppression_key: str, used_rule_ids: set[str]) -> bool:
116
+ """Check if type:ignore[rules] suppression has any used rule.
117
+
118
+ Args:
119
+ suppression_key: String in type:ignore[rules] format
120
+ used_rule_ids: Set of rule IDs used in code
121
+
122
+ Returns:
123
+ True if bracket content or any comma part is in used_rule_ids
124
+ """
125
+ bracket_content = extract_type_ignore_bracket(suppression_key)
126
+ if bracket_content is None:
127
+ return False
128
+ if bracket_content in used_rule_ids:
129
+ return True
130
+ return any_part_in_set(bracket_content, used_rule_ids)
131
+
132
+
133
+ def is_type_ignore_format_in_suppressions(normalized: str, suppressions: dict[str, str]) -> bool:
134
+ """Check if type:ignore[rule] format is in suppressions.
135
+
136
+ Args:
137
+ normalized: Normalized rule ID
138
+ suppressions: Dict of suppression keys to justifications
139
+
140
+ Returns:
141
+ True if type:ignore[normalized] is in suppressions
142
+ """
143
+ return f"type:ignore[{normalized}]" in suppressions
144
+
145
+
146
+ def rule_matches_suppression(rule_id: str, suppression_key: str, is_type_ignore: bool) -> bool:
147
+ """Check if rule_id matches a suppression key (plain or type:ignore format).
148
+
149
+ Args:
150
+ rule_id: Normalized rule ID to find
151
+ suppression_key: Suppression key to check
152
+ is_type_ignore: True if this is a type:ignore directive
153
+
154
+ Returns:
155
+ True if rule_id is found in the suppression key
156
+ """
157
+ if rule_in_comma_list(rule_id, suppression_key):
158
+ return True
159
+ if is_type_ignore:
160
+ return rule_in_type_ignore_bracket(rule_id, suppression_key)
161
+ return False
162
+
163
+
164
+ def find_rule_in_suppressions(
165
+ normalized: str, suppressions: dict[str, str], is_type_ignore: bool
166
+ ) -> bool:
167
+ """Check if rule appears in any comma-separated suppression entry.
168
+
169
+ Args:
170
+ normalized: Normalized rule ID to find
171
+ suppressions: Dict of suppression keys to justifications
172
+ is_type_ignore: True if this is a type:ignore directive
173
+
174
+ Returns:
175
+ True if rule is found in any suppression's comma list
176
+ """
177
+ return any(
178
+ rule_matches_suppression(normalized, suppression_key, is_type_ignore)
179
+ for suppression_key in suppressions
180
+ )
@@ -0,0 +1,298 @@
1
+ """
2
+ Purpose: Detect test skip patterns without proper justification in source code
3
+
4
+ Scope: pytest.mark.skip, pytest.skip(), it.skip(), describe.skip(), test.skip() pattern detection
5
+
6
+ Overview: Provides TestSkipDetector class that scans Python and JavaScript/TypeScript source
7
+ code for test skip patterns that lack proper justification. For Python, detects bare
8
+ @pytest.mark.skip and pytest.skip() without reason arguments. For JavaScript/TypeScript,
9
+ detects it.skip(), describe.skip(), and test.skip() patterns. Returns list of
10
+ IgnoreDirective objects with line/column positions for violation reporting.
11
+
12
+ Dependencies: re for pattern matching, pathlib for file paths, types module for dataclasses
13
+
14
+ Exports: TestSkipDetector
15
+
16
+ Interfaces: find_skips(code: str, file_path: Path | str | None, language: str) -> list[IgnoreDirective]
17
+
18
+ Implementation: Regex-based line-by-line scanning with language-specific pattern detection
19
+ """
20
+
21
+ import re
22
+ from collections.abc import Callable
23
+ from pathlib import Path
24
+
25
+ from src.core.constants import Language
26
+ from src.linters.lazy_ignores.directive_utils import (
27
+ create_directive_no_rules,
28
+ normalize_path,
29
+ )
30
+ from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
31
+
32
+
33
+ def _is_comment_line(line: str) -> bool:
34
+ """Check if a line is a Python comment.
35
+
36
+ Args:
37
+ line: Line of code to check
38
+
39
+ Returns:
40
+ True if the line is a comment (starts with # after whitespace)
41
+ """
42
+ return line.strip().startswith("#")
43
+
44
+
45
+ def _scan_empty(_line: str, _line_num: int, _file_path: Path) -> list[IgnoreDirective]:
46
+ """No-op scanner for unsupported languages.
47
+
48
+ Args:
49
+ _line: Unused - required for scanner interface
50
+ _line_num: Unused - required for scanner interface
51
+ _file_path: Unused - required for scanner interface
52
+
53
+ Returns:
54
+ Empty list
55
+ """
56
+ return []
57
+
58
+
59
+ def _count_unescaped_triple_quotes(line: str, quote: str) -> int:
60
+ """Count unescaped triple-quote occurrences in a line.
61
+
62
+ Uses regex to find non-escaped triple quotes.
63
+
64
+ Args:
65
+ line: Line to scan
66
+ quote: Triple-quote pattern to count (single or double)
67
+
68
+ Returns:
69
+ Number of unescaped triple-quote occurrences
70
+ """
71
+ escaped_quote = re.escape(quote)
72
+ pattern = re.compile(rf"(?<!\\){escaped_quote}")
73
+ return len(pattern.findall(line))
74
+
75
+
76
+ def _update_docstring_state(line: str, quotes: list[str], state: list[bool]) -> None:
77
+ """Update docstring tracking state based on quotes in line.
78
+
79
+ Args:
80
+ line: Line to analyze
81
+ quotes: List of quote patterns to check
82
+ state: Mutable list tracking in-docstring state for each quote type
83
+ """
84
+ for i, quote in enumerate(quotes):
85
+ if _count_unescaped_triple_quotes(line, quote) % 2 == 1:
86
+ state[i] = not state[i]
87
+
88
+
89
+ def _get_python_scannable_lines(code: str) -> list[tuple[int, str]]:
90
+ """Get Python lines that are not inside docstrings.
91
+
92
+ Args:
93
+ code: Source code to analyze
94
+
95
+ Returns:
96
+ List of (line_number, line_text) tuples for scannable lines
97
+ """
98
+ in_docstring = [False, False] # [triple_double, triple_single]
99
+ quotes = ['"""', "'''"]
100
+ scannable: list[tuple[int, str]] = []
101
+
102
+ for line_num, line in enumerate(code.splitlines(), start=1):
103
+ was_in_docstring = in_docstring[0] or in_docstring[1]
104
+ _update_docstring_state(line, quotes, in_docstring)
105
+ if not was_in_docstring:
106
+ scannable.append((line_num, line))
107
+
108
+ return scannable
109
+
110
+
111
+ class TestSkipDetector:
112
+ """Detects test skip patterns without proper justification."""
113
+
114
+ # Python patterns - violations are skips WITHOUT a reason argument
115
+ # These patterns match skips that should be flagged
116
+ # Must appear at start of line (after optional whitespace), not in comments
117
+ PYTHON_VIOLATION_PATTERNS: dict[IgnoreType, re.Pattern[str]] = {
118
+ # Matches @pytest.mark.skip or @pytest.mark.skip() without reason=
119
+ # Requires @ at start of line (after whitespace) to avoid matching in comments
120
+ IgnoreType.PYTEST_SKIP: re.compile(
121
+ r"^\s*@pytest\.mark\.skip(?:\s*\(\s*\))?(?!\s*\(.*reason\s*=)",
122
+ ),
123
+ }
124
+
125
+ # Python patterns that indicate a properly justified skip (no violation)
126
+ PYTHON_ALLOWED_PATTERNS: list[re.Pattern[str]] = [
127
+ # @pytest.mark.skip(reason="...")
128
+ re.compile(r"^\s*@pytest\.mark\.skip\s*\(\s*reason\s*="),
129
+ # @pytest.mark.skipif(..., reason="...")
130
+ re.compile(r"^\s*@pytest\.mark\.skipif\s*\(.*reason\s*="),
131
+ # pytest.skip("reason") - positional reason argument
132
+ re.compile(r"pytest\.skip\s*\(\s*['\"]"),
133
+ # pytest.skip(reason="...")
134
+ re.compile(r"pytest\.skip\s*\(\s*reason\s*="),
135
+ ]
136
+
137
+ # Pattern for bare pytest.skip() - needs special handling
138
+ PYTEST_SKIP_CALL_PATTERN = re.compile(
139
+ r"pytest\.skip\s*\(\s*\)",
140
+ )
141
+
142
+ # JavaScript/TypeScript patterns - these are always violations
143
+ # The proper way is to remove or fix the test, not skip it
144
+ JS_VIOLATION_PATTERNS: dict[IgnoreType, re.Pattern[str]] = {
145
+ IgnoreType.JEST_SKIP: re.compile(
146
+ r"(?:it|test)\.skip\s*\(",
147
+ ),
148
+ IgnoreType.MOCHA_SKIP: re.compile(
149
+ r"describe\.skip\s*\(",
150
+ ),
151
+ }
152
+
153
+ def find_skips(
154
+ self,
155
+ code: str,
156
+ file_path: Path | str | None = None,
157
+ language: str | Language = Language.PYTHON,
158
+ ) -> list[IgnoreDirective]:
159
+ """Find test skip patterns without justification.
160
+
161
+ Tracks docstring state across lines to avoid false positives from
162
+ patterns mentioned in documentation.
163
+
164
+ Args:
165
+ code: Source code to scan
166
+ file_path: Optional path to the source file (Path or string)
167
+ language: Language of source code (Language enum or string)
168
+
169
+ Returns:
170
+ List of IgnoreDirective objects for detected unjustified skips
171
+ """
172
+ effective_path = normalize_path(file_path)
173
+ lang = Language(language) if isinstance(language, str) else language
174
+ scanner = self._get_line_scanner(lang)
175
+
176
+ scannable_lines = self._get_scannable_lines(code, lang)
177
+ directives: list[IgnoreDirective] = []
178
+ for line_num, line in scannable_lines:
179
+ directives.extend(scanner(line, line_num, effective_path))
180
+ return directives
181
+
182
+ def _get_scannable_lines(self, code: str, lang: Language) -> list[tuple[int, str]]:
183
+ """Get lines that are not inside docstrings (Python) or all lines (other).
184
+
185
+ Args:
186
+ code: Source code to analyze
187
+ lang: Programming language
188
+
189
+ Returns:
190
+ List of (line_number, line_text) tuples for scannable lines
191
+ """
192
+ if lang != Language.PYTHON:
193
+ return list(enumerate(code.splitlines(), start=1))
194
+ return _get_python_scannable_lines(code)
195
+
196
+ def _get_line_scanner(
197
+ self, lang: Language
198
+ ) -> Callable[[str, int, Path], list[IgnoreDirective]]:
199
+ """Get the appropriate line scanner for a language.
200
+
201
+ Args:
202
+ lang: Programming language
203
+
204
+ Returns:
205
+ Line scanner function for the language
206
+ """
207
+ if lang == Language.PYTHON:
208
+ return self._scan_python_line
209
+ if lang in (Language.JAVASCRIPT, Language.TYPESCRIPT):
210
+ return self._scan_js_line
211
+ return _scan_empty
212
+
213
+ def _scan_python_line(self, line: str, line_num: int, file_path: Path) -> list[IgnoreDirective]:
214
+ """Scan a Python line for unjustified skip patterns.
215
+
216
+ Args:
217
+ line: Line of code to scan
218
+ line_num: 1-indexed line number
219
+ file_path: Path to the source file
220
+
221
+ Returns:
222
+ List of IgnoreDirective objects found on this line
223
+ """
224
+ if _is_comment_line(line) or self._is_justified_python_skip(line):
225
+ return []
226
+
227
+ found = self._find_decorator_violations(line, line_num, file_path)
228
+ found.extend(self._find_skip_call_violations(line, line_num, file_path))
229
+ return found
230
+
231
+ def _find_decorator_violations(
232
+ self, line: str, line_num: int, file_path: Path
233
+ ) -> list[IgnoreDirective]:
234
+ """Find @pytest.mark.skip decorator violations.
235
+
236
+ Args:
237
+ line: Line of code to scan
238
+ line_num: 1-indexed line number
239
+ file_path: Path to source file
240
+
241
+ Returns:
242
+ List of IgnoreDirective for decorator violations
243
+ """
244
+ found: list[IgnoreDirective] = []
245
+ for ignore_type, pattern in self.PYTHON_VIOLATION_PATTERNS.items():
246
+ match = pattern.search(line)
247
+ if match:
248
+ found.append(create_directive_no_rules(match, ignore_type, line_num, file_path))
249
+ return found
250
+
251
+ def _find_skip_call_violations(
252
+ self, line: str, line_num: int, file_path: Path
253
+ ) -> list[IgnoreDirective]:
254
+ """Find bare pytest.skip() call violations.
255
+
256
+ Args:
257
+ line: Line of code to scan
258
+ line_num: 1-indexed line number
259
+ file_path: Path to source file
260
+
261
+ Returns:
262
+ List of IgnoreDirective for skip call violations
263
+ """
264
+ match = self.PYTEST_SKIP_CALL_PATTERN.search(line)
265
+ if match:
266
+ return [create_directive_no_rules(match, IgnoreType.PYTEST_SKIP, line_num, file_path)]
267
+ return []
268
+
269
+ def _is_justified_python_skip(self, line: str) -> bool:
270
+ """Check if a Python line contains a justified skip.
271
+
272
+ Args:
273
+ line: Line of code to check
274
+
275
+ Returns:
276
+ True if the line has a skip with proper justification
277
+ """
278
+ return any(pattern.search(line) for pattern in self.PYTHON_ALLOWED_PATTERNS)
279
+
280
+ def _scan_js_line(self, line: str, line_num: int, file_path: Path) -> list[IgnoreDirective]:
281
+ """Scan a JavaScript/TypeScript line for skip patterns.
282
+
283
+ Args:
284
+ line: Line of code to scan
285
+ line_num: 1-indexed line number
286
+ file_path: Path to the source file
287
+
288
+ Returns:
289
+ List of IgnoreDirective objects found on this line
290
+ """
291
+ found: list[IgnoreDirective] = []
292
+
293
+ for ignore_type, pattern in self.JS_VIOLATION_PATTERNS.items():
294
+ match = pattern.search(line)
295
+ if match:
296
+ found.append(create_directive_no_rules(match, ignore_type, line_num, file_path))
297
+
298
+ return found
@@ -0,0 +1,69 @@
1
+ """
2
+ Purpose: Type definitions for lazy-ignores linter
3
+
4
+ Scope: Data structures for ignore directives and suppression entries
5
+
6
+ Overview: Defines core types for the lazy-ignores linter including IgnoreType enum for
7
+ categorizing different suppression patterns, IgnoreDirective dataclass for representing
8
+ detected ignores in code, and SuppressionEntry dataclass for representing declared
9
+ suppressions in file headers. Supports Python (noqa, type:ignore, pylint, nosec),
10
+ TypeScript (@ts-ignore, eslint-disable), thai-lint (thailint:ignore), and test skip
11
+ patterns (pytest.mark.skip, it.skip, describe.skip).
12
+
13
+ Dependencies: dataclasses, enum, pathlib
14
+
15
+ Exports: IgnoreType, IgnoreDirective, SuppressionEntry
16
+
17
+ Interfaces: Frozen dataclasses for immutable ignore representation
18
+
19
+ Implementation: Enum-based categorization with frozen dataclasses for thread safety
20
+ """
21
+
22
+ from dataclasses import dataclass
23
+ from enum import Enum
24
+ from pathlib import Path
25
+
26
+
27
+ class IgnoreType(Enum):
28
+ """Type of linting ignore directive."""
29
+
30
+ NOQA = "noqa"
31
+ TYPE_IGNORE = "type:ignore"
32
+ PYLINT_DISABLE = "pylint:disable"
33
+ NOSEC = "nosec"
34
+ TS_IGNORE = "ts-ignore"
35
+ TS_NOCHECK = "ts-nocheck"
36
+ TS_EXPECT_ERROR = "ts-expect-error"
37
+ ESLINT_DISABLE = "eslint-disable"
38
+ THAILINT_IGNORE = "thailint:ignore"
39
+ THAILINT_IGNORE_FILE = "thailint:ignore-file"
40
+ THAILINT_IGNORE_NEXT = "thailint:ignore-next-line"
41
+ THAILINT_IGNORE_BLOCK = "thailint:ignore-start"
42
+ # DRY ignore patterns
43
+ DRY_IGNORE_BLOCK = "dry:ignore-block"
44
+ # Test skip patterns
45
+ PYTEST_SKIP = "pytest:skip"
46
+ PYTEST_SKIPIF = "pytest:skipif"
47
+ JEST_SKIP = "jest:skip"
48
+ MOCHA_SKIP = "mocha:skip"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class IgnoreDirective:
53
+ """Represents a linting ignore found in code."""
54
+
55
+ ignore_type: IgnoreType
56
+ rule_ids: tuple[str, ...] # Can have multiple: noqa: PLR0912, PLR0915
57
+ line: int
58
+ column: int
59
+ raw_text: str # Original comment text
60
+ file_path: Path
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class SuppressionEntry:
65
+ """Represents a suppression declared in file header."""
66
+
67
+ rule_id: str # Normalized rule ID
68
+ justification: str
69
+ raw_text: str # Original header line
@@ -0,0 +1,146 @@
1
+ """
2
+ Purpose: Detect TypeScript/JavaScript linting ignore directives in source code
3
+
4
+ Scope: @ts-ignore, @ts-nocheck, @ts-expect-error, eslint-disable pattern detection
5
+
6
+ Overview: Provides TypeScriptIgnoreDetector class that scans TypeScript and JavaScript source
7
+ code for common linting ignore patterns. Detects TypeScript-specific patterns (@ts-ignore,
8
+ @ts-nocheck, @ts-expect-error) and ESLint patterns (eslint-disable-next-line, eslint-disable
9
+ block comments, eslint-disable-line). Handles both single-line (//) and block (/* */)
10
+ comment styles. Returns list of IgnoreDirective objects with line/column positions for
11
+ violation reporting.
12
+
13
+ Dependencies: re for pattern matching, pathlib for file paths, types module for dataclasses
14
+
15
+ Exports: TypeScriptIgnoreDetector
16
+
17
+ Interfaces: find_ignores(code: str, file_path: Path | str | None) -> list[IgnoreDirective]
18
+
19
+ Implementation: Regex-based line-by-line scanning with pattern-specific rule ID extraction
20
+ """
21
+
22
+ import re
23
+ from pathlib import Path
24
+
25
+ from src.linters.lazy_ignores.directive_utils import create_directive, normalize_path
26
+ from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
27
+
28
+
29
+ class TypeScriptIgnoreDetector:
30
+ """Detects TypeScript/JavaScript linting ignore directives in source code."""
31
+
32
+ # Regex patterns for each ignore type
33
+ # Single-line comment patterns (//)
34
+ SINGLE_LINE_PATTERNS: dict[IgnoreType, re.Pattern[str]] = {
35
+ IgnoreType.TS_IGNORE: re.compile(
36
+ r"//\s*@ts-ignore(?:\s|$)",
37
+ ),
38
+ IgnoreType.TS_NOCHECK: re.compile(
39
+ r"//\s*@ts-nocheck(?:\s|$)",
40
+ ),
41
+ IgnoreType.TS_EXPECT_ERROR: re.compile(
42
+ r"//\s*@ts-expect-error(?:\s|$)",
43
+ ),
44
+ IgnoreType.THAILINT_IGNORE: re.compile(
45
+ r"//\s*thailint:\s*ignore(?!-)(?:\[([^\]]+)\])?",
46
+ re.IGNORECASE,
47
+ ),
48
+ IgnoreType.THAILINT_IGNORE_FILE: re.compile(
49
+ r"//\s*thailint:\s*ignore-file(?:\[([^\]]+)\])?",
50
+ re.IGNORECASE,
51
+ ),
52
+ }
53
+
54
+ # ESLint patterns (can be single-line or inline)
55
+ ESLINT_PATTERNS: dict[str, re.Pattern[str]] = {
56
+ "next-line": re.compile(
57
+ r"//\s*eslint-disable-next-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
58
+ ),
59
+ "inline": re.compile(
60
+ r"//\s*eslint-disable-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
61
+ ),
62
+ "block-start": re.compile(
63
+ r"/\*\s*eslint-disable(?:\s+([a-zA-Z0-9\-/,\s]+))?\s*\*/",
64
+ ),
65
+ }
66
+
67
+ def find_ignores(self, code: str, file_path: Path | str | None = None) -> list[IgnoreDirective]:
68
+ """Find all TypeScript/JavaScript ignore directives in code.
69
+
70
+ Args:
71
+ code: TypeScript/JavaScript source code to scan
72
+ file_path: Optional path to the source file (Path or string)
73
+
74
+ Returns:
75
+ List of IgnoreDirective objects for each detected ignore pattern
76
+ """
77
+ directives: list[IgnoreDirective] = []
78
+ effective_path = normalize_path(file_path)
79
+
80
+ for line_num, line in enumerate(code.splitlines(), start=1):
81
+ directives.extend(self._scan_line(line, line_num, effective_path))
82
+
83
+ return directives
84
+
85
+ def _scan_line(self, line: str, line_num: int, file_path: Path) -> list[IgnoreDirective]:
86
+ """Scan a single line for ignore patterns.
87
+
88
+ Args:
89
+ line: Line of code to scan
90
+ line_num: 1-indexed line number
91
+ file_path: Path to the source file
92
+
93
+ Returns:
94
+ List of IgnoreDirective objects found on this line
95
+ """
96
+ found: list[IgnoreDirective] = []
97
+
98
+ # Check TypeScript-specific patterns
99
+ found.extend(self._scan_typescript_patterns(line, line_num, file_path))
100
+
101
+ # Check ESLint patterns
102
+ found.extend(self._scan_eslint_patterns(line, line_num, file_path))
103
+
104
+ return found
105
+
106
+ def _scan_typescript_patterns(
107
+ self, line: str, line_num: int, file_path: Path
108
+ ) -> list[IgnoreDirective]:
109
+ """Scan for TypeScript-specific ignore patterns.
110
+
111
+ Args:
112
+ line: Line of code to scan
113
+ line_num: 1-indexed line number
114
+ file_path: Path to the source file
115
+
116
+ Returns:
117
+ List of IgnoreDirective objects for TypeScript patterns
118
+ """
119
+ found: list[IgnoreDirective] = []
120
+ for ignore_type, pattern in self.SINGLE_LINE_PATTERNS.items():
121
+ match = pattern.search(line)
122
+ if match:
123
+ found.append(create_directive(match, ignore_type, line_num, file_path))
124
+ return found
125
+
126
+ def _scan_eslint_patterns(
127
+ self, line: str, line_num: int, file_path: Path
128
+ ) -> list[IgnoreDirective]:
129
+ """Scan for ESLint disable patterns.
130
+
131
+ Args:
132
+ line: Line of code to scan
133
+ line_num: 1-indexed line number
134
+ file_path: Path to the source file
135
+
136
+ Returns:
137
+ List of IgnoreDirective objects for ESLint patterns
138
+ """
139
+ found: list[IgnoreDirective] = []
140
+ for pattern in self.ESLINT_PATTERNS.values():
141
+ match = pattern.search(line)
142
+ if match:
143
+ found.append(
144
+ create_directive(match, IgnoreType.ESLINT_DISABLE, line_num, file_path)
145
+ )
146
+ return found