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.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +4 -0
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|