thailint 0.1.6__py3-none-any.whl → 0.2.1__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 +524 -141
- src/config.py +6 -31
- src/core/base.py +12 -0
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +99 -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 +262 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +218 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +130 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +126 -0
- src/linters/dry/file_analyzer.py +127 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +170 -0
- src/linters/dry/python_analyzer.py +517 -0
- src/linters/dry/storage_initializer.py +51 -0
- src/linters/dry/token_hasher.py +115 -0
- src/linters/dry/typescript_analyzer.py +590 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +91 -0
- src/linters/dry/violation_generator.py +174 -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 +252 -472
- 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/nesting/config.py +13 -3
- src/linters/nesting/linter.py +76 -152
- src/linters/nesting/typescript_analyzer.py +38 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +76 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +225 -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 +42 -7
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +84 -0
- {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/METADATA +414 -63
- thailint-0.2.1.dist-info/RECORD +75 -0
- src/.ai/layout.yaml +0 -48
- thailint-0.1.6.dist-info/RECORD +0 -28
- {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/LICENSE +0 -0
- {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/WHEEL +0 -0
- {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation overlap filtering
|
|
3
|
+
|
|
4
|
+
Scope: Filters overlapping violations within same file
|
|
5
|
+
|
|
6
|
+
Overview: Filters overlapping violations by comparing line ranges. When violations are close together
|
|
7
|
+
(within 3 lines), only the first one is kept. Used by ViolationDeduplicator to remove duplicate
|
|
8
|
+
reports from rolling hash windows.
|
|
9
|
+
|
|
10
|
+
Dependencies: Violation
|
|
11
|
+
|
|
12
|
+
Exports: ViolationFilter class
|
|
13
|
+
|
|
14
|
+
Interfaces: ViolationFilter.filter_overlapping(sorted_violations)
|
|
15
|
+
|
|
16
|
+
Implementation: Iterates through sorted violations, keeps first of each overlapping group
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from src.core.types import Violation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ViolationFilter:
|
|
23
|
+
"""Filters overlapping violations."""
|
|
24
|
+
|
|
25
|
+
def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
|
|
26
|
+
"""Filter overlapping violations, keeping first occurrence.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
sorted_violations: Violations sorted by line number
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Filtered list with overlaps removed
|
|
33
|
+
"""
|
|
34
|
+
kept: list[Violation] = []
|
|
35
|
+
for violation in sorted_violations:
|
|
36
|
+
if not self._overlaps_any(violation, kept):
|
|
37
|
+
kept.append(violation)
|
|
38
|
+
return kept
|
|
39
|
+
|
|
40
|
+
def _overlaps_any(self, violation: Violation, kept_violations: list[Violation]) -> bool:
|
|
41
|
+
"""Check if violation overlaps with any kept violations.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
violation: Violation to check
|
|
45
|
+
kept_violations: Previously kept violations
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if violation overlaps with any kept violation
|
|
49
|
+
"""
|
|
50
|
+
for kept in kept_violations:
|
|
51
|
+
if self._overlaps(violation, kept):
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def _overlaps(self, v1: Violation, v2: Violation) -> bool:
|
|
56
|
+
"""Check if two violations overlap.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
v1: First violation (later line number)
|
|
60
|
+
v2: Second violation (earlier line number)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if violations overlap based on code block size
|
|
64
|
+
"""
|
|
65
|
+
line1 = v1.line or 0
|
|
66
|
+
line2 = v2.line or 0
|
|
67
|
+
|
|
68
|
+
# Extract line count from message format: "Duplicate code (N lines, ...)"
|
|
69
|
+
line_count = self._extract_line_count(v1.message)
|
|
70
|
+
|
|
71
|
+
# Blocks overlap if their line ranges intersect
|
|
72
|
+
# Block at line2 covers [line2, line2 + line_count - 1]
|
|
73
|
+
# Block at line1 overlaps if line1 < line2 + line_count
|
|
74
|
+
return line1 < line2 + line_count
|
|
75
|
+
|
|
76
|
+
def _extract_line_count(self, message: str) -> int:
|
|
77
|
+
"""Extract line count from violation message.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
message: Violation message containing line count
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Number of lines in the duplicate code block (default 5 if not found)
|
|
84
|
+
"""
|
|
85
|
+
# Message format: "Duplicate code (5 lines, 2 occurrences)..."
|
|
86
|
+
try:
|
|
87
|
+
start = message.index("(") + 1
|
|
88
|
+
end = message.index(" lines")
|
|
89
|
+
return int(message[start:end])
|
|
90
|
+
except (ValueError, IndexError):
|
|
91
|
+
return 5 # Default fallback
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation generation from duplicate code blocks
|
|
3
|
+
|
|
4
|
+
Scope: Generates violations from duplicate hashes
|
|
5
|
+
|
|
6
|
+
Overview: Handles violation generation for duplicate code blocks. Queries storage for duplicate
|
|
7
|
+
hashes, retrieves blocks for each hash, deduplicates overlapping blocks, builds violations
|
|
8
|
+
using ViolationBuilder, and filters violations based on ignore patterns. Separates violation
|
|
9
|
+
generation logic from main linter rule to maintain SRP compliance.
|
|
10
|
+
|
|
11
|
+
Dependencies: DuplicateStorage, ViolationDeduplicator, DRYViolationBuilder, Violation, DRYConfig
|
|
12
|
+
|
|
13
|
+
Exports: ViolationGenerator class
|
|
14
|
+
|
|
15
|
+
Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
|
|
16
|
+
|
|
17
|
+
Implementation: Queries storage, deduplicates blocks, builds violations, filters by ignore patterns
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
from src.orchestrator.language_detector import detect_language
|
|
24
|
+
|
|
25
|
+
from .config import DRYConfig
|
|
26
|
+
from .deduplicator import ViolationDeduplicator
|
|
27
|
+
from .duplicate_storage import DuplicateStorage
|
|
28
|
+
from .inline_ignore import InlineIgnoreParser
|
|
29
|
+
from .violation_builder import DRYViolationBuilder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ViolationGenerator:
|
|
33
|
+
"""Generates violations from duplicate code blocks."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize with deduplicator and violation builder."""
|
|
37
|
+
self._deduplicator = ViolationDeduplicator()
|
|
38
|
+
self._violation_builder = DRYViolationBuilder()
|
|
39
|
+
|
|
40
|
+
def generate_violations(
|
|
41
|
+
self,
|
|
42
|
+
storage: DuplicateStorage,
|
|
43
|
+
rule_id: str,
|
|
44
|
+
config: DRYConfig,
|
|
45
|
+
inline_ignore: InlineIgnoreParser,
|
|
46
|
+
) -> list[Violation]:
|
|
47
|
+
"""Generate violations from storage.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
storage: Duplicate storage instance
|
|
51
|
+
rule_id: Rule identifier for violations
|
|
52
|
+
config: DRY configuration with ignore patterns
|
|
53
|
+
inline_ignore: Parser with inline ignore directives
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of violations filtered by ignore patterns and inline directives
|
|
57
|
+
"""
|
|
58
|
+
duplicate_hashes = storage.get_duplicate_hashes()
|
|
59
|
+
violations = []
|
|
60
|
+
|
|
61
|
+
for hash_value in duplicate_hashes:
|
|
62
|
+
blocks = storage.get_blocks_for_hash(hash_value)
|
|
63
|
+
dedup_blocks = self._deduplicator.deduplicate_blocks(blocks)
|
|
64
|
+
|
|
65
|
+
# Check min_occurrences threshold (language-aware)
|
|
66
|
+
if not self._meets_min_occurrences(dedup_blocks, config):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
for block in dedup_blocks:
|
|
70
|
+
violation = self._violation_builder.build_violation(block, dedup_blocks, rule_id)
|
|
71
|
+
violations.append(violation)
|
|
72
|
+
|
|
73
|
+
deduplicated = self._deduplicator.deduplicate_violations(violations)
|
|
74
|
+
pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
|
|
75
|
+
return self._filter_inline_ignored(pattern_filtered, inline_ignore)
|
|
76
|
+
|
|
77
|
+
def _meets_min_occurrences(self, blocks: list, config: DRYConfig) -> bool:
|
|
78
|
+
"""Check if blocks meet minimum occurrence threshold for the language.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
blocks: List of duplicate code blocks
|
|
82
|
+
config: DRY configuration with min_occurrences settings
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if blocks meet or exceed minimum occurrence threshold
|
|
86
|
+
"""
|
|
87
|
+
if len(blocks) == 0:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Get language from first block's file extension
|
|
91
|
+
first_block = blocks[0]
|
|
92
|
+
language = detect_language(first_block.file_path)
|
|
93
|
+
|
|
94
|
+
# Get language-specific threshold
|
|
95
|
+
min_occurrences = config.get_min_occurrences_for_language(language)
|
|
96
|
+
|
|
97
|
+
return len(blocks) >= min_occurrences
|
|
98
|
+
|
|
99
|
+
def _filter_ignored(
|
|
100
|
+
self, violations: list[Violation], ignore_patterns: list[str]
|
|
101
|
+
) -> list[Violation]:
|
|
102
|
+
"""Filter violations based on ignore patterns.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
violations: List of violations to filter
|
|
106
|
+
ignore_patterns: List of path patterns to ignore
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Filtered list of violations
|
|
110
|
+
"""
|
|
111
|
+
if not ignore_patterns:
|
|
112
|
+
return violations
|
|
113
|
+
|
|
114
|
+
filtered = []
|
|
115
|
+
for violation in violations:
|
|
116
|
+
if not self._is_ignored(violation.file_path, ignore_patterns):
|
|
117
|
+
filtered.append(violation)
|
|
118
|
+
return filtered
|
|
119
|
+
|
|
120
|
+
def _is_ignored(self, file_path: str, ignore_patterns: list[str]) -> bool:
|
|
121
|
+
"""Check if file path matches any ignore pattern.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
file_path: Path to check
|
|
125
|
+
ignore_patterns: List of patterns to match against
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
True if file should be ignored
|
|
129
|
+
"""
|
|
130
|
+
path_str = str(Path(file_path))
|
|
131
|
+
for pattern in ignore_patterns:
|
|
132
|
+
if pattern in path_str:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _filter_inline_ignored(
|
|
137
|
+
self, violations: list[Violation], inline_ignore: InlineIgnoreParser
|
|
138
|
+
) -> list[Violation]:
|
|
139
|
+
"""Filter violations based on inline ignore directives.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
violations: List of violations to filter
|
|
143
|
+
inline_ignore: Parser with inline ignore directives
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Filtered list of violations
|
|
147
|
+
"""
|
|
148
|
+
filtered = []
|
|
149
|
+
for violation in violations:
|
|
150
|
+
start_line = violation.line or 0
|
|
151
|
+
# Extract line count from message to calculate end_line
|
|
152
|
+
line_count = self._extract_line_count(violation.message)
|
|
153
|
+
end_line = start_line + line_count - 1
|
|
154
|
+
|
|
155
|
+
if not inline_ignore.should_ignore(violation.file_path, start_line, end_line):
|
|
156
|
+
filtered.append(violation)
|
|
157
|
+
return filtered
|
|
158
|
+
|
|
159
|
+
def _extract_line_count(self, message: str) -> int:
|
|
160
|
+
"""Extract line count from violation message.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
message: Violation message
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Number of lines (default 1)
|
|
167
|
+
"""
|
|
168
|
+
# Message format: "Duplicate code (N lines, ...)"
|
|
169
|
+
try:
|
|
170
|
+
start = message.index("(") + 1
|
|
171
|
+
end = message.index(" lines")
|
|
172
|
+
return int(message[start:end])
|
|
173
|
+
except (ValueError, IndexError):
|
|
174
|
+
return 1
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration file loading for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Handles loading and parsing of JSON/YAML configuration files
|
|
5
|
+
|
|
6
|
+
Overview: Provides configuration file loading functionality for the file placement linter.
|
|
7
|
+
Supports both JSON and YAML config formats, handles path resolution relative to project
|
|
8
|
+
root, and provides safe defaults when config files are missing or invalid. Isolates
|
|
9
|
+
file I/O concerns from business logic to maintain single responsibility.
|
|
10
|
+
|
|
11
|
+
Dependencies: pathlib, json, yaml
|
|
12
|
+
|
|
13
|
+
Exports: ConfigLoader
|
|
14
|
+
|
|
15
|
+
Interfaces: load_config_file(config_file, project_root) -> dict
|
|
16
|
+
|
|
17
|
+
Implementation: Uses standard library JSON and PyYAML for parsing, returns empty dict on errors
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigLoader:
|
|
28
|
+
"""Loads configuration files for file placement linter."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, project_root: Path):
|
|
31
|
+
"""Initialize config loader.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project_root: Project root directory
|
|
35
|
+
"""
|
|
36
|
+
self.project_root = project_root
|
|
37
|
+
|
|
38
|
+
def load_config_file(self, config_file: str) -> dict[str, Any]:
|
|
39
|
+
"""Load configuration from file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config_file: Path to config file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Loaded configuration dict, or empty dict if file doesn't exist
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If config file format is unsupported
|
|
49
|
+
"""
|
|
50
|
+
config_path = self._resolve_path(config_file)
|
|
51
|
+
if not config_path.exists():
|
|
52
|
+
return {}
|
|
53
|
+
return self._parse_file(config_path)
|
|
54
|
+
|
|
55
|
+
def _resolve_path(self, config_file: str) -> Path:
|
|
56
|
+
"""Resolve config file path relative to project root.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config_file: Config file path (relative or absolute)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Resolved absolute path
|
|
63
|
+
"""
|
|
64
|
+
config_path = Path(config_file)
|
|
65
|
+
if not config_path.is_absolute():
|
|
66
|
+
config_path = self.project_root / config_path
|
|
67
|
+
return config_path
|
|
68
|
+
|
|
69
|
+
def _parse_file(self, config_path: Path) -> dict[str, Any]:
|
|
70
|
+
"""Parse config file based on extension.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config_path: Path to config file
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Parsed configuration dict
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If file format is unsupported
|
|
80
|
+
"""
|
|
81
|
+
with config_path.open(encoding="utf-8") as f:
|
|
82
|
+
if config_path.suffix in [".yaml", ".yml"]:
|
|
83
|
+
return yaml.safe_load(f) or {}
|
|
84
|
+
if config_path.suffix == ".json":
|
|
85
|
+
return json.load(f)
|
|
86
|
+
raise ValueError(f"Unsupported config format: {config_path.suffix}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Directory rule matching for file placement linter
|
|
3
|
+
|
|
4
|
+
Scope: Finds most specific directory rule matching a file path
|
|
5
|
+
|
|
6
|
+
Overview: Provides directory matching functionality for the file placement linter. Implements
|
|
7
|
+
most-specific-directory matching logic by comparing path prefixes and calculating directory
|
|
8
|
+
depth. Handles special case of root directory matching. Returns matched rule and path for
|
|
9
|
+
further processing. Isolates directory matching logic from rule checking and pattern matching.
|
|
10
|
+
|
|
11
|
+
Dependencies: typing
|
|
12
|
+
|
|
13
|
+
Exports: DirectoryMatcher
|
|
14
|
+
|
|
15
|
+
Interfaces: find_matching_rule(path_str, directories) -> (rule_dict, matched_path)
|
|
16
|
+
|
|
17
|
+
Implementation: Prefix matching with depth-based precedence, root directory special case
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DirectoryMatcher:
|
|
24
|
+
"""Finds matching directory rules based on path prefixes."""
|
|
25
|
+
|
|
26
|
+
def find_matching_rule(
|
|
27
|
+
self, path_str: str, directories: dict[str, Any]
|
|
28
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
29
|
+
"""Find most specific directory rule matching the path.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
path_str: File path string
|
|
33
|
+
directories: Directory rules
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (rule_dict, matched_path)
|
|
37
|
+
"""
|
|
38
|
+
best_match = None
|
|
39
|
+
best_path = None
|
|
40
|
+
best_depth = -1
|
|
41
|
+
|
|
42
|
+
for dir_path, rules in directories.items():
|
|
43
|
+
matches, depth = self._check_path_match(dir_path, path_str)
|
|
44
|
+
if matches and depth > best_depth:
|
|
45
|
+
best_match = rules
|
|
46
|
+
best_path = dir_path
|
|
47
|
+
best_depth = depth
|
|
48
|
+
|
|
49
|
+
return best_match, best_path
|
|
50
|
+
|
|
51
|
+
def _check_path_match(self, dir_path: str, path_str: str) -> tuple[bool, int]:
|
|
52
|
+
"""Check if path matches directory rule.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
dir_path: Directory path pattern
|
|
56
|
+
path_str: File path string
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple of (matches, depth) where depth is directory nesting level
|
|
60
|
+
"""
|
|
61
|
+
if dir_path == "/":
|
|
62
|
+
return self._check_root_match(dir_path, path_str)
|
|
63
|
+
if path_str.startswith(dir_path):
|
|
64
|
+
depth = len(dir_path.split("/"))
|
|
65
|
+
return True, depth
|
|
66
|
+
return False, -1
|
|
67
|
+
|
|
68
|
+
def _check_root_match(self, dir_path: str, path_str: str) -> tuple[bool, int]:
|
|
69
|
+
"""Check if path matches root directory rule.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
dir_path: Directory path (should be "/")
|
|
73
|
+
path_str: File path string
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (matches, depth)
|
|
77
|
+
"""
|
|
78
|
+
if dir_path == "/" and "/" not in path_str:
|
|
79
|
+
return True, 0
|
|
80
|
+
return False, -1
|