thailint 0.1.5__py3-none-any.whl → 0.5.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/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Path resolution and normalization for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Handles path operations including relative path calculation and normalization
|
|
5
|
+
|
|
6
|
+
Overview: Provides path resolution utilities for the file placement linter. Converts
|
|
7
|
+
absolute paths to paths relative to project root, normalizes path separators for
|
|
8
|
+
cross-platform compatibility, and handles edge cases like paths outside project root.
|
|
9
|
+
Isolates path manipulation logic from rule checking and pattern matching.
|
|
10
|
+
|
|
11
|
+
Dependencies: pathlib
|
|
12
|
+
|
|
13
|
+
Exports: PathResolver
|
|
14
|
+
|
|
15
|
+
Interfaces: get_relative_path(file_path) -> Path, normalize_path_string(path) -> str
|
|
16
|
+
|
|
17
|
+
Implementation: Uses pathlib for robust path operations, handles ValueError for out-of-tree paths
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PathResolver:
|
|
24
|
+
"""Resolves and normalizes file paths for file placement linter."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_root: Path):
|
|
27
|
+
"""Initialize path resolver.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project_root: Project root directory
|
|
31
|
+
"""
|
|
32
|
+
self.project_root = project_root
|
|
33
|
+
|
|
34
|
+
def get_relative_path(self, file_path: Path) -> Path:
|
|
35
|
+
"""Get path relative to project root.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_path: File path to convert
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path relative to project root, or original path if outside project
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
if file_path.is_absolute():
|
|
45
|
+
return file_path.relative_to(self.project_root)
|
|
46
|
+
return file_path
|
|
47
|
+
except ValueError:
|
|
48
|
+
# If path is outside project root, return it as-is
|
|
49
|
+
# This allows detection of absolute paths in global_deny patterns
|
|
50
|
+
return file_path
|
|
51
|
+
|
|
52
|
+
def normalize_path_string(self, path: Path) -> str:
|
|
53
|
+
"""Normalize path to forward slashes for cross-platform consistency.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
path: Path to normalize
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path string with forward slashes
|
|
60
|
+
"""
|
|
61
|
+
return str(path).replace("\\", "/")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Pattern matching utilities for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Handles regex pattern matching for allow/deny file placement rules
|
|
5
|
+
|
|
6
|
+
Overview: Provides pattern matching functionality for the file placement linter. Matches
|
|
7
|
+
file paths against regex patterns for both allow and deny lists. Supports case-insensitive
|
|
8
|
+
matching and extracts denial reasons from configuration. Isolates pattern matching logic
|
|
9
|
+
from rule checking and configuration validation.
|
|
10
|
+
|
|
11
|
+
Dependencies: re
|
|
12
|
+
|
|
13
|
+
Exports: PatternMatcher
|
|
14
|
+
|
|
15
|
+
Interfaces: match_deny_patterns(path, patterns) -> (bool, reason), match_allow_patterns(path, patterns) -> bool
|
|
16
|
+
|
|
17
|
+
Implementation: Uses re.search() for pattern matching with IGNORECASE flag
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PatternMatcher:
|
|
24
|
+
"""Handles regex pattern matching for file paths."""
|
|
25
|
+
|
|
26
|
+
def match_deny_patterns(
|
|
27
|
+
self, path_str: str, deny_patterns: list[dict[str, str]]
|
|
28
|
+
) -> tuple[bool, str | None]:
|
|
29
|
+
"""Check if path matches any deny patterns.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
path_str: File path to check
|
|
33
|
+
deny_patterns: List of deny pattern dicts with 'pattern' and 'reason'
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (is_denied, reason)
|
|
37
|
+
"""
|
|
38
|
+
for deny_item in deny_patterns:
|
|
39
|
+
pattern = deny_item["pattern"]
|
|
40
|
+
if re.search(pattern, path_str, re.IGNORECASE):
|
|
41
|
+
reason = deny_item.get("reason", "File not allowed in this location")
|
|
42
|
+
return True, reason
|
|
43
|
+
return False, None
|
|
44
|
+
|
|
45
|
+
def match_allow_patterns(self, path_str: str, allow_patterns: list[str]) -> bool:
|
|
46
|
+
"""Check if path matches any allow patterns.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path_str: File path to check
|
|
50
|
+
allow_patterns: List of regex patterns
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if path matches any pattern
|
|
54
|
+
"""
|
|
55
|
+
return any(re.search(pattern, path_str, re.IGNORECASE) for pattern in allow_patterns)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Regex pattern validation for file placement linter configuration
|
|
3
|
+
|
|
4
|
+
Scope: Validates regex patterns in configuration files to ensure they are compilable
|
|
5
|
+
|
|
6
|
+
Overview: Provides validation functionality for regex patterns used in file placement rules.
|
|
7
|
+
Validates patterns in allow/deny lists, directory-specific rules, and global patterns.
|
|
8
|
+
Raises descriptive errors when patterns are invalid, helping users debug configuration
|
|
9
|
+
issues early. Isolates pattern validation logic from rule checking and config loading.
|
|
10
|
+
|
|
11
|
+
Dependencies: re, typing
|
|
12
|
+
|
|
13
|
+
Exports: PatternValidator
|
|
14
|
+
|
|
15
|
+
Interfaces: validate_config(config) -> None (raises ValueError on invalid patterns)
|
|
16
|
+
|
|
17
|
+
Implementation: Uses re.compile() to test pattern validity, provides detailed error messages
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PatternValidator:
|
|
25
|
+
"""Validates regex patterns in file placement configuration."""
|
|
26
|
+
|
|
27
|
+
def validate_config(self, config: dict[str, Any]) -> None:
|
|
28
|
+
"""Validate all regex patterns in configuration.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: Full configuration dict
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If any regex pattern is invalid
|
|
35
|
+
"""
|
|
36
|
+
fp_config = config.get("file-placement", {})
|
|
37
|
+
self._validate_directory_patterns(fp_config)
|
|
38
|
+
self._validate_global_patterns(fp_config)
|
|
39
|
+
self._validate_global_deny_patterns(fp_config)
|
|
40
|
+
|
|
41
|
+
def _validate_pattern(self, pattern: str) -> None:
|
|
42
|
+
"""Validate a single regex pattern.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
pattern: Regex pattern to validate
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If pattern is invalid
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
re.compile(pattern)
|
|
52
|
+
except re.error as e:
|
|
53
|
+
raise ValueError(f"Invalid regex pattern '{pattern}': {e}") from e
|
|
54
|
+
|
|
55
|
+
def _validate_allow_patterns(self, rules: dict[str, Any]) -> None:
|
|
56
|
+
"""Validate allow patterns in a rules dict.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
rules: Rules dictionary containing allow patterns
|
|
60
|
+
"""
|
|
61
|
+
if "allow" in rules:
|
|
62
|
+
for pattern in rules["allow"]:
|
|
63
|
+
self._validate_pattern(pattern)
|
|
64
|
+
|
|
65
|
+
def _validate_deny_patterns(self, rules: dict[str, Any]) -> None:
|
|
66
|
+
"""Validate deny patterns in a rules dict.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
rules: Rules dictionary containing deny patterns
|
|
70
|
+
"""
|
|
71
|
+
if "deny" in rules:
|
|
72
|
+
for deny_item in rules["deny"]:
|
|
73
|
+
pattern = deny_item.get("pattern", "")
|
|
74
|
+
self._validate_pattern(pattern)
|
|
75
|
+
|
|
76
|
+
def _validate_directory_patterns(self, fp_config: dict[str, Any]) -> None:
|
|
77
|
+
"""Validate all directory-specific patterns.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
fp_config: File placement configuration section
|
|
81
|
+
"""
|
|
82
|
+
if "directories" in fp_config:
|
|
83
|
+
for _dir_path, rules in fp_config["directories"].items():
|
|
84
|
+
self._validate_allow_patterns(rules)
|
|
85
|
+
self._validate_deny_patterns(rules)
|
|
86
|
+
|
|
87
|
+
def _validate_global_patterns(self, fp_config: dict[str, Any]) -> None:
|
|
88
|
+
"""Validate global patterns section.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
fp_config: File placement configuration section
|
|
92
|
+
"""
|
|
93
|
+
if "global_patterns" in fp_config:
|
|
94
|
+
self._validate_allow_patterns(fp_config["global_patterns"])
|
|
95
|
+
self._validate_deny_patterns(fp_config["global_patterns"])
|
|
96
|
+
|
|
97
|
+
def _validate_global_deny_patterns(self, fp_config: dict[str, Any]) -> None:
|
|
98
|
+
"""Validate global_deny patterns.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
fp_config: File placement configuration section
|
|
102
|
+
"""
|
|
103
|
+
if "global_deny" in fp_config:
|
|
104
|
+
for deny_item in fp_config["global_deny"]:
|
|
105
|
+
pattern = deny_item.get("pattern", "")
|
|
106
|
+
self._validate_pattern(pattern)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rule checking logic for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Executes file placement rules including directory-specific and global patterns
|
|
5
|
+
|
|
6
|
+
Overview: Provides core rule checking functionality for the file placement linter. Checks
|
|
7
|
+
files against directory-specific allow/deny rules, global patterns, and global deny lists.
|
|
8
|
+
Uses pattern matcher for regex matching, directory matcher for finding rules, and violation
|
|
9
|
+
factory for creating violations. Implements deny-takes-precedence logic. Isolates rule
|
|
10
|
+
execution logic from configuration, validation, and violation creation.
|
|
11
|
+
|
|
12
|
+
Dependencies: pathlib, typing, dataclasses, PatternMatcher, DirectoryMatcher, ViolationFactory, src.core.types
|
|
13
|
+
|
|
14
|
+
Exports: RuleChecker
|
|
15
|
+
|
|
16
|
+
Interfaces: check_all_rules(path_str, rel_path, fp_config) -> list[Violation]
|
|
17
|
+
|
|
18
|
+
Implementation: Checks deny before allow, delegates directory matching to DirectoryMatcher,
|
|
19
|
+
uses RuleCheckContext dataclass to reduce parameter duplication
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from src.core.types import Violation
|
|
27
|
+
|
|
28
|
+
from .directory_matcher import DirectoryMatcher
|
|
29
|
+
from .pattern_matcher import PatternMatcher
|
|
30
|
+
from .violation_factory import ViolationFactory
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class RuleCheckContext:
|
|
35
|
+
"""Context information for rule checking.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
path_str: Normalized path string
|
|
39
|
+
rel_path: Relative path object
|
|
40
|
+
dir_rule: Directory rule configuration
|
|
41
|
+
matched_path: Matched directory path
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
path_str: str
|
|
45
|
+
rel_path: Path
|
|
46
|
+
dir_rule: dict[str, Any]
|
|
47
|
+
matched_path: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RuleChecker:
|
|
51
|
+
"""Checks file placement rules and returns violations."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, pattern_matcher: PatternMatcher, violation_factory: ViolationFactory):
|
|
54
|
+
"""Initialize rule checker.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
pattern_matcher: Pattern matcher for regex matching
|
|
58
|
+
violation_factory: Factory for creating violations
|
|
59
|
+
"""
|
|
60
|
+
self.pattern_matcher = pattern_matcher
|
|
61
|
+
self.violation_factory = violation_factory
|
|
62
|
+
self.directory_matcher = DirectoryMatcher()
|
|
63
|
+
|
|
64
|
+
def check_all_rules(
|
|
65
|
+
self, path_str: str, rel_path: Path, fp_config: dict[str, Any]
|
|
66
|
+
) -> list[Violation]:
|
|
67
|
+
"""Check all file placement rules.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path_str: Normalized path string
|
|
71
|
+
rel_path: Relative path object
|
|
72
|
+
fp_config: File placement configuration
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of violations found
|
|
76
|
+
"""
|
|
77
|
+
violations: list[Violation] = []
|
|
78
|
+
|
|
79
|
+
if "directories" in fp_config:
|
|
80
|
+
dir_violations = self._check_directory_rules(
|
|
81
|
+
path_str, rel_path, fp_config["directories"]
|
|
82
|
+
)
|
|
83
|
+
violations.extend(dir_violations)
|
|
84
|
+
|
|
85
|
+
if "global_deny" in fp_config:
|
|
86
|
+
deny_violations = self._check_global_deny(path_str, rel_path, fp_config["global_deny"])
|
|
87
|
+
violations.extend(deny_violations)
|
|
88
|
+
|
|
89
|
+
if "global_patterns" in fp_config:
|
|
90
|
+
global_violations = self._check_global_patterns(
|
|
91
|
+
path_str, rel_path, fp_config["global_patterns"]
|
|
92
|
+
)
|
|
93
|
+
violations.extend(global_violations)
|
|
94
|
+
|
|
95
|
+
return violations
|
|
96
|
+
|
|
97
|
+
def _check_directory_deny_rules(self, ctx: RuleCheckContext) -> Violation | None:
|
|
98
|
+
"""Check directory deny rules.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ctx: Rule check context with file and rule information
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Violation if denied, None otherwise
|
|
105
|
+
"""
|
|
106
|
+
if "deny" not in ctx.dir_rule:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
is_denied, reason = self.pattern_matcher.match_deny_patterns(
|
|
110
|
+
ctx.path_str, ctx.dir_rule["deny"]
|
|
111
|
+
)
|
|
112
|
+
if is_denied:
|
|
113
|
+
return self.violation_factory.create_deny_violation(
|
|
114
|
+
ctx.rel_path, ctx.matched_path, reason or "Pattern denied"
|
|
115
|
+
)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _check_directory_allow_rules(self, ctx: RuleCheckContext) -> Violation | None:
|
|
119
|
+
"""Check directory allow rules.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
ctx: Rule check context with file and rule information
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Violation if not allowed, None otherwise
|
|
126
|
+
"""
|
|
127
|
+
if "allow" not in ctx.dir_rule:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
is_allowed = self.pattern_matcher.match_allow_patterns(ctx.path_str, ctx.dir_rule["allow"])
|
|
131
|
+
if not is_allowed:
|
|
132
|
+
return self.violation_factory.create_allow_violation(ctx.rel_path, ctx.matched_path)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def _check_directory_rules(
|
|
136
|
+
self, path_str: str, rel_path: Path, directories: dict[str, Any]
|
|
137
|
+
) -> list[Violation]:
|
|
138
|
+
"""Check file against directory-specific rules.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
path_str: Normalized path string
|
|
142
|
+
rel_path: Relative path object
|
|
143
|
+
directories: Directory rules config
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of violations
|
|
147
|
+
"""
|
|
148
|
+
dir_rule, matched_path = self.directory_matcher.find_matching_rule(path_str, directories)
|
|
149
|
+
if not dir_rule or not matched_path:
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
ctx = RuleCheckContext(
|
|
153
|
+
path_str=path_str,
|
|
154
|
+
rel_path=rel_path,
|
|
155
|
+
dir_rule=dir_rule,
|
|
156
|
+
matched_path=matched_path,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Check deny patterns first (takes precedence)
|
|
160
|
+
deny_violation = self._check_directory_deny_rules(ctx)
|
|
161
|
+
if deny_violation:
|
|
162
|
+
return self._wrap_violation(deny_violation)
|
|
163
|
+
|
|
164
|
+
# Check allow patterns
|
|
165
|
+
allow_violation = self._check_directory_allow_rules(ctx)
|
|
166
|
+
return self._wrap_violation(allow_violation)
|
|
167
|
+
|
|
168
|
+
def _wrap_violation(self, violation: Violation | None) -> list[Violation]:
|
|
169
|
+
"""Wrap single violation in list, or return empty list if None.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
violation: Violation to wrap, or None
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List containing violation, or empty list
|
|
176
|
+
"""
|
|
177
|
+
return [violation] if violation else []
|
|
178
|
+
|
|
179
|
+
def _check_global_deny(
|
|
180
|
+
self, path_str: str, rel_path: Path, global_deny: list[dict[str, str]]
|
|
181
|
+
) -> list[Violation]:
|
|
182
|
+
"""Check file against global deny patterns.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path_str: Normalized path string
|
|
186
|
+
rel_path: Relative path object
|
|
187
|
+
global_deny: Global deny patterns
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of violations
|
|
191
|
+
"""
|
|
192
|
+
is_denied, reason = self.pattern_matcher.match_deny_patterns(path_str, global_deny)
|
|
193
|
+
if is_denied:
|
|
194
|
+
violation = self.violation_factory.create_global_deny_violation(rel_path, reason)
|
|
195
|
+
return self._wrap_violation(violation)
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
def _check_global_patterns(
|
|
199
|
+
self, path_str: str, rel_path: Path, global_patterns: dict[str, Any]
|
|
200
|
+
) -> list[Violation]:
|
|
201
|
+
"""Check file against global patterns.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
path_str: Normalized path string
|
|
205
|
+
rel_path: Relative path object
|
|
206
|
+
global_patterns: Global patterns config
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of violations
|
|
210
|
+
"""
|
|
211
|
+
# Check deny patterns first
|
|
212
|
+
if "deny" in global_patterns:
|
|
213
|
+
is_denied, reason = self.pattern_matcher.match_deny_patterns(
|
|
214
|
+
path_str, global_patterns["deny"]
|
|
215
|
+
)
|
|
216
|
+
if is_denied:
|
|
217
|
+
violation = self.violation_factory.create_global_deny_violation(rel_path, reason)
|
|
218
|
+
return self._wrap_violation(violation)
|
|
219
|
+
|
|
220
|
+
# Check allow patterns
|
|
221
|
+
if "allow" in global_patterns:
|
|
222
|
+
is_allowed = self.pattern_matcher.match_allow_patterns(
|
|
223
|
+
path_str, global_patterns["allow"]
|
|
224
|
+
)
|
|
225
|
+
if not is_allowed:
|
|
226
|
+
violation = self.violation_factory.create_global_allow_violation(rel_path)
|
|
227
|
+
return self._wrap_violation(violation)
|
|
228
|
+
|
|
229
|
+
return []
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation creation with helpful suggestions for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Creates Violation objects with contextual messages and actionable suggestions
|
|
5
|
+
|
|
6
|
+
Overview: Provides violation creation functionality for the file placement linter. Generates
|
|
7
|
+
descriptive error messages, creates helpful suggestions based on file type and location,
|
|
8
|
+
and encapsulates all violation construction logic. Separates violation building from
|
|
9
|
+
rule checking to maintain single responsibility and improve message consistency.
|
|
10
|
+
|
|
11
|
+
Dependencies: pathlib, src.core.types (Violation, Severity), src.core.violation_builder
|
|
12
|
+
|
|
13
|
+
Exports: ViolationFactory
|
|
14
|
+
|
|
15
|
+
Interfaces: create_deny_violation, create_allow_violation, create_global_deny_violation
|
|
16
|
+
|
|
17
|
+
Implementation: Uses file extension and name patterns to generate context-aware suggestions,
|
|
18
|
+
extends BaseViolationBuilder for consistent violation construction
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from src.core.types import Severity, Violation
|
|
24
|
+
from src.core.violation_builder import BaseViolationBuilder
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ViolationFactory(BaseViolationBuilder):
|
|
28
|
+
"""Creates violations with helpful suggestions for file placement linter."""
|
|
29
|
+
|
|
30
|
+
# Suggestion lookup by file type
|
|
31
|
+
_SUGGESTIONS = {
|
|
32
|
+
"test": "Move to tests/ directory",
|
|
33
|
+
"component": "Move to src/components/",
|
|
34
|
+
"source": "Move to src/",
|
|
35
|
+
"temp": "Move to debug/logs/ or add to .gitignore",
|
|
36
|
+
}
|
|
37
|
+
_DEFAULT_SUGGESTION = "Review file organization and move to appropriate directory"
|
|
38
|
+
|
|
39
|
+
def create_deny_violation(self, rel_path: Path, matched_path: str, reason: str) -> Violation:
|
|
40
|
+
"""Create violation for denied file.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
rel_path: Relative path to file
|
|
44
|
+
matched_path: Directory path that matched
|
|
45
|
+
reason: Reason file is denied
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Violation with message and suggestion
|
|
49
|
+
"""
|
|
50
|
+
message = f"File '{rel_path}' not allowed in {matched_path}: {reason}"
|
|
51
|
+
suggestion = self._get_suggestion(rel_path.name)
|
|
52
|
+
return self.build_from_params(
|
|
53
|
+
rule_id="file-placement",
|
|
54
|
+
file_path=str(rel_path),
|
|
55
|
+
line=1,
|
|
56
|
+
column=0,
|
|
57
|
+
message=message,
|
|
58
|
+
severity=Severity.ERROR,
|
|
59
|
+
suggestion=suggestion,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def create_allow_violation(self, rel_path: Path, matched_path: str) -> Violation:
|
|
63
|
+
"""Create violation for file not matching allow patterns.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
rel_path: Relative path to file
|
|
67
|
+
matched_path: Directory path that matched
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Violation with message and suggestion
|
|
71
|
+
"""
|
|
72
|
+
message = f"File '{rel_path}' does not match allowed patterns for {matched_path}"
|
|
73
|
+
suggestion = f"Move to {matched_path} or ensure file type is allowed"
|
|
74
|
+
return self.build_from_params(
|
|
75
|
+
rule_id="file-placement",
|
|
76
|
+
file_path=str(rel_path),
|
|
77
|
+
line=1,
|
|
78
|
+
column=0,
|
|
79
|
+
message=message,
|
|
80
|
+
severity=Severity.ERROR,
|
|
81
|
+
suggestion=suggestion,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def create_global_deny_violation(self, rel_path: Path, reason: str | None) -> Violation:
|
|
85
|
+
"""Create violation for global deny pattern match.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
rel_path: Relative path to file
|
|
89
|
+
reason: Reason file is denied (optional)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Violation with message and suggestion
|
|
93
|
+
"""
|
|
94
|
+
message = reason or f"File '{rel_path}' matches denied pattern"
|
|
95
|
+
suggestion = self._get_suggestion(rel_path.name)
|
|
96
|
+
return self.build_from_params(
|
|
97
|
+
rule_id="file-placement",
|
|
98
|
+
file_path=str(rel_path),
|
|
99
|
+
line=1,
|
|
100
|
+
column=0,
|
|
101
|
+
message=message,
|
|
102
|
+
severity=Severity.ERROR,
|
|
103
|
+
suggestion=suggestion,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def create_global_allow_violation(self, rel_path: Path) -> Violation:
|
|
107
|
+
"""Create violation for file not matching global allow patterns.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
rel_path: Relative path to file
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Violation with message and suggestion
|
|
114
|
+
"""
|
|
115
|
+
message = f"File '{rel_path}' does not match any allowed patterns"
|
|
116
|
+
suggestion = "Ensure file matches project structure patterns"
|
|
117
|
+
return self.build_from_params(
|
|
118
|
+
rule_id="file-placement",
|
|
119
|
+
file_path=str(rel_path),
|
|
120
|
+
line=1,
|
|
121
|
+
column=0,
|
|
122
|
+
message=message,
|
|
123
|
+
severity=Severity.ERROR,
|
|
124
|
+
suggestion=suggestion,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _is_temp_file(self, filename: str) -> bool:
|
|
128
|
+
"""Check if file is a temporary or utility file.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
filename: File name
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if file is temporary/utility file
|
|
135
|
+
"""
|
|
136
|
+
return filename.startswith(("debug", "temp")) or filename.endswith(".log")
|
|
137
|
+
|
|
138
|
+
def _classify_file_type(self, filename: str) -> str | None:
|
|
139
|
+
"""Classify file type based on filename patterns.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
filename: File name
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
File type classification key, or None if no pattern matches
|
|
146
|
+
"""
|
|
147
|
+
filename_lower = filename.lower()
|
|
148
|
+
|
|
149
|
+
# Check keyword-based patterns
|
|
150
|
+
if "test" in filename_lower:
|
|
151
|
+
return "test"
|
|
152
|
+
if "component" in filename_lower:
|
|
153
|
+
return "component"
|
|
154
|
+
|
|
155
|
+
# Check extension-based patterns
|
|
156
|
+
if filename.endswith((".ts", ".tsx", ".jsx", ".py")):
|
|
157
|
+
return "source"
|
|
158
|
+
|
|
159
|
+
# Check temp/utility patterns
|
|
160
|
+
if self._is_temp_file(filename):
|
|
161
|
+
return "temp"
|
|
162
|
+
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _get_suggestion(self, filename: str) -> str:
|
|
166
|
+
"""Get suggestion for file placement based on filename patterns.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
filename: File name
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Suggestion string with actionable guidance
|
|
173
|
+
"""
|
|
174
|
+
file_type = self._classify_file_type(filename)
|
|
175
|
+
if file_type is None:
|
|
176
|
+
return self._DEFAULT_SUGGESTION
|
|
177
|
+
return self._SUGGESTIONS.get(file_type, self._DEFAULT_SUGGESTION)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Magic numbers linter package exports and convenience functions
|
|
3
|
+
|
|
4
|
+
Scope: Public API for magic numbers linter module
|
|
5
|
+
|
|
6
|
+
Overview: Provides the public interface for the magic numbers linter package. Exports main
|
|
7
|
+
MagicNumberRule class for use by the orchestrator and MagicNumberConfig for configuration.
|
|
8
|
+
Includes lint() convenience function that provides a simple API for running the magic numbers
|
|
9
|
+
linter on a file or directory without directly interacting with the orchestrator. This module
|
|
10
|
+
serves as the entry point for users of the magic numbers linter, hiding implementation details
|
|
11
|
+
and exposing only the essential components needed for linting operations.
|
|
12
|
+
|
|
13
|
+
Dependencies: .linter for MagicNumberRule, .config for MagicNumberConfig
|
|
14
|
+
|
|
15
|
+
Exports: MagicNumberRule class, MagicNumberConfig dataclass, lint() convenience function
|
|
16
|
+
|
|
17
|
+
Interfaces: lint(path, config) -> list[Violation] for simple linting operations
|
|
18
|
+
|
|
19
|
+
Implementation: Module-level exports with __all__ definition, convenience function wrapper
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import MagicNumberConfig
|
|
23
|
+
from .linter import MagicNumberRule
|
|
24
|
+
|
|
25
|
+
__all__ = ["MagicNumberRule", "MagicNumberConfig", "lint"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def lint(file_path: str, config: dict | None = None) -> list:
|
|
29
|
+
"""Convenience function for linting a file for magic numbers.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
file_path: Path to the file to lint
|
|
33
|
+
config: Optional configuration dictionary
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of violations found
|
|
37
|
+
"""
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from src.orchestrator.core import FileLintContext
|
|
41
|
+
|
|
42
|
+
rule = MagicNumberRule()
|
|
43
|
+
context = FileLintContext(
|
|
44
|
+
path=Path(file_path),
|
|
45
|
+
lang="python",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return rule.check(context)
|