thailint 0.12.0__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +9 -0
- src/cli/linters/code_patterns.py +107 -257
- src/cli/linters/code_smells.py +48 -165
- src/cli/linters/documentation.py +21 -95
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +26 -21
- src/cli/linters/structure_quality.py +28 -21
- 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 +95 -6
- 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 +58 -40
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/config.py +14 -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 +205 -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 +69 -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 -1
- src/linters/method_property/linter.py +5 -10
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/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 +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.14.0.dist-info/METADATA +185 -0
- thailint-0.14.0.dist-info/RECORD +199 -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.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -42,15 +42,12 @@ class FieldValidator:
|
|
|
42
42
|
Returns:
|
|
43
43
|
List of (field_name, error_message) tuples for missing/invalid fields
|
|
44
44
|
"""
|
|
45
|
-
violations = []
|
|
46
45
|
required_fields = self._get_required_fields(language)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if error
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return violations
|
|
46
|
+
return [
|
|
47
|
+
error
|
|
48
|
+
for field_name in required_fields
|
|
49
|
+
if (error := self._check_field(fields, field_name))
|
|
50
|
+
]
|
|
54
51
|
|
|
55
52
|
def _check_field(self, fields: dict[str, str], field_name: str) -> tuple[str, str] | None:
|
|
56
53
|
"""Check a single field for presence and content."""
|
|
@@ -20,15 +20,22 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
|
|
|
20
20
|
|
|
21
21
|
Implementation: Composition pattern with helper classes for parsing, validation,
|
|
22
22
|
and violation building
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- type:ignore[type-var]: Protocol pattern with generic type matching
|
|
26
|
+
- srp: Rule class coordinates parsing, validation, and violation building for multiple
|
|
27
|
+
languages. Methods support single responsibility of file header validation.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
from pathlib import Path
|
|
26
31
|
from typing import Protocol
|
|
27
32
|
|
|
28
33
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
34
|
+
from src.core.constants import HEADER_SCAN_LINES, Language
|
|
29
35
|
from src.core.linter_utils import load_linter_config
|
|
30
36
|
from src.core.types import Violation
|
|
31
|
-
from src.linter_config.
|
|
37
|
+
from src.linter_config.directive_markers import check_general_ignore, has_ignore_directive_marker
|
|
38
|
+
from src.linter_config.ignore import _check_specific_rule_ignore, get_ignore_parser
|
|
32
39
|
|
|
33
40
|
from .atemporal_detector import AtemporalDetector
|
|
34
41
|
from .bash_parser import BashHeaderParser
|
|
@@ -111,7 +118,7 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
111
118
|
return []
|
|
112
119
|
|
|
113
120
|
# Markdown has special atemporal handling
|
|
114
|
-
if context.language ==
|
|
121
|
+
if context.language == Language.MARKDOWN:
|
|
115
122
|
return self._check_markdown_header(parser, context, config)
|
|
116
123
|
|
|
117
124
|
return self._check_header_with_parser(parser, context, config)
|
|
@@ -158,20 +165,20 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
158
165
|
|
|
159
166
|
return self._has_custom_ignore_syntax(file_content)
|
|
160
167
|
|
|
161
|
-
def _has_standard_ignore(self, file_content: str) -> bool:
|
|
168
|
+
def _has_standard_ignore(self, file_content: str) -> bool:
|
|
162
169
|
"""Check standard ignore parser for file-level ignores."""
|
|
163
|
-
first_lines = file_content.splitlines()[:
|
|
164
|
-
for line in first_lines
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
170
|
+
first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
171
|
+
return any(self._line_has_matching_ignore(line) for line in first_lines)
|
|
172
|
+
|
|
173
|
+
def _line_has_matching_ignore(self, line: str) -> bool:
|
|
174
|
+
"""Check if line has matching ignore directive for this rule."""
|
|
175
|
+
if not has_ignore_directive_marker(line):
|
|
176
|
+
return False
|
|
177
|
+
return _check_specific_rule_ignore(line, self.rule_id) or check_general_ignore(line)
|
|
171
178
|
|
|
172
179
|
def _has_custom_ignore_syntax(self, file_content: str) -> bool:
|
|
173
180
|
"""Check custom file-level ignore syntax."""
|
|
174
|
-
first_lines = file_content.splitlines()[:
|
|
181
|
+
first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
175
182
|
return any(self._is_ignore_line(line) for line in first_lines)
|
|
176
183
|
|
|
177
184
|
def _is_ignore_line(self, line: str) -> bool:
|
|
@@ -17,6 +17,12 @@ Interfaces: extract_header(code) -> str | None for frontmatter extraction,
|
|
|
17
17
|
parse_fields(header) -> dict[str, str] for field parsing
|
|
18
18
|
|
|
19
19
|
Implementation: YAML frontmatter extraction with PyYAML parsing and regex fallback for robustness
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- BLE001: Broad exception catch for YAML parsing fallback (any exception triggers regex fallback)
|
|
23
|
+
- srp: Class coordinates YAML extraction, parsing, and field validation for Markdown.
|
|
24
|
+
Method count exceeds limit due to complexity refactoring.
|
|
25
|
+
- nesting,dry: _parse_simple_yaml uses nested loops for YAML structure traversal.
|
|
20
26
|
"""
|
|
21
27
|
|
|
22
28
|
import logging
|
|
@@ -23,6 +23,8 @@ from typing import Any
|
|
|
23
23
|
|
|
24
24
|
import yaml
|
|
25
25
|
|
|
26
|
+
from src.core.constants import CONFIG_EXTENSIONS
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
class ConfigLoader:
|
|
28
30
|
"""Loads configuration files for file placement linter."""
|
|
@@ -79,7 +81,7 @@ class ConfigLoader:
|
|
|
79
81
|
ValueError: If file format is unsupported
|
|
80
82
|
"""
|
|
81
83
|
with config_path.open(encoding="utf-8") as f:
|
|
82
|
-
if config_path.suffix in
|
|
84
|
+
if config_path.suffix in CONFIG_EXTENSIONS:
|
|
83
85
|
return yaml.safe_load(f) or {}
|
|
84
86
|
if config_path.suffix == ".json":
|
|
85
87
|
return json.load(f)
|
|
@@ -20,6 +20,10 @@ Interfaces: lint_path(file_path) -> list[Violation], check_file_allowed(file_pat
|
|
|
20
20
|
Implementation: Composition pattern with helper classes for each responsibility
|
|
21
21
|
(ConfigLoader, PathResolver, PatternMatcher, PatternValidator, RuleChecker,
|
|
22
22
|
ViolationFactory)
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- srp.violation: Rule class coordinates multiple helper classes for comprehensive
|
|
26
|
+
file placement validation. Method count reflects composition orchestration.
|
|
23
27
|
"""
|
|
24
28
|
|
|
25
29
|
import json
|
|
@@ -72,21 +76,31 @@ class FilePlacementLinter:
|
|
|
72
76
|
|
|
73
77
|
# Load and validate config
|
|
74
78
|
if config_obj:
|
|
75
|
-
|
|
76
|
-
# Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
|
|
77
|
-
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
78
|
-
# Try both hyphenated and underscored keys for backward compatibility
|
|
79
|
-
self.config = config_obj.get(
|
|
80
|
-
"file-placement", config_obj.get("file_placement", config_obj)
|
|
81
|
-
)
|
|
79
|
+
self.config = self._unwrap_config(config_obj)
|
|
82
80
|
elif config_file:
|
|
83
|
-
|
|
81
|
+
raw_config = self._components.config_loader.load_config_file(config_file)
|
|
82
|
+
self.config = self._unwrap_config(raw_config)
|
|
84
83
|
else:
|
|
85
84
|
self.config = {}
|
|
86
85
|
|
|
87
86
|
# Validate regex patterns in config
|
|
88
87
|
self._components.pattern_validator.validate_config(self.config)
|
|
89
88
|
|
|
89
|
+
def _unwrap_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
"""Unwrap file-placement config from wrapper if present.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
config: Raw config dict (may be wrapped or unwrapped)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Unwrapped file-placement config dict
|
|
97
|
+
"""
|
|
98
|
+
# Handle both wrapped and unwrapped config formats
|
|
99
|
+
# Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
|
|
100
|
+
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
101
|
+
# Try both hyphenated and underscored keys for backward compatibility
|
|
102
|
+
return config.get("file-placement", config.get("file_placement", config))
|
|
103
|
+
|
|
90
104
|
def lint_path(self, file_path: Path) -> list[Violation]:
|
|
91
105
|
"""Lint a single file path.
|
|
92
106
|
|
|
@@ -18,6 +18,7 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
|
+
from collections.abc import Sequence
|
|
21
22
|
from re import Pattern
|
|
22
23
|
|
|
23
24
|
|
|
@@ -42,24 +43,40 @@ class PatternMatcher:
|
|
|
42
43
|
return self._compiled_patterns[pattern]
|
|
43
44
|
|
|
44
45
|
def match_deny_patterns(
|
|
45
|
-
self, path_str: str, deny_patterns:
|
|
46
|
+
self, path_str: str, deny_patterns: Sequence[dict[str, str] | str]
|
|
46
47
|
) -> tuple[bool, str | None]:
|
|
47
48
|
"""Check if path matches any deny patterns.
|
|
48
49
|
|
|
49
50
|
Args:
|
|
50
51
|
path_str: File path to check
|
|
51
|
-
deny_patterns: List of deny
|
|
52
|
+
deny_patterns: List of deny patterns (either dicts with 'pattern'/'reason'
|
|
53
|
+
or plain regex strings for backward compatibility)
|
|
52
54
|
|
|
53
55
|
Returns:
|
|
54
56
|
Tuple of (is_denied, reason)
|
|
55
57
|
"""
|
|
56
58
|
for deny_item in deny_patterns:
|
|
57
|
-
|
|
59
|
+
pattern, reason = self._extract_pattern_and_reason(deny_item)
|
|
60
|
+
compiled = self._get_compiled(pattern)
|
|
58
61
|
if compiled.search(path_str):
|
|
59
|
-
reason = deny_item.get("reason", "File not allowed in this location")
|
|
60
62
|
return True, reason
|
|
61
63
|
return False, None
|
|
62
64
|
|
|
65
|
+
def _extract_pattern_and_reason(self, deny_item: dict[str, str] | str) -> tuple[str, str]:
|
|
66
|
+
"""Extract pattern and reason from a deny item.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
deny_item: Either a dict with 'pattern' key or a plain string pattern
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple of (pattern, reason)
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(deny_item, str):
|
|
75
|
+
return deny_item, "File not allowed in this location"
|
|
76
|
+
return deny_item["pattern"], deny_item.get(
|
|
77
|
+
"reason", deny_item.get("message", "File not allowed in this location")
|
|
78
|
+
)
|
|
79
|
+
|
|
63
80
|
def match_allow_patterns(self, path_str: str, allow_patterns: list[str]) -> bool:
|
|
64
81
|
"""Check if path matches any allow patterns.
|
|
65
82
|
|
|
@@ -21,6 +21,20 @@ import re
|
|
|
21
21
|
from typing import Any
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _extract_pattern(deny_item: dict[str, str] | str) -> str:
|
|
25
|
+
"""Extract pattern from a deny item.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
deny_item: Either a dict with 'pattern' key or a plain string pattern
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The pattern string
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(deny_item, str):
|
|
34
|
+
return deny_item
|
|
35
|
+
return deny_item.get("pattern", "")
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
class PatternValidator:
|
|
25
39
|
"""Validates regex patterns in file placement configuration."""
|
|
26
40
|
|
|
@@ -32,15 +46,15 @@ class PatternValidator:
|
|
|
32
46
|
"""Validate all regex patterns in configuration.
|
|
33
47
|
|
|
34
48
|
Args:
|
|
35
|
-
config:
|
|
49
|
+
config: File placement configuration dict (already unwrapped)
|
|
36
50
|
|
|
37
51
|
Raises:
|
|
38
52
|
ValueError: If any regex pattern is invalid
|
|
39
53
|
"""
|
|
40
|
-
|
|
41
|
-
self._validate_directory_patterns(
|
|
42
|
-
self._validate_global_patterns(
|
|
43
|
-
self._validate_global_deny_patterns(
|
|
54
|
+
# Config is already unwrapped from file-placement key by FilePlacementLinter
|
|
55
|
+
self._validate_directory_patterns(config)
|
|
56
|
+
self._validate_global_patterns(config)
|
|
57
|
+
self._validate_global_deny_patterns(config)
|
|
44
58
|
|
|
45
59
|
def _validate_pattern(self, pattern: str) -> None:
|
|
46
60
|
"""Validate a single regex pattern.
|
|
@@ -74,7 +88,7 @@ class PatternValidator:
|
|
|
74
88
|
"""
|
|
75
89
|
if "deny" in rules:
|
|
76
90
|
for deny_item in rules["deny"]:
|
|
77
|
-
pattern = deny_item
|
|
91
|
+
pattern = _extract_pattern(deny_item)
|
|
78
92
|
self._validate_pattern(pattern)
|
|
79
93
|
|
|
80
94
|
def _validate_directory_patterns(self, fp_config: dict[str, Any]) -> None:
|
|
@@ -106,5 +120,5 @@ class PatternValidator:
|
|
|
106
120
|
"""
|
|
107
121
|
if "global_deny" in fp_config:
|
|
108
122
|
for deny_item in fp_config["global_deny"]:
|
|
109
|
-
pattern = deny_item
|
|
123
|
+
pattern = _extract_pattern(deny_item)
|
|
110
124
|
self._validate_pattern(pattern)
|
|
@@ -177,14 +177,14 @@ class RuleChecker:
|
|
|
177
177
|
return [violation] if violation else []
|
|
178
178
|
|
|
179
179
|
def _check_global_deny(
|
|
180
|
-
self, path_str: str, rel_path: Path, global_deny: list[dict[str, str]]
|
|
180
|
+
self, path_str: str, rel_path: Path, global_deny: list[dict[str, str] | str]
|
|
181
181
|
) -> list[Violation]:
|
|
182
182
|
"""Check file against global deny patterns.
|
|
183
183
|
|
|
184
184
|
Args:
|
|
185
185
|
path_str: Normalized path string
|
|
186
186
|
rel_path: Relative path object
|
|
187
|
-
global_deny: Global deny patterns
|
|
187
|
+
global_deny: Global deny patterns (dicts with pattern/reason or plain strings)
|
|
188
188
|
|
|
189
189
|
Returns:
|
|
190
190
|
List of violations
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Lazy-ignores linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect unjustified linting suppressions in code files
|
|
5
|
+
|
|
6
|
+
Overview: Package providing lazy-ignores linter functionality. Detects when AI agents add
|
|
7
|
+
linting suppressions (noqa, type:ignore, pylint:disable, nosec, thailint:ignore, etc.)
|
|
8
|
+
or test skips (pytest.mark.skip, it.skip, describe.skip) without proper justification
|
|
9
|
+
in the file header's Suppressions section. Enforces header-based declaration model
|
|
10
|
+
where all suppressions must be documented with human approval.
|
|
11
|
+
|
|
12
|
+
Dependencies: src.core for base types, re for pattern matching
|
|
13
|
+
|
|
14
|
+
Exports: IgnoreType, IgnoreDirective, SuppressionEntry, LazyIgnoresConfig,
|
|
15
|
+
PythonIgnoreDetector, TypeScriptIgnoreDetector, TestSkipDetector, SuppressionsParser
|
|
16
|
+
|
|
17
|
+
Interfaces: LazyIgnoresConfig.from_dict() for YAML configuration loading
|
|
18
|
+
|
|
19
|
+
Implementation: Enum and dataclass definitions for ignore directive representation
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import LazyIgnoresConfig
|
|
23
|
+
from .header_parser import SuppressionsParser
|
|
24
|
+
from .linter import LazyIgnoresRule
|
|
25
|
+
from .python_analyzer import PythonIgnoreDetector
|
|
26
|
+
from .skip_detector import TestSkipDetector
|
|
27
|
+
from .types import IgnoreDirective, IgnoreType, SuppressionEntry
|
|
28
|
+
from .typescript_analyzer import TypeScriptIgnoreDetector
|
|
29
|
+
from .violation_builder import build_orphaned_violation, build_unjustified_violation
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"IgnoreType",
|
|
33
|
+
"IgnoreDirective",
|
|
34
|
+
"SuppressionEntry",
|
|
35
|
+
"LazyIgnoresConfig",
|
|
36
|
+
"PythonIgnoreDetector",
|
|
37
|
+
"TypeScriptIgnoreDetector",
|
|
38
|
+
"TestSkipDetector",
|
|
39
|
+
"SuppressionsParser",
|
|
40
|
+
"LazyIgnoresRule",
|
|
41
|
+
"build_unjustified_violation",
|
|
42
|
+
"build_orphaned_violation",
|
|
43
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration for lazy-ignores linter
|
|
3
|
+
|
|
4
|
+
Scope: All configurable options for ignore detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides LazyIgnoresConfig dataclass with pattern-specific toggles for each
|
|
7
|
+
ignore type (noqa, type:ignore, pylint, nosec, typescript, eslint, thailint). Includes
|
|
8
|
+
orphaned detection toggle and file pattern ignores. Configuration can be loaded from
|
|
9
|
+
dictionary (YAML) with sensible defaults for all options.
|
|
10
|
+
|
|
11
|
+
Dependencies: dataclasses, typing
|
|
12
|
+
|
|
13
|
+
Exports: LazyIgnoresConfig
|
|
14
|
+
|
|
15
|
+
Interfaces: LazyIgnoresConfig.from_dict() for YAML configuration loading
|
|
16
|
+
|
|
17
|
+
Implementation: Dataclass with factory defaults and validation in from_dict
|
|
18
|
+
|
|
19
|
+
Suppressions:
|
|
20
|
+
too-many-instance-attributes: Configuration dataclass requires many toggles
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class LazyIgnoresConfig: # pylint: disable=too-many-instance-attributes
|
|
29
|
+
"""Configuration for the lazy-ignores linter."""
|
|
30
|
+
|
|
31
|
+
# Pattern detection toggles
|
|
32
|
+
check_noqa: bool = True
|
|
33
|
+
check_type_ignore: bool = True
|
|
34
|
+
check_pylint_disable: bool = True
|
|
35
|
+
check_nosec: bool = True
|
|
36
|
+
check_ts_ignore: bool = True
|
|
37
|
+
check_eslint_disable: bool = True
|
|
38
|
+
check_thailint_ignore: bool = True
|
|
39
|
+
check_test_skips: bool = True
|
|
40
|
+
|
|
41
|
+
# Orphaned detection
|
|
42
|
+
check_orphaned: bool = True # Header entries without matching ignores
|
|
43
|
+
|
|
44
|
+
# File patterns to ignore
|
|
45
|
+
ignore_patterns: list[str] = field(
|
|
46
|
+
default_factory=lambda: [
|
|
47
|
+
"tests/**", # Don't enforce in test files by default
|
|
48
|
+
"**/__pycache__/**",
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, config_dict: dict[str, Any]) -> "LazyIgnoresConfig":
|
|
54
|
+
"""Create config from dictionary."""
|
|
55
|
+
return cls(
|
|
56
|
+
check_noqa=config_dict.get("check_noqa", True),
|
|
57
|
+
check_type_ignore=config_dict.get("check_type_ignore", True),
|
|
58
|
+
check_pylint_disable=config_dict.get("check_pylint_disable", True),
|
|
59
|
+
check_nosec=config_dict.get("check_nosec", True),
|
|
60
|
+
check_ts_ignore=config_dict.get("check_ts_ignore", True),
|
|
61
|
+
check_eslint_disable=config_dict.get("check_eslint_disable", True),
|
|
62
|
+
check_thailint_ignore=config_dict.get("check_thailint_ignore", True),
|
|
63
|
+
check_test_skips=config_dict.get("check_test_skips", True),
|
|
64
|
+
check_orphaned=config_dict.get("check_orphaned", True),
|
|
65
|
+
ignore_patterns=config_dict.get("ignore_patterns", []),
|
|
66
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared utility functions for creating IgnoreDirective objects
|
|
3
|
+
|
|
4
|
+
Scope: Common directive creation and path normalization for ignore detectors
|
|
5
|
+
|
|
6
|
+
Overview: Provides shared utility functions used across Python, TypeScript, and test skip
|
|
7
|
+
detectors. Centralizes logic for normalizing file paths, extracting rule IDs from
|
|
8
|
+
regex matches, and creating IgnoreDirective objects to avoid code duplication.
|
|
9
|
+
|
|
10
|
+
Dependencies: re for match handling, pathlib for file paths, types module for dataclasses
|
|
11
|
+
|
|
12
|
+
Exports: normalize_path, extract_rule_ids, create_directive, create_directive_no_rules
|
|
13
|
+
|
|
14
|
+
Interfaces: Pure utility functions with no state
|
|
15
|
+
|
|
16
|
+
Implementation: Simple helper functions for directive creation
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_path(file_path: Path | str | None) -> Path:
|
|
26
|
+
"""Normalize file path to Path object.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Path object, string path, or None
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path object, defaults to Path("unknown") if None
|
|
33
|
+
"""
|
|
34
|
+
if file_path is None:
|
|
35
|
+
return Path("unknown")
|
|
36
|
+
if isinstance(file_path, str):
|
|
37
|
+
return Path(file_path)
|
|
38
|
+
return file_path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_captured_group(match: re.Match[str]) -> str | None:
|
|
42
|
+
"""Get the first captured group from a regex match if it exists.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
match: Regex match object
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Captured group text or None if no capture groups
|
|
49
|
+
"""
|
|
50
|
+
if match.lastindex is None or match.lastindex < 1:
|
|
51
|
+
return None
|
|
52
|
+
return match.group(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract_rule_ids(match: re.Match[str]) -> list[str]:
|
|
56
|
+
"""Extract rule IDs from regex match group 1.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
match: Regex match object with optional group 1 containing rule IDs
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of rule ID strings, empty if no specific rules
|
|
63
|
+
"""
|
|
64
|
+
group = _get_captured_group(match)
|
|
65
|
+
if not group:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
ids = [rule_id.strip() for rule_id in group.split(",")]
|
|
69
|
+
return [rule_id for rule_id in ids if rule_id]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_directive(
|
|
73
|
+
match: re.Match[str],
|
|
74
|
+
ignore_type: IgnoreType,
|
|
75
|
+
line_num: int,
|
|
76
|
+
file_path: Path,
|
|
77
|
+
rule_ids: tuple[str, ...] | None = None,
|
|
78
|
+
) -> IgnoreDirective:
|
|
79
|
+
"""Create an IgnoreDirective from a regex match.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
match: Regex match object
|
|
83
|
+
ignore_type: Type of ignore pattern
|
|
84
|
+
line_num: 1-indexed line number
|
|
85
|
+
file_path: Path to source file
|
|
86
|
+
rule_ids: Optional tuple of rule IDs; if None, extracts from match group 1
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
IgnoreDirective for this match
|
|
90
|
+
"""
|
|
91
|
+
if rule_ids is None:
|
|
92
|
+
rule_ids = tuple(extract_rule_ids(match))
|
|
93
|
+
|
|
94
|
+
return IgnoreDirective(
|
|
95
|
+
ignore_type=ignore_type,
|
|
96
|
+
rule_ids=rule_ids,
|
|
97
|
+
line=line_num,
|
|
98
|
+
column=match.start() + 1,
|
|
99
|
+
raw_text=match.group(0).strip(),
|
|
100
|
+
file_path=file_path,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def create_directive_no_rules(
|
|
105
|
+
match: re.Match[str],
|
|
106
|
+
ignore_type: IgnoreType,
|
|
107
|
+
line_num: int,
|
|
108
|
+
file_path: Path,
|
|
109
|
+
) -> IgnoreDirective:
|
|
110
|
+
"""Create an IgnoreDirective without rule IDs (for patterns like test skips).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
match: Regex match object
|
|
114
|
+
ignore_type: Type of ignore pattern
|
|
115
|
+
line_num: 1-indexed line number
|
|
116
|
+
file_path: Path to source file
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
IgnoreDirective with empty rule_ids tuple
|
|
120
|
+
"""
|
|
121
|
+
return create_directive(match, ignore_type, line_num, file_path, rule_ids=())
|