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
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/violation_builder.py
|
|
3
2
|
Purpose: Builds violation messages for file header linter
|
|
4
|
-
Exports: ViolationBuilder class
|
|
5
|
-
Depends: Violation type from core
|
|
6
|
-
Implements: Message templates with context-specific details
|
|
7
|
-
Related: linter.py for builder usage, atemporal_detector.py for temporal violations
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Scope: Violation message creation for file header validation failures
|
|
5
|
+
|
|
6
|
+
Overview: Creates formatted violation messages for file header validation failures.
|
|
11
7
|
Handles missing fields, atemporal language, and other header issues with clear,
|
|
12
|
-
actionable messages. Provides consistent violation format across all validation types
|
|
8
|
+
actionable messages. Provides consistent violation format across all validation types
|
|
9
|
+
including rule_id, message, location, severity, and helpful suggestions. Supports
|
|
10
|
+
multiple violation types with appropriate error messages and remediation guidance.
|
|
11
|
+
|
|
12
|
+
Dependencies: Violation and Severity types from core.types module
|
|
13
|
+
|
|
14
|
+
Exports: ViolationBuilder class
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
violation = builder.build_missing_field("Purpose", "test.py", 1)
|
|
16
|
+
Interfaces: build_missing_field(field_name, file_path, line) -> Violation,
|
|
17
|
+
build_atemporal_violation(pattern, description, file_path, line) -> Violation
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Implementation: Builder pattern with message templates for different violation types
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
from src.core.types import Severity, Violation
|
|
@@ -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)
|
|
@@ -23,6 +23,10 @@ from typing import Any
|
|
|
23
23
|
class DirectoryMatcher:
|
|
24
24
|
"""Finds matching directory rules based on path prefixes."""
|
|
25
25
|
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the directory matcher."""
|
|
28
|
+
pass # Stateless matcher for directory rules
|
|
29
|
+
|
|
26
30
|
def find_matching_rule(
|
|
27
31
|
self, path_str: str, directories: dict[str, Any]
|
|
28
32
|
) -> tuple[dict[str, Any] | None, str | None]:
|
|
@@ -6,25 +6,28 @@ Scope: Validate file organization against allow/deny patterns
|
|
|
6
6
|
Overview: Implements file placement validation using regex patterns from JSON/YAML config.
|
|
7
7
|
Orchestrates configuration loading, pattern validation, path resolution, rule checking,
|
|
8
8
|
and violation creation through focused helper classes. Supports directory-specific rules,
|
|
9
|
-
global patterns, and generates helpful suggestions. Main linter class acts as coordinator
|
|
9
|
+
global patterns, and generates helpful suggestions. Main linter class acts as coordinator
|
|
10
|
+
using composition pattern with specialized helper classes for configuration loading,
|
|
11
|
+
path resolution, pattern matching, and violation creation.
|
|
10
12
|
|
|
11
|
-
Dependencies: src.core (base classes, types), pathlib, typing
|
|
13
|
+
Dependencies: src.core (base classes, types), pathlib, typing, json, yaml modules
|
|
12
14
|
|
|
13
15
|
Exports: FilePlacementLinter, FilePlacementRule
|
|
14
16
|
|
|
17
|
+
Interfaces: lint_path(file_path) -> list[Violation], check_file_allowed(file_path) -> bool,
|
|
18
|
+
lint_directory(dir_path) -> list[Violation]
|
|
19
|
+
|
|
15
20
|
Implementation: Composition pattern with helper classes for each responsibility
|
|
21
|
+
(ConfigLoader, PathResolver, PatternMatcher, PatternValidator, RuleChecker,
|
|
22
|
+
ViolationFactory)
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
multiple config formats (wrapped vs unwrapped), project root detection with fallbacks,
|
|
21
|
-
and linter caching. This complexity is inherent to adapter pattern - splitting would
|
|
22
|
-
create unnecessary indirection between framework and implementation without improving
|
|
23
|
-
maintainability. All methods are focused on the single responsibility of integrating
|
|
24
|
-
file placement validation with the linting framework.
|
|
24
|
+
Suppressions:
|
|
25
|
+
- srp.violation: Rule class coordinates multiple helper classes for comprehensive
|
|
26
|
+
file placement validation. Method count reflects composition orchestration.
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
import json
|
|
30
|
+
from contextlib import suppress
|
|
28
31
|
from pathlib import Path
|
|
29
32
|
from typing import Any
|
|
30
33
|
|
|
@@ -74,21 +77,31 @@ class FilePlacementLinter:
|
|
|
74
77
|
|
|
75
78
|
# Load and validate config
|
|
76
79
|
if config_obj:
|
|
77
|
-
|
|
78
|
-
# Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
|
|
79
|
-
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
80
|
-
# Try both hyphenated and underscored keys for backward compatibility
|
|
81
|
-
self.config = config_obj.get(
|
|
82
|
-
"file-placement", config_obj.get("file_placement", config_obj)
|
|
83
|
-
)
|
|
80
|
+
self.config = self._unwrap_config(config_obj)
|
|
84
81
|
elif config_file:
|
|
85
|
-
|
|
82
|
+
raw_config = self._components.config_loader.load_config_file(config_file)
|
|
83
|
+
self.config = self._unwrap_config(raw_config)
|
|
86
84
|
else:
|
|
87
85
|
self.config = {}
|
|
88
86
|
|
|
89
87
|
# Validate regex patterns in config
|
|
90
88
|
self._components.pattern_validator.validate_config(self.config)
|
|
91
89
|
|
|
90
|
+
def _unwrap_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
"""Unwrap file-placement config from wrapper if present.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
config: Raw config dict (may be wrapped or unwrapped)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Unwrapped file-placement config dict
|
|
98
|
+
"""
|
|
99
|
+
# Handle both wrapped and unwrapped config formats
|
|
100
|
+
# Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
|
|
101
|
+
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
102
|
+
# Try both hyphenated and underscored keys for backward compatibility
|
|
103
|
+
return config.get("file-placement", config.get("file_placement", config))
|
|
104
|
+
|
|
92
105
|
def lint_path(self, file_path: Path) -> list[Violation]:
|
|
93
106
|
"""Lint a single file path.
|
|
94
107
|
|
|
@@ -126,20 +139,40 @@ class FilePlacementLinter:
|
|
|
126
139
|
Returns:
|
|
127
140
|
List of all violations found
|
|
128
141
|
"""
|
|
129
|
-
|
|
142
|
+
valid_files = self._get_valid_files(dir_path, recursive)
|
|
143
|
+
return self._lint_files(valid_files)
|
|
144
|
+
|
|
145
|
+
def _get_valid_files(self, dir_path: Path, recursive: bool) -> list[Path]:
|
|
146
|
+
"""Get list of valid files to lint from directory.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
dir_path: Directory to scan
|
|
150
|
+
recursive: Scan recursively
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of file paths to lint
|
|
154
|
+
"""
|
|
155
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
130
156
|
|
|
131
|
-
ignore_parser =
|
|
157
|
+
ignore_parser = get_ignore_parser(self.project_root)
|
|
132
158
|
pattern = "**/*" if recursive else "*"
|
|
133
159
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
160
|
+
return [
|
|
161
|
+
f for f in dir_path.glob(pattern) if f.is_file() and not ignore_parser.is_ignored(f)
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def _lint_files(self, file_paths: list[Path]) -> list[Violation]:
|
|
165
|
+
"""Lint multiple files and collect violations.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
file_paths: List of file paths to lint
|
|
142
169
|
|
|
170
|
+
Returns:
|
|
171
|
+
List of all violations found
|
|
172
|
+
"""
|
|
173
|
+
violations = []
|
|
174
|
+
for file_path in file_paths:
|
|
175
|
+
violations.extend(self.lint_path(file_path))
|
|
143
176
|
return violations
|
|
144
177
|
|
|
145
178
|
|
|
@@ -295,10 +328,10 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
295
328
|
if not hasattr(context, "metadata"):
|
|
296
329
|
return None
|
|
297
330
|
# Try hyphenated format first (original format)
|
|
298
|
-
|
|
331
|
+
with suppress(KeyError):
|
|
299
332
|
return context.metadata["file-placement"]
|
|
300
333
|
# Try underscored format (normalized format)
|
|
301
|
-
|
|
334
|
+
with suppress(KeyError):
|
|
302
335
|
return context.metadata["file_placement"]
|
|
303
336
|
return None
|
|
304
337
|
|
|
@@ -332,7 +365,7 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
332
365
|
FilePlacementLinter instance
|
|
333
366
|
"""
|
|
334
367
|
# Check if cached linter exists for this project root
|
|
335
|
-
|
|
368
|
+
with suppress(KeyError):
|
|
336
369
|
return self._linter_cache[project_root]
|
|
337
370
|
|
|
338
371
|
# Try to get config from context metadata (orchestrator passes config here)
|
|
@@ -388,11 +421,10 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
388
421
|
config = self._parse_layout_file(layout_path)
|
|
389
422
|
|
|
390
423
|
# Unwrap file-placement key if present (try both formats for backward compatibility)
|
|
391
|
-
|
|
424
|
+
with suppress(KeyError):
|
|
392
425
|
return config["file-placement"]
|
|
393
|
-
|
|
426
|
+
with suppress(KeyError):
|
|
394
427
|
return config["file_placement"]
|
|
395
|
-
|
|
396
428
|
return config
|
|
397
429
|
except Exception:
|
|
398
430
|
return {}
|
|
@@ -18,30 +18,65 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from re import Pattern
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class PatternMatcher:
|
|
24
26
|
"""Handles regex pattern matching for file paths."""
|
|
25
27
|
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the pattern matcher with compiled regex cache."""
|
|
30
|
+
self._compiled_patterns: dict[str, Pattern[str]] = {}
|
|
31
|
+
|
|
32
|
+
def _get_compiled(self, pattern: str) -> Pattern[str]:
|
|
33
|
+
"""Get compiled regex pattern, caching for reuse.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pattern: Regex pattern string
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Compiled regex Pattern object
|
|
40
|
+
"""
|
|
41
|
+
if pattern not in self._compiled_patterns:
|
|
42
|
+
self._compiled_patterns[pattern] = re.compile(pattern, re.IGNORECASE)
|
|
43
|
+
return self._compiled_patterns[pattern]
|
|
44
|
+
|
|
26
45
|
def match_deny_patterns(
|
|
27
|
-
self, path_str: str, deny_patterns:
|
|
46
|
+
self, path_str: str, deny_patterns: Sequence[dict[str, str] | str]
|
|
28
47
|
) -> tuple[bool, str | None]:
|
|
29
48
|
"""Check if path matches any deny patterns.
|
|
30
49
|
|
|
31
50
|
Args:
|
|
32
51
|
path_str: File path to check
|
|
33
|
-
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)
|
|
34
54
|
|
|
35
55
|
Returns:
|
|
36
56
|
Tuple of (is_denied, reason)
|
|
37
57
|
"""
|
|
38
58
|
for deny_item in deny_patterns:
|
|
39
|
-
pattern = deny_item
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
pattern, reason = self._extract_pattern_and_reason(deny_item)
|
|
60
|
+
compiled = self._get_compiled(pattern)
|
|
61
|
+
if compiled.search(path_str):
|
|
42
62
|
return True, reason
|
|
43
63
|
return False, None
|
|
44
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
|
+
|
|
45
80
|
def match_allow_patterns(self, path_str: str, allow_patterns: list[str]) -> bool:
|
|
46
81
|
"""Check if path matches any allow patterns.
|
|
47
82
|
|
|
@@ -52,4 +87,4 @@ class PatternMatcher:
|
|
|
52
87
|
Returns:
|
|
53
88
|
True if path matches any pattern
|
|
54
89
|
"""
|
|
55
|
-
return any(
|
|
90
|
+
return any(self._get_compiled(pattern).search(path_str) for pattern in allow_patterns)
|
|
@@ -18,25 +18,44 @@ Implementation: Uses re.compile() to test pattern validity, provides detailed er
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
|
+
from contextlib import suppress
|
|
21
22
|
from typing import Any
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _extract_pattern(deny_item: dict[str, str] | str) -> str:
|
|
26
|
+
"""Extract pattern from a deny item.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
deny_item: Either a dict with 'pattern' key or a plain string pattern
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The pattern string
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(deny_item, str):
|
|
35
|
+
return deny_item
|
|
36
|
+
return deny_item.get("pattern", "")
|
|
37
|
+
|
|
38
|
+
|
|
24
39
|
class PatternValidator:
|
|
25
40
|
"""Validates regex patterns in file placement configuration."""
|
|
26
41
|
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""Initialize the pattern validator."""
|
|
44
|
+
pass # Stateless validator for regex patterns
|
|
45
|
+
|
|
27
46
|
def validate_config(self, config: dict[str, Any]) -> None:
|
|
28
47
|
"""Validate all regex patterns in configuration.
|
|
29
48
|
|
|
30
49
|
Args:
|
|
31
|
-
config:
|
|
50
|
+
config: File placement configuration dict (already unwrapped)
|
|
32
51
|
|
|
33
52
|
Raises:
|
|
34
53
|
ValueError: If any regex pattern is invalid
|
|
35
54
|
"""
|
|
36
|
-
|
|
37
|
-
self._validate_directory_patterns(
|
|
38
|
-
self._validate_global_patterns(
|
|
39
|
-
self._validate_global_deny_patterns(
|
|
55
|
+
# Config is already unwrapped from file-placement key by FilePlacementLinter
|
|
56
|
+
self._validate_directory_patterns(config)
|
|
57
|
+
self._validate_global_patterns(config)
|
|
58
|
+
self._validate_global_deny_patterns(config)
|
|
40
59
|
|
|
41
60
|
def _validate_pattern(self, pattern: str) -> None:
|
|
42
61
|
"""Validate a single regex pattern.
|
|
@@ -58,7 +77,7 @@ class PatternValidator:
|
|
|
58
77
|
Args:
|
|
59
78
|
rules: Rules dictionary containing allow patterns
|
|
60
79
|
"""
|
|
61
|
-
|
|
80
|
+
with suppress(KeyError):
|
|
62
81
|
for pattern in rules["allow"]:
|
|
63
82
|
self._validate_pattern(pattern)
|
|
64
83
|
|
|
@@ -68,9 +87,9 @@ class PatternValidator:
|
|
|
68
87
|
Args:
|
|
69
88
|
rules: Rules dictionary containing deny patterns
|
|
70
89
|
"""
|
|
71
|
-
|
|
90
|
+
with suppress(KeyError):
|
|
72
91
|
for deny_item in rules["deny"]:
|
|
73
|
-
pattern = deny_item
|
|
92
|
+
pattern = _extract_pattern(deny_item)
|
|
74
93
|
self._validate_pattern(pattern)
|
|
75
94
|
|
|
76
95
|
def _validate_directory_patterns(self, fp_config: dict[str, Any]) -> None:
|
|
@@ -79,7 +98,7 @@ class PatternValidator:
|
|
|
79
98
|
Args:
|
|
80
99
|
fp_config: File placement configuration section
|
|
81
100
|
"""
|
|
82
|
-
|
|
101
|
+
with suppress(KeyError):
|
|
83
102
|
for _dir_path, rules in fp_config["directories"].items():
|
|
84
103
|
self._validate_allow_patterns(rules)
|
|
85
104
|
self._validate_deny_patterns(rules)
|
|
@@ -90,7 +109,7 @@ class PatternValidator:
|
|
|
90
109
|
Args:
|
|
91
110
|
fp_config: File placement configuration section
|
|
92
111
|
"""
|
|
93
|
-
|
|
112
|
+
with suppress(KeyError):
|
|
94
113
|
self._validate_allow_patterns(fp_config["global_patterns"])
|
|
95
114
|
self._validate_deny_patterns(fp_config["global_patterns"])
|
|
96
115
|
|
|
@@ -100,7 +119,7 @@ class PatternValidator:
|
|
|
100
119
|
Args:
|
|
101
120
|
fp_config: File placement configuration section
|
|
102
121
|
"""
|
|
103
|
-
|
|
122
|
+
with suppress(KeyError):
|
|
104
123
|
for deny_item in fp_config["global_deny"]:
|
|
105
|
-
pattern = deny_item
|
|
124
|
+
pattern = _extract_pattern(deny_item)
|
|
106
125
|
self._validate_pattern(pattern)
|
|
@@ -19,6 +19,7 @@ Implementation: Checks deny before allow, delegates directory matching to Direct
|
|
|
19
19
|
uses RuleCheckContext dataclass to reduce parameter duplication
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
from contextlib import suppress
|
|
22
23
|
from dataclasses import dataclass
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
from typing import Any
|
|
@@ -76,17 +77,17 @@ class RuleChecker:
|
|
|
76
77
|
"""
|
|
77
78
|
violations: list[Violation] = []
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
with suppress(KeyError):
|
|
80
81
|
dir_violations = self._check_directory_rules(
|
|
81
82
|
path_str, rel_path, fp_config["directories"]
|
|
82
83
|
)
|
|
83
84
|
violations.extend(dir_violations)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
with suppress(KeyError):
|
|
86
87
|
deny_violations = self._check_global_deny(path_str, rel_path, fp_config["global_deny"])
|
|
87
88
|
violations.extend(deny_violations)
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
with suppress(KeyError):
|
|
90
91
|
global_violations = self._check_global_patterns(
|
|
91
92
|
path_str, rel_path, fp_config["global_patterns"]
|
|
92
93
|
)
|
|
@@ -177,14 +178,14 @@ class RuleChecker:
|
|
|
177
178
|
return [violation] if violation else []
|
|
178
179
|
|
|
179
180
|
def _check_global_deny(
|
|
180
|
-
self, path_str: str, rel_path: Path, global_deny: list[dict[str, str]]
|
|
181
|
+
self, path_str: str, rel_path: Path, global_deny: list[dict[str, str] | str]
|
|
181
182
|
) -> list[Violation]:
|
|
182
183
|
"""Check file against global deny patterns.
|
|
183
184
|
|
|
184
185
|
Args:
|
|
185
186
|
path_str: Normalized path string
|
|
186
187
|
rel_path: Relative path object
|
|
187
|
-
global_deny: Global deny patterns
|
|
188
|
+
global_deny: Global deny patterns (dicts with pattern/reason or plain strings)
|
|
188
189
|
|
|
189
190
|
Returns:
|
|
190
191
|
List of violations
|
|
@@ -209,21 +210,25 @@ class RuleChecker:
|
|
|
209
210
|
List of violations
|
|
210
211
|
"""
|
|
211
212
|
# Check deny patterns first
|
|
212
|
-
|
|
213
|
+
try:
|
|
213
214
|
is_denied, reason = self.pattern_matcher.match_deny_patterns(
|
|
214
215
|
path_str, global_patterns["deny"]
|
|
215
216
|
)
|
|
216
217
|
if is_denied:
|
|
217
218
|
violation = self.violation_factory.create_global_deny_violation(rel_path, reason)
|
|
218
219
|
return self._wrap_violation(violation)
|
|
220
|
+
except KeyError:
|
|
221
|
+
pass # No deny patterns
|
|
219
222
|
|
|
220
223
|
# Check allow patterns
|
|
221
|
-
|
|
224
|
+
try:
|
|
222
225
|
is_allowed = self.pattern_matcher.match_allow_patterns(
|
|
223
226
|
path_str, global_patterns["allow"]
|
|
224
227
|
)
|
|
225
228
|
if not is_allowed:
|
|
226
229
|
violation = self.violation_factory.create_global_allow_violation(rel_path)
|
|
227
230
|
return self._wrap_violation(violation)
|
|
231
|
+
except KeyError:
|
|
232
|
+
pass # No allow patterns
|
|
228
233
|
|
|
229
234
|
return []
|
|
@@ -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,74 @@
|
|
|
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, pyright, typescript, eslint, thailint).
|
|
8
|
+
Includes orphaned detection toggle and file pattern ignores. Configuration can be
|
|
9
|
+
loaded from 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_pyright_ignore: bool = True
|
|
37
|
+
check_ts_ignore: bool = True
|
|
38
|
+
check_eslint_disable: bool = True
|
|
39
|
+
check_thailint_ignore: bool = True
|
|
40
|
+
check_test_skips: bool = True
|
|
41
|
+
|
|
42
|
+
# Orphaned detection
|
|
43
|
+
check_orphaned: bool = True # Header entries without matching ignores
|
|
44
|
+
|
|
45
|
+
# Inline justification options
|
|
46
|
+
allow_inline_justifications: bool = True # Allow " - reason" syntax
|
|
47
|
+
min_justification_length: int = 10 # Minimum chars for valid justification
|
|
48
|
+
|
|
49
|
+
# File patterns to ignore
|
|
50
|
+
ignore_patterns: list[str] = field(
|
|
51
|
+
default_factory=lambda: [
|
|
52
|
+
"tests/**", # Don't enforce in test files by default
|
|
53
|
+
"**/__pycache__/**",
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, config_dict: dict[str, Any]) -> "LazyIgnoresConfig":
|
|
59
|
+
"""Create config from dictionary."""
|
|
60
|
+
return cls(
|
|
61
|
+
check_noqa=config_dict.get("check_noqa", True),
|
|
62
|
+
check_type_ignore=config_dict.get("check_type_ignore", True),
|
|
63
|
+
check_pylint_disable=config_dict.get("check_pylint_disable", True),
|
|
64
|
+
check_nosec=config_dict.get("check_nosec", True),
|
|
65
|
+
check_pyright_ignore=config_dict.get("check_pyright_ignore", True),
|
|
66
|
+
check_ts_ignore=config_dict.get("check_ts_ignore", True),
|
|
67
|
+
check_eslint_disable=config_dict.get("check_eslint_disable", True),
|
|
68
|
+
check_thailint_ignore=config_dict.get("check_thailint_ignore", True),
|
|
69
|
+
check_test_skips=config_dict.get("check_test_skips", True),
|
|
70
|
+
check_orphaned=config_dict.get("check_orphaned", True),
|
|
71
|
+
allow_inline_justifications=config_dict.get("allow_inline_justifications", True),
|
|
72
|
+
min_justification_length=config_dict.get("min_justification_length", 10),
|
|
73
|
+
ignore_patterns=config_dict.get("ignore_patterns", []),
|
|
74
|
+
)
|