thailint 0.5.0__py3-none-any.whl → 0.15.3__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/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -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 +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -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 +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/linter_config/ignore.py
CHANGED
|
@@ -11,465 +11,324 @@ Overview: Implements a sophisticated ignore directive system that allows develop
|
|
|
11
11
|
Method level supports ignore-next-line directives placed before functions. Line level enables
|
|
12
12
|
inline ignore comments at the end of code lines. All levels support rule-specific ignores
|
|
13
13
|
using bracket syntax [rule-id] and wildcard rule matching (literals.* matches literals.magic-number).
|
|
14
|
-
The should_ignore_violation() method provides unified checking across all levels, integrating
|
|
15
|
-
with the violation reporting system to filter out suppressed violations before displaying
|
|
16
|
-
results to users.
|
|
17
14
|
|
|
18
|
-
Dependencies:
|
|
19
|
-
pathlib for file operations, Violation type for violation checking, yaml for config loading
|
|
15
|
+
Dependencies: pathlib, yaml, rule_matcher module, directive_markers module, pattern_utils module
|
|
20
16
|
|
|
21
|
-
Exports: IgnoreDirectiveParser class
|
|
17
|
+
Exports: IgnoreDirectiveParser class, get_ignore_parser, clear_ignore_parser_cache
|
|
22
18
|
|
|
23
|
-
Interfaces: is_ignored(file_path
|
|
24
|
-
|
|
25
|
-
has_line_ignore(code: str, line_num: int, rule_id: str | None) -> bool for line-level,
|
|
26
|
-
should_ignore_violation(violation: Violation, file_content: str) -> bool for unified checking
|
|
19
|
+
Interfaces: is_ignored(file_path) -> bool, has_file_ignore(file_path, rule_id) -> bool,
|
|
20
|
+
has_line_ignore(code, line_num, rule_id) -> bool, should_ignore_violation(violation, content) -> bool
|
|
27
21
|
|
|
28
|
-
Implementation:
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
Implementation: Modular design with extracted pure functions for pattern matching and marker detection
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- global-statement: Module-level singleton pattern for parser caching (performance optimization)
|
|
31
26
|
"""
|
|
32
27
|
|
|
33
|
-
import
|
|
28
|
+
import logging
|
|
34
29
|
import re
|
|
30
|
+
from contextlib import suppress
|
|
35
31
|
from pathlib import Path
|
|
36
32
|
from typing import TYPE_CHECKING
|
|
37
33
|
|
|
38
34
|
import yaml
|
|
39
35
|
|
|
36
|
+
from src.core.constants import HEADER_SCAN_LINES
|
|
37
|
+
from src.linter_config.directive_markers import (
|
|
38
|
+
check_general_ignore,
|
|
39
|
+
has_ignore_directive_marker,
|
|
40
|
+
has_ignore_end_marker,
|
|
41
|
+
has_ignore_next_line_marker,
|
|
42
|
+
has_ignore_start_marker,
|
|
43
|
+
has_line_ignore_marker,
|
|
44
|
+
)
|
|
45
|
+
from src.linter_config.pattern_utils import extract_patterns_from_content, matches_pattern
|
|
46
|
+
from src.linter_config.rule_matcher import (
|
|
47
|
+
check_bracket_rules,
|
|
48
|
+
check_space_separated_rules,
|
|
49
|
+
rules_match_violation,
|
|
50
|
+
)
|
|
51
|
+
|
|
40
52
|
if TYPE_CHECKING:
|
|
41
53
|
from src.core.types import Violation
|
|
42
54
|
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
43
56
|
|
|
44
|
-
class IgnoreDirectiveParser:
|
|
45
|
-
"""Parse and check ignore directives at all 5 levels.
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"""
|
|
58
|
+
class IgnoreDirectiveParser:
|
|
59
|
+
"""Parse and check ignore directives at all 5 levels."""
|
|
50
60
|
|
|
51
61
|
def __init__(self, project_root: Path | None = None):
|
|
52
|
-
"""Initialize parser.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
project_root: Root directory of the project. Defaults to current directory.
|
|
56
|
-
"""
|
|
62
|
+
"""Initialize parser with project root directory."""
|
|
57
63
|
self.project_root = project_root or Path.cwd()
|
|
58
|
-
self.repo_patterns = self.
|
|
59
|
-
|
|
60
|
-
def _load_repo_ignores(self) -> list[str]:
|
|
61
|
-
"""Load global ignore patterns from .thailintignore or .thailint.yaml."""
|
|
62
|
-
# First, try to load from .thailintignore (gitignore-style)
|
|
63
|
-
thailintignore = self.project_root / ".thailintignore"
|
|
64
|
-
if thailintignore.exists():
|
|
65
|
-
return self._parse_thailintignore_file(thailintignore)
|
|
66
|
-
|
|
67
|
-
# Fall back to .thailint.yaml
|
|
68
|
-
config_file = self.project_root / ".thailint.yaml"
|
|
69
|
-
if config_file.exists():
|
|
70
|
-
return self._parse_config_file(config_file)
|
|
71
|
-
|
|
72
|
-
return []
|
|
73
|
-
|
|
74
|
-
def _parse_thailintignore_file(self, ignore_file: Path) -> list[str]:
|
|
75
|
-
"""Parse .thailintignore file (gitignore-style).
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
ignore_file: Path to .thailintignore file
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
List of ignore patterns
|
|
82
|
-
"""
|
|
83
|
-
try:
|
|
84
|
-
content = ignore_file.read_text(encoding="utf-8")
|
|
85
|
-
patterns = []
|
|
86
|
-
for line in content.splitlines():
|
|
87
|
-
line = line.strip()
|
|
88
|
-
# Skip empty lines and comments
|
|
89
|
-
if line and not line.startswith("#"):
|
|
90
|
-
patterns.append(line)
|
|
91
|
-
return patterns
|
|
92
|
-
except (OSError, UnicodeDecodeError):
|
|
93
|
-
return []
|
|
94
|
-
|
|
95
|
-
def _parse_config_file(self, config_file: Path) -> list[str]:
|
|
96
|
-
"""Parse YAML config file and extract ignore patterns."""
|
|
97
|
-
try:
|
|
98
|
-
config = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
|
99
|
-
return self._extract_ignore_patterns(config)
|
|
100
|
-
except (yaml.YAMLError, OSError, UnicodeDecodeError):
|
|
101
|
-
return []
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def _extract_ignore_patterns(config: dict | None) -> list[str]:
|
|
105
|
-
"""Extract ignore patterns from config dict."""
|
|
106
|
-
if not config or not isinstance(config, dict):
|
|
107
|
-
return []
|
|
108
|
-
|
|
109
|
-
ignore_patterns = config.get("ignore", [])
|
|
110
|
-
if isinstance(ignore_patterns, list):
|
|
111
|
-
return [str(pattern) for pattern in ignore_patterns]
|
|
112
|
-
return []
|
|
64
|
+
self.repo_patterns = _load_repo_ignores(self.project_root)
|
|
65
|
+
self._ignore_cache: dict[str, bool] = {}
|
|
113
66
|
|
|
114
67
|
def is_ignored(self, file_path: Path) -> bool:
|
|
115
|
-
"""Check if file matches repository-level ignore patterns.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
True if file should be ignored.
|
|
122
|
-
"""
|
|
123
|
-
# Convert to string relative to project root if possible
|
|
68
|
+
"""Check if file matches repository-level ignore patterns (cached)."""
|
|
69
|
+
path_str = str(file_path)
|
|
70
|
+
with suppress(KeyError):
|
|
71
|
+
return self._ignore_cache[path_str]
|
|
124
72
|
try:
|
|
125
|
-
|
|
126
|
-
path_str = str(relative_path)
|
|
73
|
+
check_path = str(file_path.relative_to(self.project_root))
|
|
127
74
|
except ValueError:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if self._matches_pattern(path_str, pattern):
|
|
133
|
-
return True
|
|
134
|
-
return False
|
|
135
|
-
|
|
136
|
-
def _matches_pattern(self, path: str, pattern: str) -> bool:
|
|
137
|
-
"""Check if path matches gitignore-style pattern.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
path: File path to check.
|
|
141
|
-
pattern: Gitignore-style pattern.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
True if path matches pattern.
|
|
145
|
-
"""
|
|
146
|
-
# Handle directory patterns (trailing /)
|
|
147
|
-
if pattern.endswith("/"):
|
|
148
|
-
# Match directory and all its contents
|
|
149
|
-
dir_pattern = pattern.rstrip("/")
|
|
150
|
-
# Check if path starts with the directory
|
|
151
|
-
path_parts = Path(path).parts
|
|
152
|
-
if dir_pattern in path_parts:
|
|
153
|
-
return True
|
|
154
|
-
# Also check direct match
|
|
155
|
-
if fnmatch.fnmatch(path, dir_pattern + "*"):
|
|
156
|
-
return True
|
|
157
|
-
|
|
158
|
-
# Standard fnmatch for file patterns
|
|
159
|
-
return fnmatch.fnmatch(path, pattern) or fnmatch.fnmatch(str(Path(path)), pattern)
|
|
160
|
-
|
|
161
|
-
def _has_ignore_directive_marker(self, line: str) -> bool:
|
|
162
|
-
"""Check if line contains an ignore directive marker."""
|
|
163
|
-
line_lower = line.lower()
|
|
164
|
-
return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
|
|
165
|
-
|
|
166
|
-
def _check_specific_rule_ignore(self, line: str, rule_id: str) -> bool:
|
|
167
|
-
"""Check if line ignores a specific rule."""
|
|
168
|
-
# Check for bracket syntax: # thailint: ignore-file[rule1, rule2]
|
|
169
|
-
if self._check_bracket_syntax_file_ignore(line, rule_id):
|
|
170
|
-
return True
|
|
171
|
-
|
|
172
|
-
# Check for space-separated syntax: # thailint: ignore-file rule1 rule2
|
|
173
|
-
return self._check_space_syntax_file_ignore(line, rule_id)
|
|
174
|
-
|
|
175
|
-
def _check_bracket_syntax_file_ignore(self, line: str, rule_id: str) -> bool:
|
|
176
|
-
"""Check bracket syntax for file-level ignore."""
|
|
177
|
-
bracket_match = re.search(r"ignore-file\[([^\]]+)\]", line, re.IGNORECASE)
|
|
178
|
-
if bracket_match:
|
|
179
|
-
ignored_rules = [r.strip() for r in bracket_match.group(1).split(",")]
|
|
180
|
-
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
181
|
-
return False
|
|
182
|
-
|
|
183
|
-
def _check_space_syntax_file_ignore(self, line: str, rule_id: str) -> bool:
|
|
184
|
-
"""Check space-separated syntax for file-level ignore."""
|
|
185
|
-
space_match = re.search(r"ignore-file\s+([^\s#]+(?:\s+[^\s#]+)*)", line, re.IGNORECASE)
|
|
186
|
-
if space_match:
|
|
187
|
-
ignored_rules = [
|
|
188
|
-
r.strip() for r in re.split(r"[,\s]+", space_match.group(1)) if r.strip()
|
|
189
|
-
]
|
|
190
|
-
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
def _check_general_ignore(self, line: str) -> bool:
|
|
194
|
-
"""Check if line has general ignore directive (no specific rules)."""
|
|
195
|
-
return "ignore-file[" not in line
|
|
196
|
-
|
|
197
|
-
def _read_file_first_lines(self, file_path: Path) -> list[str]:
|
|
198
|
-
"""Read first 10 lines of file, return empty list on error."""
|
|
199
|
-
if not file_path.exists():
|
|
200
|
-
return []
|
|
201
|
-
try:
|
|
202
|
-
content = file_path.read_text(encoding="utf-8")
|
|
203
|
-
return content.splitlines()[:10]
|
|
204
|
-
except (UnicodeDecodeError, OSError):
|
|
205
|
-
return []
|
|
206
|
-
|
|
207
|
-
def _check_line_for_ignore(self, line: str, rule_id: str | None) -> bool:
|
|
208
|
-
"""Check if line has matching ignore directive."""
|
|
209
|
-
if not self._has_ignore_directive_marker(line):
|
|
210
|
-
return False
|
|
211
|
-
if rule_id:
|
|
212
|
-
return self._check_specific_rule_ignore(line, rule_id)
|
|
213
|
-
return self._check_general_ignore(line)
|
|
75
|
+
check_path = path_str
|
|
76
|
+
result = any(matches_pattern(check_path, p) for p in self.repo_patterns)
|
|
77
|
+
self._ignore_cache[path_str] = result
|
|
78
|
+
return result
|
|
214
79
|
|
|
215
80
|
def has_file_ignore(self, file_path: Path, rule_id: str | None = None) -> bool:
|
|
216
|
-
"""Check for file-level ignore directive.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
file_path: Path to file to check.
|
|
222
|
-
rule_id: Optional specific rule ID to check for.
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
True if file has ignore directive (general or for specific rule).
|
|
226
|
-
"""
|
|
227
|
-
first_lines = self._read_file_first_lines(file_path)
|
|
228
|
-
return any(self._check_line_for_ignore(line, rule_id) for line in first_lines)
|
|
229
|
-
|
|
230
|
-
def _has_line_ignore_marker(self, code: str) -> bool:
|
|
231
|
-
"""Check if code line has ignore marker."""
|
|
232
|
-
code_lower = code.lower()
|
|
233
|
-
return (
|
|
234
|
-
"# thailint: ignore" in code_lower
|
|
235
|
-
or "# design-lint: ignore" in code_lower
|
|
236
|
-
or "// thailint: ignore" in code_lower
|
|
237
|
-
or "// design-lint: ignore" in code_lower
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
def _check_specific_rule_in_line(self, code: str, rule_id: str) -> bool:
|
|
241
|
-
"""Check if line's ignore directive matches specific rule."""
|
|
242
|
-
# Check for bracket syntax: # thailint: ignore[rule1, rule2]
|
|
243
|
-
bracket_match = re.search(r"ignore\[([^\]]+)\]", code, re.IGNORECASE)
|
|
244
|
-
if bracket_match:
|
|
245
|
-
return self._check_bracket_rules(bracket_match.group(1), rule_id)
|
|
246
|
-
|
|
247
|
-
# Check for space-separated syntax: # thailint: ignore rule1 rule2
|
|
248
|
-
space_match = re.search(r"ignore\s+([^\s#]+(?:\s+[^\s#]+)*)", code, re.IGNORECASE)
|
|
249
|
-
if space_match:
|
|
250
|
-
return self._check_space_separated_rules(space_match.group(1), rule_id)
|
|
251
|
-
|
|
252
|
-
# No specific rules - check for "ignore-all"
|
|
253
|
-
return "ignore-all" in code.lower()
|
|
254
|
-
|
|
255
|
-
def _check_bracket_rules(self, rules_text: str, rule_id: str) -> bool:
|
|
256
|
-
"""Check if bracketed rules match the rule ID."""
|
|
257
|
-
ignored_rules = [r.strip() for r in rules_text.split(",")]
|
|
258
|
-
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
259
|
-
|
|
260
|
-
def _check_space_separated_rules(self, rules_text: str, rule_id: str) -> bool:
|
|
261
|
-
"""Check if space-separated rules match the rule ID."""
|
|
262
|
-
ignored_rules = [r.strip() for r in re.split(r"[,\s]+", rules_text) if r.strip()]
|
|
263
|
-
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
81
|
+
"""Check for file-level ignore directive in first 10 lines."""
|
|
82
|
+
first_lines = _read_file_first_lines(file_path)
|
|
83
|
+
return any(_check_line_for_ignore(line, rule_id) for line in first_lines)
|
|
264
84
|
|
|
265
85
|
def has_line_ignore(self, code: str, line_num: int, rule_id: str | None = None) -> bool:
|
|
266
|
-
"""Check for line-level ignore directive.
|
|
267
|
-
|
|
268
|
-
Args:
|
|
269
|
-
code: Line of code to check.
|
|
270
|
-
line_num: Line number (currently unused, for API compatibility).
|
|
271
|
-
rule_id: Optional specific rule ID to check for.
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
True if line has ignore directive.
|
|
275
|
-
"""
|
|
276
|
-
if not self._has_line_ignore_marker(code):
|
|
86
|
+
"""Check for line-level ignore directive."""
|
|
87
|
+
if not has_line_ignore_marker(code):
|
|
277
88
|
return False
|
|
278
|
-
|
|
279
89
|
if rule_id:
|
|
280
|
-
return
|
|
90
|
+
return _check_specific_rule_in_line(code, rule_id)
|
|
281
91
|
return True
|
|
282
92
|
|
|
283
|
-
def
|
|
284
|
-
"""Check if
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
rule_id: Rule ID to check (e.g., "nesting.excessive-depth").
|
|
288
|
-
pattern: Pattern with optional wildcard (e.g., "nesting.*" or "nesting").
|
|
289
|
-
|
|
290
|
-
Returns:
|
|
291
|
-
True if rule matches pattern.
|
|
292
|
-
"""
|
|
293
|
-
# Case-insensitive comparison
|
|
294
|
-
rule_id_lower = rule_id.lower()
|
|
295
|
-
pattern_lower = pattern.lower()
|
|
296
|
-
|
|
297
|
-
if pattern_lower.endswith("*"):
|
|
298
|
-
# Wildcard match: literals.* matches literals.magic-number
|
|
299
|
-
prefix = pattern_lower[:-1]
|
|
300
|
-
return rule_id_lower.startswith(prefix)
|
|
301
|
-
|
|
302
|
-
# Exact match
|
|
303
|
-
if rule_id_lower == pattern_lower:
|
|
93
|
+
def should_ignore_violation(self, violation: "Violation", file_content: str) -> bool:
|
|
94
|
+
"""Check if a violation should be ignored based on all levels."""
|
|
95
|
+
file_path = Path(violation.file_path)
|
|
96
|
+
if self._is_ignored_at_file_level(file_path, violation.rule_id, file_content):
|
|
304
97
|
return True
|
|
98
|
+
return _is_ignored_in_content(file_content, violation)
|
|
305
99
|
|
|
306
|
-
|
|
307
|
-
|
|
100
|
+
def _is_ignored_at_file_level(self, file_path: Path, rule_id: str, file_content: str) -> bool:
|
|
101
|
+
"""Check repository and file level ignores."""
|
|
102
|
+
if self.is_ignored(file_path):
|
|
308
103
|
return True
|
|
104
|
+
if _has_file_ignore_in_content(file_content, rule_id):
|
|
105
|
+
return True
|
|
106
|
+
return self.has_file_ignore(file_path, rule_id)
|
|
309
107
|
|
|
310
|
-
return False
|
|
311
108
|
|
|
312
|
-
|
|
313
|
-
"""Check if line has ignore-next-line marker."""
|
|
314
|
-
return (
|
|
315
|
-
"# thailint: ignore-next-line" in prev_line
|
|
316
|
-
or "# design-lint: ignore-next-line" in prev_line
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
def _matches_ignore_next_line_rules(self, prev_line: str, rule_id: str) -> bool:
|
|
320
|
-
"""Check if ignore-next-line directive matches the rule."""
|
|
321
|
-
match = re.search(r"ignore-next-line\[([^\]]+)\]", prev_line)
|
|
322
|
-
if match:
|
|
323
|
-
ignored_rules = [r.strip() for r in match.group(1).split(",")]
|
|
324
|
-
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
325
|
-
return True
|
|
109
|
+
# Module-level helper functions (don't need instance state)
|
|
326
110
|
|
|
327
|
-
def _is_valid_prev_line_index(self, lines: list[str], violation: "Violation") -> bool:
|
|
328
|
-
"""Check if previous line index is valid."""
|
|
329
|
-
if violation.line <= 1 or violation.line > len(lines) + 1:
|
|
330
|
-
return False
|
|
331
|
-
prev_line_idx = violation.line - 2
|
|
332
|
-
return 0 <= prev_line_idx < len(lines)
|
|
333
111
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
112
|
+
def _load_repo_ignores(project_root: Path) -> list[str]:
|
|
113
|
+
"""Load global ignore patterns from .thailintignore or .thailint.yaml."""
|
|
114
|
+
thailintignore = project_root / ".thailintignore"
|
|
115
|
+
if thailintignore.exists():
|
|
116
|
+
return _parse_thailintignore_file(thailintignore)
|
|
117
|
+
config_file = project_root / ".thailint.yaml"
|
|
118
|
+
if config_file.exists():
|
|
119
|
+
return _parse_config_file(config_file)
|
|
120
|
+
return []
|
|
338
121
|
|
|
339
|
-
prev_line_idx = violation.line - 2
|
|
340
|
-
prev_line = lines[prev_line_idx]
|
|
341
|
-
if not self._has_ignore_next_line_marker(prev_line):
|
|
342
|
-
return False
|
|
343
122
|
|
|
344
|
-
|
|
123
|
+
def _parse_thailintignore_file(ignore_file: Path) -> list[str]:
|
|
124
|
+
"""Parse .thailintignore file (gitignore-style)."""
|
|
125
|
+
try:
|
|
126
|
+
content = ignore_file.read_text(encoding="utf-8")
|
|
127
|
+
return extract_patterns_from_content(content)
|
|
128
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
129
|
+
logger.warning("Failed to read .thailintignore file %s: %s", ignore_file, e)
|
|
130
|
+
return []
|
|
345
131
|
|
|
346
|
-
def _check_current_line_ignore(self, lines: list[str], violation: "Violation") -> bool:
|
|
347
|
-
"""Check if current line has inline ignore directive."""
|
|
348
|
-
if violation.line <= 0 or violation.line > len(lines):
|
|
349
|
-
return False
|
|
350
132
|
|
|
351
|
-
|
|
352
|
-
|
|
133
|
+
def _parse_config_file(config_file: Path) -> list[str]:
|
|
134
|
+
"""Parse YAML config file and extract ignore patterns."""
|
|
135
|
+
try:
|
|
136
|
+
config = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
|
137
|
+
return _extract_ignore_patterns(config)
|
|
138
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
|
|
139
|
+
logger.warning("Failed to parse config file %s: %s", config_file, e)
|
|
140
|
+
return []
|
|
353
141
|
|
|
354
|
-
def should_ignore_violation(self, violation: "Violation", file_content: str) -> bool:
|
|
355
|
-
"""Check if a violation should be ignored based on all levels."""
|
|
356
|
-
file_path = Path(violation.file_path)
|
|
357
142
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
143
|
+
def _extract_ignore_patterns(config: dict | None) -> list[str]:
|
|
144
|
+
"""Extract ignore patterns from config dict."""
|
|
145
|
+
if not config or not isinstance(config, dict):
|
|
146
|
+
return []
|
|
147
|
+
ignore_patterns = config.get("ignore", [])
|
|
148
|
+
if isinstance(ignore_patterns, list):
|
|
149
|
+
return [str(pattern) for pattern in ignore_patterns]
|
|
150
|
+
return []
|
|
361
151
|
|
|
362
|
-
# Line-based checks
|
|
363
|
-
return self._is_ignored_in_content(file_content, violation)
|
|
364
152
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
153
|
+
def _read_file_first_lines(file_path: Path) -> list[str]:
|
|
154
|
+
"""Read first lines of file for header scanning, return empty list on error."""
|
|
155
|
+
if not file_path.exists():
|
|
156
|
+
return []
|
|
157
|
+
try:
|
|
158
|
+
content = file_path.read_text(encoding="utf-8")
|
|
159
|
+
return content.splitlines()[:HEADER_SCAN_LINES]
|
|
160
|
+
except (UnicodeDecodeError, OSError) as e:
|
|
161
|
+
logger.debug("Failed to read file %s: %s", file_path, e)
|
|
162
|
+
return []
|
|
374
163
|
|
|
375
|
-
def _has_file_ignore_in_content(self, file_content: str, rule_id: str | None) -> bool:
|
|
376
|
-
"""Check if file content has ignore-file directive."""
|
|
377
|
-
lines = file_content.splitlines()[:10] # Check first 10 lines
|
|
378
|
-
return any(self._check_line_for_ignore(line, rule_id) for line in lines)
|
|
379
164
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
165
|
+
def _check_line_for_ignore(line: str, rule_id: str | None) -> bool:
|
|
166
|
+
"""Check if line has matching ignore directive."""
|
|
167
|
+
if not has_ignore_directive_marker(line):
|
|
168
|
+
return False
|
|
169
|
+
if rule_id:
|
|
170
|
+
return _check_specific_rule_ignore(line, rule_id)
|
|
171
|
+
return check_general_ignore(line)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_specific_rule_ignore(line: str, rule_id: str) -> bool:
|
|
175
|
+
"""Check if line ignores a specific rule."""
|
|
176
|
+
bracket_match = re.search(r"ignore-file\[([^\]]+)\]", line, re.IGNORECASE)
|
|
177
|
+
if bracket_match:
|
|
178
|
+
return check_bracket_rules(bracket_match.group(1), rule_id)
|
|
179
|
+
space_match = re.search(r"ignore-file\s+([^\s#]+(?:\s+[^\s#]+)*)", line, re.IGNORECASE)
|
|
180
|
+
if space_match:
|
|
181
|
+
return check_space_separated_rules(space_match.group(1), rule_id)
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _check_specific_rule_in_line(code: str, rule_id: str) -> bool:
|
|
186
|
+
"""Check if line's ignore directive matches specific rule."""
|
|
187
|
+
bracket_match = re.search(r"ignore\[([^\]]+)\]", code, re.IGNORECASE)
|
|
188
|
+
if bracket_match:
|
|
189
|
+
return check_bracket_rules(bracket_match.group(1), rule_id)
|
|
190
|
+
space_match = re.search(r"ignore\s+([^\s#]+(?:\s+[^\s#]+)*)", code, re.IGNORECASE)
|
|
191
|
+
if space_match:
|
|
192
|
+
return check_space_separated_rules(space_match.group(1), rule_id)
|
|
193
|
+
return "ignore-all" in code.lower()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _has_file_ignore_in_content(file_content: str, rule_id: str | None) -> bool:
|
|
197
|
+
"""Check if file content has ignore-file directive."""
|
|
198
|
+
lines = file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
199
|
+
return any(_check_line_for_ignore(line, rule_id) for line in lines)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_ignored_in_content(file_content: str, violation: "Violation") -> bool:
|
|
203
|
+
"""Check content-based ignores (block, line, method level)."""
|
|
204
|
+
lines = file_content.splitlines()
|
|
205
|
+
if _check_block_ignore(lines, violation):
|
|
206
|
+
return True
|
|
207
|
+
if _check_prev_line_ignore(lines, violation):
|
|
208
|
+
return True
|
|
209
|
+
return _check_current_line_ignore(lines, violation)
|
|
383
210
|
|
|
384
|
-
if self._check_block_ignore(lines, violation):
|
|
385
|
-
return True
|
|
386
|
-
if self._check_prev_line_ignore(lines, violation):
|
|
387
|
-
return True
|
|
388
|
-
if self._check_current_line_ignore(lines, violation):
|
|
389
|
-
return True
|
|
390
211
|
|
|
212
|
+
def _check_block_ignore(lines: list[str], violation: "Violation") -> bool:
|
|
213
|
+
"""Check if violation is within an ignore-start/ignore-end block."""
|
|
214
|
+
if not _is_valid_line_range(violation.line, len(lines)):
|
|
391
215
|
return False
|
|
216
|
+
state = _BlockState()
|
|
217
|
+
for i, line in enumerate(lines, 1):
|
|
218
|
+
result = _process_block_line(line, i, violation, state)
|
|
219
|
+
if result is not None:
|
|
220
|
+
return result
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class _BlockState:
|
|
225
|
+
"""Mutable state for block ignore scanning."""
|
|
226
|
+
|
|
227
|
+
def __init__(self) -> None:
|
|
228
|
+
self.in_block = False
|
|
229
|
+
self.rules: set[str] = set()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_valid_line_range(line: int, max_lines: int) -> bool:
|
|
233
|
+
"""Check if line number is within valid range."""
|
|
234
|
+
return 0 < line <= max_lines
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _process_block_line(
|
|
238
|
+
line: str, line_num: int, violation: "Violation", state: _BlockState
|
|
239
|
+
) -> bool | None:
|
|
240
|
+
"""Process a line for block ignore, returning True/False if decided, None to continue."""
|
|
241
|
+
if has_ignore_start_marker(line):
|
|
242
|
+
state.rules = _parse_ignore_start_rules(line)
|
|
243
|
+
state.in_block = True
|
|
244
|
+
return None
|
|
245
|
+
if has_ignore_end_marker(line):
|
|
246
|
+
return _handle_block_end(line_num, violation, state)
|
|
247
|
+
if line_num == violation.line and state.in_block:
|
|
248
|
+
return rules_match_violation(state.rules, violation.rule_id)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _handle_block_end(line_num: int, violation: "Violation", state: _BlockState) -> bool | None:
|
|
253
|
+
"""Handle block end marker."""
|
|
254
|
+
if state.in_block and line_num > violation.line:
|
|
255
|
+
if rules_match_violation(state.rules, violation.rule_id):
|
|
256
|
+
return True
|
|
257
|
+
state.in_block = False
|
|
258
|
+
state.rules = set()
|
|
259
|
+
return None
|
|
392
260
|
|
|
393
|
-
def _check_block_ignore(self, lines: list[str], violation: "Violation") -> bool:
|
|
394
|
-
"""Check if violation is within an ignore-start/ignore-end block."""
|
|
395
|
-
if violation.line <= 0 or violation.line > len(lines):
|
|
396
|
-
return False
|
|
397
261
|
|
|
398
|
-
|
|
262
|
+
def _parse_ignore_start_rules(line: str) -> set[str]:
|
|
263
|
+
"""Extract rule names from ignore-start directive."""
|
|
264
|
+
match = re.search(r"ignore-start\s+([^\s#]+(?:\s+[^\s#]+)*)", line)
|
|
265
|
+
if match:
|
|
266
|
+
rules_text = match.group(1).strip()
|
|
267
|
+
rules = [r.strip() for r in re.split(r"[,\s]+", rules_text) if r.strip()]
|
|
268
|
+
return set(rules)
|
|
269
|
+
return {"*"}
|
|
399
270
|
|
|
400
|
-
for i, line in enumerate(lines):
|
|
401
|
-
if self._process_block_line(line, i + 1, violation, block_state):
|
|
402
|
-
return True
|
|
403
271
|
|
|
272
|
+
def _check_prev_line_ignore(lines: list[str], violation: "Violation") -> bool:
|
|
273
|
+
"""Check if previous line has ignore-next-line directive."""
|
|
274
|
+
prev_line = _get_prev_line(lines, violation.line)
|
|
275
|
+
if prev_line is None:
|
|
276
|
+
return False
|
|
277
|
+
if not has_ignore_next_line_marker(prev_line):
|
|
404
278
|
return False
|
|
279
|
+
return _matches_ignore_next_line_rules(prev_line, violation.rule_id)
|
|
405
280
|
|
|
406
|
-
def _process_block_line(
|
|
407
|
-
self, line: str, line_num: int, violation: "Violation", block_state: dict
|
|
408
|
-
) -> bool:
|
|
409
|
-
"""Process a single line for block ignore checking."""
|
|
410
|
-
if "ignore-start" in line:
|
|
411
|
-
block_state["rules"] = self._parse_ignore_start_rules(line)
|
|
412
|
-
block_state["in_block"] = True
|
|
413
|
-
return False
|
|
414
281
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
282
|
+
def _get_prev_line(lines: list[str], violation_line: int) -> str | None:
|
|
283
|
+
"""Get previous line if it exists and is valid."""
|
|
284
|
+
if violation_line <= 1:
|
|
285
|
+
return None
|
|
286
|
+
prev_idx = violation_line - 2
|
|
287
|
+
if prev_idx < 0 or prev_idx >= len(lines):
|
|
288
|
+
return None
|
|
289
|
+
return lines[prev_idx]
|
|
419
290
|
|
|
420
|
-
if self._is_violation_line_ignored(
|
|
421
|
-
line_num, violation, block_state["in_block"], block_state["rules"]
|
|
422
|
-
):
|
|
423
|
-
return True
|
|
424
291
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
292
|
+
def _matches_ignore_next_line_rules(prev_line: str, rule_id: str) -> bool:
|
|
293
|
+
"""Check if ignore-next-line directive matches the rule."""
|
|
294
|
+
match = re.search(r"ignore-next-line\[([^\]]+)\]", prev_line)
|
|
295
|
+
if match:
|
|
296
|
+
return check_bracket_rules(match.group(1), rule_id)
|
|
297
|
+
return True
|
|
428
298
|
|
|
429
|
-
return False
|
|
430
299
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return False
|
|
442
|
-
if not in_ignore_block or line_num <= violation.line:
|
|
443
|
-
return False
|
|
444
|
-
return self._rules_match_violation(current_ignored_rules, violation.rule_id)
|
|
445
|
-
|
|
446
|
-
def _is_violation_line_ignored(
|
|
447
|
-
self,
|
|
448
|
-
line_num: int,
|
|
449
|
-
violation: "Violation",
|
|
450
|
-
in_ignore_block: bool,
|
|
451
|
-
current_ignored_rules: set[str],
|
|
452
|
-
) -> bool:
|
|
453
|
-
"""Check if current line is the violation line in an ignore block."""
|
|
454
|
-
if line_num != violation.line or not in_ignore_block:
|
|
455
|
-
return False
|
|
456
|
-
return self._rules_match_violation(current_ignored_rules, violation.rule_id)
|
|
457
|
-
|
|
458
|
-
def _parse_ignore_start_rules(self, line: str) -> set[str]:
|
|
459
|
-
"""Extract rule names from ignore-start directive."""
|
|
460
|
-
match = re.search(r"ignore-start\s+([^\s#]+(?:\s+[^\s#]+)*)", line)
|
|
461
|
-
if match:
|
|
462
|
-
rules_text = match.group(1).strip()
|
|
463
|
-
rules = [r.strip() for r in re.split(r"[,\s]+", rules_text) if r.strip()]
|
|
464
|
-
return set(rules)
|
|
465
|
-
return {"*"}
|
|
466
|
-
|
|
467
|
-
def _rules_match_violation(self, ignored_rules: set[str], rule_id: str) -> bool:
|
|
468
|
-
"""Check if any of the ignored rules match the violation rule ID."""
|
|
469
|
-
if "*" in ignored_rules:
|
|
470
|
-
return True
|
|
471
|
-
return any(self._rule_matches(rule_id, pattern) for pattern in ignored_rules)
|
|
300
|
+
def _check_current_line_ignore(lines: list[str], violation: "Violation") -> bool:
|
|
301
|
+
"""Check if current line has inline ignore directive."""
|
|
302
|
+
if violation.line <= 0 or violation.line > len(lines):
|
|
303
|
+
return False
|
|
304
|
+
current_line = lines[violation.line - 1]
|
|
305
|
+
if not has_line_ignore_marker(current_line):
|
|
306
|
+
return False
|
|
307
|
+
return (
|
|
308
|
+
_check_specific_rule_in_line(current_line, violation.rule_id) if violation.rule_id else True
|
|
309
|
+
)
|
|
472
310
|
|
|
473
311
|
|
|
474
312
|
# Alias for backwards compatibility
|
|
475
313
|
IgnoreParser = IgnoreDirectiveParser
|
|
314
|
+
|
|
315
|
+
# Singleton pattern for performance
|
|
316
|
+
_CACHED_PARSER: IgnoreDirectiveParser | None = None
|
|
317
|
+
_CACHED_PROJECT_ROOT: Path | None = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_ignore_parser(project_root: Path | None = None) -> IgnoreDirectiveParser:
|
|
321
|
+
"""Get cached ignore parser instance (singleton pattern for performance)."""
|
|
322
|
+
global _CACHED_PARSER, _CACHED_PROJECT_ROOT # pylint: disable=global-statement
|
|
323
|
+
effective_root = project_root or Path.cwd()
|
|
324
|
+
if _CACHED_PARSER is None or _CACHED_PROJECT_ROOT != effective_root:
|
|
325
|
+
_CACHED_PARSER = IgnoreDirectiveParser(effective_root)
|
|
326
|
+
_CACHED_PROJECT_ROOT = effective_root
|
|
327
|
+
return _CACHED_PARSER
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def clear_ignore_parser_cache() -> None:
|
|
331
|
+
"""Clear cached parser for test isolation or project root changes."""
|
|
332
|
+
global _CACHED_PARSER, _CACHED_PROJECT_ROOT # pylint: disable=global-statement
|
|
333
|
+
_CACHED_PARSER = None
|
|
334
|
+
_CACHED_PROJECT_ROOT = None
|