thailint 0.2.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 +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- 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 +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- 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 +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- 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 +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- 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/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -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/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- 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/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- 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 +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -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,18 +77,31 @@ class FilePlacementLinter:
|
|
|
74
77
|
|
|
75
78
|
# Load and validate config
|
|
76
79
|
if config_obj:
|
|
77
|
-
|
|
78
|
-
# Wrapped: {"file-placement": {...}}
|
|
79
|
-
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
80
|
-
self.config = config_obj.get("file-placement", config_obj)
|
|
80
|
+
self.config = self._unwrap_config(config_obj)
|
|
81
81
|
elif config_file:
|
|
82
|
-
|
|
82
|
+
raw_config = self._components.config_loader.load_config_file(config_file)
|
|
83
|
+
self.config = self._unwrap_config(raw_config)
|
|
83
84
|
else:
|
|
84
85
|
self.config = {}
|
|
85
86
|
|
|
86
87
|
# Validate regex patterns in config
|
|
87
88
|
self._components.pattern_validator.validate_config(self.config)
|
|
88
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
|
+
|
|
89
105
|
def lint_path(self, file_path: Path) -> list[Violation]:
|
|
90
106
|
"""Lint a single file path.
|
|
91
107
|
|
|
@@ -123,20 +139,40 @@ class FilePlacementLinter:
|
|
|
123
139
|
Returns:
|
|
124
140
|
List of all violations found
|
|
125
141
|
"""
|
|
126
|
-
|
|
142
|
+
valid_files = self._get_valid_files(dir_path, recursive)
|
|
143
|
+
return self._lint_files(valid_files)
|
|
127
144
|
|
|
128
|
-
|
|
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
|
|
156
|
+
|
|
157
|
+
ignore_parser = get_ignore_parser(self.project_root)
|
|
129
158
|
pattern = "**/*" if recursive else "*"
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
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))
|
|
140
176
|
return violations
|
|
141
177
|
|
|
142
178
|
|
|
@@ -279,7 +315,9 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
279
315
|
|
|
280
316
|
@staticmethod
|
|
281
317
|
def _get_wrapped_config(context: BaseLintContext) -> dict[str, Any] | None:
|
|
282
|
-
"""Get config from wrapped format: {"file-placement": {...}}.
|
|
318
|
+
"""Get config from wrapped format: {"file-placement": {...}} or {"file_placement": {...}}.
|
|
319
|
+
|
|
320
|
+
Supports both hyphenated and underscored keys for backward compatibility.
|
|
283
321
|
|
|
284
322
|
Args:
|
|
285
323
|
context: Lint context with metadata
|
|
@@ -289,8 +327,12 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
289
327
|
"""
|
|
290
328
|
if not hasattr(context, "metadata"):
|
|
291
329
|
return None
|
|
292
|
-
|
|
330
|
+
# Try hyphenated format first (original format)
|
|
331
|
+
with suppress(KeyError):
|
|
293
332
|
return context.metadata["file-placement"]
|
|
333
|
+
# Try underscored format (normalized format)
|
|
334
|
+
with suppress(KeyError):
|
|
335
|
+
return context.metadata["file_placement"]
|
|
294
336
|
return None
|
|
295
337
|
|
|
296
338
|
@staticmethod
|
|
@@ -323,7 +365,7 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
323
365
|
FilePlacementLinter instance
|
|
324
366
|
"""
|
|
325
367
|
# Check if cached linter exists for this project root
|
|
326
|
-
|
|
368
|
+
with suppress(KeyError):
|
|
327
369
|
return self._linter_cache[project_root]
|
|
328
370
|
|
|
329
371
|
# Try to get config from context metadata (orchestrator passes config here)
|
|
@@ -378,10 +420,11 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
378
420
|
try:
|
|
379
421
|
config = self._parse_layout_file(layout_path)
|
|
380
422
|
|
|
381
|
-
# Unwrap file-placement key if present
|
|
382
|
-
|
|
423
|
+
# Unwrap file-placement key if present (try both formats for backward compatibility)
|
|
424
|
+
with suppress(KeyError):
|
|
383
425
|
return config["file-placement"]
|
|
384
|
-
|
|
426
|
+
with suppress(KeyError):
|
|
427
|
+
return config["file_placement"]
|
|
385
428
|
return config
|
|
386
429
|
except Exception:
|
|
387
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
|
+
)
|