thailint 0.12.0__py3-none-any.whl → 0.13.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 (121) 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 +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +4 -0
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli_main.py +3 -0
  14. src/config.py +2 -1
  15. src/core/base.py +3 -2
  16. src/core/cli_utils.py +3 -1
  17. src/core/config_parser.py +5 -2
  18. src/core/constants.py +54 -0
  19. src/core/linter_utils.py +4 -0
  20. src/core/rule_discovery.py +5 -1
  21. src/core/violation_builder.py +3 -0
  22. src/linter_config/directive_markers.py +109 -0
  23. src/linter_config/ignore.py +225 -383
  24. src/linter_config/pattern_utils.py +65 -0
  25. src/linter_config/rule_matcher.py +89 -0
  26. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  27. src/linters/collection_pipeline/ast_utils.py +40 -0
  28. src/linters/collection_pipeline/config.py +12 -0
  29. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  30. src/linters/collection_pipeline/detector.py +262 -32
  31. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  32. src/linters/collection_pipeline/linter.py +18 -35
  33. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  34. src/linters/dry/base_token_analyzer.py +16 -9
  35. src/linters/dry/block_filter.py +7 -4
  36. src/linters/dry/cache.py +7 -2
  37. src/linters/dry/config.py +7 -1
  38. src/linters/dry/constant_matcher.py +34 -25
  39. src/linters/dry/file_analyzer.py +4 -2
  40. src/linters/dry/inline_ignore.py +7 -16
  41. src/linters/dry/linter.py +48 -25
  42. src/linters/dry/python_analyzer.py +18 -10
  43. src/linters/dry/python_constant_extractor.py +51 -52
  44. src/linters/dry/single_statement_detector.py +14 -12
  45. src/linters/dry/token_hasher.py +115 -115
  46. src/linters/dry/typescript_analyzer.py +11 -6
  47. src/linters/dry/typescript_constant_extractor.py +4 -0
  48. src/linters/dry/typescript_statement_detector.py +208 -208
  49. src/linters/dry/typescript_value_extractor.py +3 -0
  50. src/linters/dry/violation_filter.py +1 -4
  51. src/linters/dry/violation_generator.py +1 -4
  52. src/linters/file_header/atemporal_detector.py +4 -0
  53. src/linters/file_header/base_parser.py +4 -0
  54. src/linters/file_header/bash_parser.py +4 -0
  55. src/linters/file_header/field_validator.py +5 -8
  56. src/linters/file_header/linter.py +19 -12
  57. src/linters/file_header/markdown_parser.py +6 -0
  58. src/linters/file_placement/config_loader.py +3 -1
  59. src/linters/file_placement/linter.py +22 -8
  60. src/linters/file_placement/pattern_matcher.py +21 -4
  61. src/linters/file_placement/pattern_validator.py +21 -7
  62. src/linters/file_placement/rule_checker.py +2 -2
  63. src/linters/lazy_ignores/__init__.py +43 -0
  64. src/linters/lazy_ignores/config.py +66 -0
  65. src/linters/lazy_ignores/directive_utils.py +121 -0
  66. src/linters/lazy_ignores/header_parser.py +177 -0
  67. src/linters/lazy_ignores/linter.py +158 -0
  68. src/linters/lazy_ignores/matcher.py +135 -0
  69. src/linters/lazy_ignores/python_analyzer.py +201 -0
  70. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  71. src/linters/lazy_ignores/skip_detector.py +298 -0
  72. src/linters/lazy_ignores/types.py +67 -0
  73. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  74. src/linters/lazy_ignores/violation_builder.py +131 -0
  75. src/linters/lbyl/__init__.py +29 -0
  76. src/linters/lbyl/config.py +63 -0
  77. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  78. src/linters/lbyl/pattern_detectors/base.py +46 -0
  79. src/linters/magic_numbers/context_analyzer.py +227 -229
  80. src/linters/magic_numbers/linter.py +20 -15
  81. src/linters/magic_numbers/python_analyzer.py +4 -16
  82. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  83. src/linters/method_property/config.py +4 -0
  84. src/linters/method_property/linter.py +5 -4
  85. src/linters/method_property/python_analyzer.py +5 -4
  86. src/linters/method_property/violation_builder.py +3 -0
  87. src/linters/nesting/typescript_analyzer.py +6 -12
  88. src/linters/nesting/typescript_function_extractor.py +0 -4
  89. src/linters/print_statements/linter.py +6 -4
  90. src/linters/print_statements/python_analyzer.py +85 -81
  91. src/linters/print_statements/typescript_analyzer.py +6 -15
  92. src/linters/srp/heuristics.py +4 -4
  93. src/linters/srp/linter.py +12 -12
  94. src/linters/srp/violation_builder.py +0 -4
  95. src/linters/stateless_class/linter.py +30 -36
  96. src/linters/stateless_class/python_analyzer.py +11 -20
  97. src/linters/stringly_typed/config.py +4 -5
  98. src/linters/stringly_typed/context_filter.py +410 -410
  99. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  100. src/linters/stringly_typed/linter.py +48 -16
  101. src/linters/stringly_typed/python/analyzer.py +5 -1
  102. src/linters/stringly_typed/python/call_tracker.py +8 -5
  103. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  104. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  105. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  106. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  107. src/linters/stringly_typed/python/validation_detector.py +3 -0
  108. src/linters/stringly_typed/storage.py +14 -14
  109. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  110. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  111. src/linters/stringly_typed/violation_generator.py +288 -259
  112. src/orchestrator/core.py +13 -4
  113. src/templates/thailint_config_template.yaml +166 -0
  114. src/utils/project_root.py +3 -0
  115. thailint-0.13.0.dist-info/METADATA +184 -0
  116. thailint-0.13.0.dist-info/RECORD +189 -0
  117. thailint-0.12.0.dist-info/METADATA +0 -1667
  118. thailint-0.12.0.dist-info/RECORD +0 -164
  119. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  120. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  121. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,67 @@
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
+ # Test skip patterns
43
+ PYTEST_SKIP = "pytest:skip"
44
+ PYTEST_SKIPIF = "pytest:skipif"
45
+ JEST_SKIP = "jest:skip"
46
+ MOCHA_SKIP = "mocha:skip"
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class IgnoreDirective:
51
+ """Represents a linting ignore found in code."""
52
+
53
+ ignore_type: IgnoreType
54
+ rule_ids: tuple[str, ...] # Can have multiple: noqa: PLR0912, PLR0915
55
+ line: int
56
+ column: int
57
+ raw_text: str # Original comment text
58
+ file_path: Path
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class SuppressionEntry:
63
+ """Represents a suppression declared in file header."""
64
+
65
+ rule_id: str # Normalized rule ID
66
+ justification: str
67
+ 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
@@ -0,0 +1,131 @@
1
+ """
2
+ Purpose: Build agent-friendly violation messages for lazy-ignores linter
3
+
4
+ Scope: Violation construction for unjustified ignores and orphaned header suppressions
5
+
6
+ Overview: Provides functions to construct Violation objects with AI-agent-friendly error
7
+ messages. Messages include explicit guidance for adding Suppressions section entries to
8
+ file headers and emphasize the requirement for human approval before adding suppressions.
9
+ Designed to help AI coding assistants understand the proper workflow for handling linting
10
+ suppressions rather than blindly adding ignore directives.
11
+
12
+ Dependencies: src.core.types for Violation dataclass
13
+
14
+ Exports: build_unjustified_violation, build_orphaned_violation
15
+
16
+ Interfaces: Two builder functions returning Violation objects
17
+
18
+ Implementation: Template-based message construction with rule ID formatting
19
+ """
20
+
21
+ from src.core.types import Severity, Violation
22
+
23
+
24
+ def build_unjustified_violation(
25
+ file_path: str,
26
+ line: int,
27
+ column: int,
28
+ rule_id: str,
29
+ raw_text: str,
30
+ ) -> Violation:
31
+ """Create violation for an ignore directive without header justification.
32
+
33
+ Args:
34
+ file_path: Path to the file containing the violation.
35
+ line: Line number where the ignore was found (1-indexed).
36
+ column: Column number where the ignore starts (0-indexed).
37
+ rule_id: The rule ID(s) being suppressed (e.g., "PLR0912").
38
+ raw_text: The raw ignore directive text found in code.
39
+
40
+ Returns:
41
+ Violation object with agent-friendly guidance message.
42
+ """
43
+ message = (
44
+ f"Unjustified suppression found: {raw_text} "
45
+ f"(ASK PERMISSION before adding Suppressions header)"
46
+ )
47
+
48
+ suggestion = _build_unjustified_suggestion(rule_id)
49
+
50
+ return Violation(
51
+ rule_id="lazy-ignores.unjustified",
52
+ file_path=file_path,
53
+ line=line,
54
+ column=column,
55
+ message=message,
56
+ severity=Severity.ERROR,
57
+ suggestion=suggestion,
58
+ )
59
+
60
+
61
+ def _build_unjustified_suggestion(rule_id: str) -> str:
62
+ """Build the suggestion text for unjustified violations.
63
+
64
+ Args:
65
+ rule_id: The rule ID(s) being suppressed.
66
+
67
+ Returns:
68
+ Formatted suggestion string with header instructions.
69
+ """
70
+ # Handle multiple rules (e.g., "PLR0912, PLR0915")
71
+ rule_ids = [r.strip() for r in rule_id.split(",")]
72
+
73
+ suppression_entries = "\n".join(f" {rid}: [Your justification here]" for rid in rule_ids)
74
+
75
+ return f"""To fix, add an entry to the file header Suppressions section:
76
+
77
+ Suppressions:
78
+ {suppression_entries}
79
+
80
+ IMPORTANT: Adding suppressions requires human approval.
81
+ Do not add this entry without explicit permission from a human reviewer.
82
+ Ask first, then add if approved."""
83
+
84
+
85
+ def build_orphaned_violation(
86
+ file_path: str,
87
+ header_line: int,
88
+ rule_id: str,
89
+ justification: str,
90
+ ) -> Violation:
91
+ """Create violation for a header entry without matching code ignore.
92
+
93
+ Args:
94
+ file_path: Path to the file containing the orphaned entry.
95
+ header_line: Line number of the suppression in the header (1-indexed).
96
+ rule_id: The orphaned rule ID from the header.
97
+ justification: The justification text from the header.
98
+
99
+ Returns:
100
+ Violation object suggesting removal of the orphaned entry.
101
+ """
102
+ message = f"Orphaned suppression in header: {rule_id}: {justification}"
103
+
104
+ suggestion = _build_orphaned_suggestion(rule_id)
105
+
106
+ return Violation(
107
+ rule_id="lazy-ignores.orphaned",
108
+ file_path=file_path,
109
+ line=header_line,
110
+ column=0,
111
+ message=message,
112
+ severity=Severity.ERROR,
113
+ suggestion=suggestion,
114
+ )
115
+
116
+
117
+ def _build_orphaned_suggestion(rule_id: str) -> str:
118
+ """Build the suggestion text for orphaned violations.
119
+
120
+ Args:
121
+ rule_id: The orphaned rule ID.
122
+
123
+ Returns:
124
+ Formatted suggestion string with removal instructions.
125
+ """
126
+ return f"""This rule is declared in the Suppressions section but no matching
127
+ ignore directive was found in the code.
128
+
129
+ Either:
130
+ 1. Remove the entry for {rule_id} from the Suppressions section if the ignore was removed from code
131
+ 2. Add the ignore directive if it's missing from the code"""
@@ -0,0 +1,29 @@
1
+ """
2
+ Purpose: LBYL (Look Before You Leap) linter package exports
3
+
4
+ Scope: Detect LBYL anti-patterns in Python code and suggest EAFP alternatives
5
+
6
+ Overview: Package providing LBYL pattern detection for Python code. Identifies common
7
+ anti-patterns where explicit checks are performed before operations (e.g., if key in
8
+ dict before dict[key]) and suggests EAFP (Easier to Ask Forgiveness than Permission)
9
+ alternatives using try/except blocks. Supports 8 pattern types including dict key
10
+ checking, hasattr, isinstance, file exists, length checks, None checks, string
11
+ validation, and division safety checks.
12
+
13
+ Dependencies: ast module for Python parsing, src.core for base classes
14
+
15
+ Exports: LBYLConfig, LBYLPattern, BaseLBYLDetector
16
+
17
+ Interfaces: LBYLConfig.from_dict() for YAML configuration loading
18
+
19
+ Implementation: AST-based pattern detection with configurable pattern toggles
20
+ """
21
+
22
+ from .config import LBYLConfig
23
+ from .pattern_detectors.base import BaseLBYLDetector, LBYLPattern
24
+
25
+ __all__ = [
26
+ "LBYLConfig",
27
+ "LBYLPattern",
28
+ "BaseLBYLDetector",
29
+ ]