thailint 0.1.5__py3-none-any.whl → 0.2.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 +498 -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.5.dist-info → thailint-0.2.0.dist-info}/METADATA +414 -63
- thailint-0.2.0.dist-info/RECORD +75 -0
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/LICENSE +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/WHEEL +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/entry_points.txt +0 -0
src/orchestrator/core.py
CHANGED
|
@@ -89,11 +89,12 @@ class Orchestrator:
|
|
|
89
89
|
detection to provide comprehensive linting of files and directories.
|
|
90
90
|
"""
|
|
91
91
|
|
|
92
|
-
def __init__(self, project_root: Path | None = None):
|
|
92
|
+
def __init__(self, project_root: Path | None = None, config: dict | None = None):
|
|
93
93
|
"""Initialize orchestrator.
|
|
94
94
|
|
|
95
95
|
Args:
|
|
96
96
|
project_root: Root directory of project. Defaults to current directory.
|
|
97
|
+
config: Optional pre-loaded configuration dict. If provided, skips config file loading.
|
|
97
98
|
"""
|
|
98
99
|
self.project_root = project_root or Path.cwd()
|
|
99
100
|
self.registry = RuleRegistry()
|
|
@@ -103,12 +104,16 @@ class Orchestrator:
|
|
|
103
104
|
# Auto-discover and register all linting rules from src.linters
|
|
104
105
|
self.registry.discover_rules("src.linters")
|
|
105
106
|
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
# Use provided config or load from project root
|
|
108
|
+
if config is not None:
|
|
109
|
+
self.config = config
|
|
110
|
+
else:
|
|
111
|
+
# Load configuration from project root
|
|
112
|
+
config_path = self.project_root / ".thailint.yaml"
|
|
113
|
+
if not config_path.exists():
|
|
114
|
+
config_path = self.project_root / ".thailint.json"
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
self.config = self.config_loader.load(config_path)
|
|
112
117
|
|
|
113
118
|
def lint_file(self, file_path: Path) -> list[Violation]:
|
|
114
119
|
"""Lint a single file.
|
|
@@ -124,10 +129,33 @@ class Orchestrator:
|
|
|
124
129
|
|
|
125
130
|
language = detect_language(file_path)
|
|
126
131
|
rules = self._get_rules_for_file(file_path, language)
|
|
127
|
-
|
|
132
|
+
|
|
133
|
+
# Add project_root to metadata for rules that need it (e.g., DRY linter cache)
|
|
134
|
+
metadata = {**self.config, "_project_root": self.project_root}
|
|
135
|
+
context = FileLintContext(file_path, language, metadata=metadata)
|
|
128
136
|
|
|
129
137
|
return self._execute_rules(rules, context)
|
|
130
138
|
|
|
139
|
+
def lint_files(self, file_paths: list[Path]) -> list[Violation]:
|
|
140
|
+
"""Lint multiple files.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
file_paths: List of file paths to lint.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of violations found across all files.
|
|
147
|
+
"""
|
|
148
|
+
violations = []
|
|
149
|
+
|
|
150
|
+
for file_path in file_paths:
|
|
151
|
+
violations.extend(self.lint_file(file_path))
|
|
152
|
+
|
|
153
|
+
# Call finalize() on all rules after processing all files
|
|
154
|
+
for rule in self.registry.list_all():
|
|
155
|
+
violations.extend(rule.finalize())
|
|
156
|
+
|
|
157
|
+
return violations
|
|
158
|
+
|
|
131
159
|
def _execute_rules(
|
|
132
160
|
self, rules: list[BaseLintRule], context: BaseLintContext
|
|
133
161
|
) -> list[Violation]:
|
|
@@ -150,6 +178,9 @@ class Orchestrator:
|
|
|
150
178
|
"""Safely check a rule, returning empty list on error."""
|
|
151
179
|
try:
|
|
152
180
|
return rule.check(context)
|
|
181
|
+
except ValueError:
|
|
182
|
+
# Re-raise configuration validation errors (these are user-facing)
|
|
183
|
+
raise
|
|
153
184
|
except Exception: # nosec B112
|
|
154
185
|
# Skip rules that fail (defensive programming)
|
|
155
186
|
return []
|
|
@@ -171,6 +202,10 @@ class Orchestrator:
|
|
|
171
202
|
if file_path.is_file():
|
|
172
203
|
violations.extend(self.lint_file(file_path))
|
|
173
204
|
|
|
205
|
+
# Call finalize() on all rules after processing all files
|
|
206
|
+
for rule in self.registry.list_all():
|
|
207
|
+
violations.extend(rule.finalize())
|
|
208
|
+
|
|
174
209
|
return violations
|
|
175
210
|
|
|
176
211
|
def _get_rules_for_file(self, file_path: Path, language: str) -> list[BaseLintRule]:
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Project root detection utility.
|
|
2
|
+
|
|
3
|
+
Purpose: Centralized project root detection for consistent file placement
|
|
4
|
+
Scope: Single source of truth for finding project root directory
|
|
5
|
+
|
|
6
|
+
Overview: Uses pyprojroot package to provide reliable project root detection across
|
|
7
|
+
different environments (development, CI/CD, user installations). Delegates all
|
|
8
|
+
project root detection logic to the industry-standard pyprojroot library which
|
|
9
|
+
handles various project markers and edge cases that we cannot anticipate.
|
|
10
|
+
|
|
11
|
+
Dependencies: pyprojroot for robust project root detection
|
|
12
|
+
|
|
13
|
+
Exports: is_project_root(), get_project_root()
|
|
14
|
+
|
|
15
|
+
Interfaces: Path-based functions for checking and finding project roots
|
|
16
|
+
|
|
17
|
+
Implementation: Pure delegation to pyprojroot with fallback to start_path when no root found
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from pyprojroot import find_root
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_project_root(path: Path) -> bool:
|
|
26
|
+
"""Check if a directory is a project root.
|
|
27
|
+
|
|
28
|
+
Uses pyprojroot to detect if the given path is a project root by checking
|
|
29
|
+
if finding the root from this path returns the same path.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
path: Directory path to check
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if the directory is a project root, False otherwise
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
>>> is_project_root(Path("/home/user/myproject"))
|
|
39
|
+
True
|
|
40
|
+
>>> is_project_root(Path("/home/user/myproject/src"))
|
|
41
|
+
False
|
|
42
|
+
"""
|
|
43
|
+
if not path.exists() or not path.is_dir():
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Find root from this path - if it equals this path, it's a root
|
|
48
|
+
found_root = find_root(path)
|
|
49
|
+
return found_root == path.resolve()
|
|
50
|
+
except (OSError, RuntimeError):
|
|
51
|
+
# pyprojroot couldn't find a root
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_project_root(start_path: Path | None = None) -> Path:
|
|
56
|
+
"""Find project root by walking up the directory tree.
|
|
57
|
+
|
|
58
|
+
This is the single source of truth for project root detection.
|
|
59
|
+
All code that needs to find the project root should use this function.
|
|
60
|
+
|
|
61
|
+
Uses pyprojroot which searches for standard project markers defined by the
|
|
62
|
+
pyprojroot library (git repos, Python projects, etc).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
start_path: Directory to start searching from. If None, uses current working directory.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Path to project root directory. If no root markers found, returns the start_path.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
>>> root = get_project_root()
|
|
72
|
+
>>> config_file = root / ".thailint.yaml"
|
|
73
|
+
"""
|
|
74
|
+
if start_path is None:
|
|
75
|
+
start_path = Path.cwd()
|
|
76
|
+
|
|
77
|
+
current = start_path.resolve()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Use pyprojroot to find the project root
|
|
81
|
+
return find_root(current)
|
|
82
|
+
except (OSError, RuntimeError):
|
|
83
|
+
# No project markers found, return the start path
|
|
84
|
+
return current
|