thailint 0.1.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/.ai/layout.yaml +48 -0
- src/__init__.py +49 -0
- src/api.py +118 -0
- src/cli.py +698 -0
- src/config.py +386 -0
- src/core/__init__.py +17 -0
- src/core/base.py +122 -0
- src/core/registry.py +170 -0
- src/core/types.py +83 -0
- src/linter_config/__init__.py +13 -0
- src/linter_config/ignore.py +403 -0
- src/linter_config/loader.py +77 -0
- src/linters/__init__.py +4 -0
- src/linters/file_placement/__init__.py +31 -0
- src/linters/file_placement/linter.py +621 -0
- src/linters/nesting/__init__.py +87 -0
- src/linters/nesting/config.py +50 -0
- src/linters/nesting/linter.py +257 -0
- src/linters/nesting/python_analyzer.py +89 -0
- src/linters/nesting/typescript_analyzer.py +180 -0
- src/orchestrator/__init__.py +9 -0
- src/orchestrator/core.py +188 -0
- src/orchestrator/language_detector.py +81 -0
- thailint-0.1.0.dist-info/LICENSE +21 -0
- thailint-0.1.0.dist-info/METADATA +601 -0
- thailint-0.1.0.dist-info/RECORD +28 -0
- thailint-0.1.0.dist-info/WHEEL +4 -0
- thailint-0.1.0.dist-info/entry_points.txt +4 -0
src/orchestrator/core.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main orchestration engine coordinating rule execution across files and directories
|
|
3
|
+
|
|
4
|
+
Scope: Central coordination of linting operations integrating registry, config, and ignore systems
|
|
5
|
+
|
|
6
|
+
Overview: Provides the main entry point for linting operations by coordinating execution of rules
|
|
7
|
+
across single files and entire directory trees. Integrates with the rule registry for dynamic
|
|
8
|
+
rule discovery, configuration loader for user settings, ignore directive parser for suppression
|
|
9
|
+
patterns, and language detector for file routing. Creates lint contexts for each file with
|
|
10
|
+
appropriate file information and language metadata, executes applicable rules against contexts,
|
|
11
|
+
and collects violations across all processed files. Supports recursive and non-recursive
|
|
12
|
+
directory traversal, respects .thailintignore patterns at repository level, and provides
|
|
13
|
+
configurable linting through .thailint.yaml configuration files. Serves as the primary
|
|
14
|
+
interface between the linter framework and user-facing CLI/library APIs.
|
|
15
|
+
|
|
16
|
+
Dependencies: pathlib for file operations, BaseLintRule and BaseLintContext from core.base,
|
|
17
|
+
Violation from core.types, RuleRegistry from core.registry, LinterConfigLoader from
|
|
18
|
+
linter_config.loader, IgnoreDirectiveParser from linter_config.ignore, detect_language
|
|
19
|
+
from language_detector
|
|
20
|
+
|
|
21
|
+
Exports: Orchestrator class, FileLintContext implementation class
|
|
22
|
+
|
|
23
|
+
Interfaces: Orchestrator(project_root: Path | None), lint_file(file_path: Path) -> list[Violation],
|
|
24
|
+
lint_directory(dir_path: Path, recursive: bool) -> list[Violation]
|
|
25
|
+
|
|
26
|
+
Implementation: Directory glob pattern matching for traversal (** for recursive, * for shallow),
|
|
27
|
+
ignore pattern checking before file processing, dynamic context creation per file,
|
|
28
|
+
rule filtering by applicability, violation collection and aggregation across files
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
34
|
+
from src.core.registry import RuleRegistry
|
|
35
|
+
from src.core.types import Violation
|
|
36
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
37
|
+
from src.linter_config.loader import LinterConfigLoader
|
|
38
|
+
|
|
39
|
+
from .language_detector import detect_language
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FileLintContext(BaseLintContext):
|
|
43
|
+
"""Concrete implementation of lint context for file analysis."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self, path: Path, lang: str, content: str | None = None, metadata: dict | None = None
|
|
47
|
+
):
|
|
48
|
+
"""Initialize file lint context.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: Path to the file being analyzed.
|
|
52
|
+
lang: Programming language identifier.
|
|
53
|
+
content: Optional pre-loaded file content.
|
|
54
|
+
metadata: Optional metadata dict containing configuration.
|
|
55
|
+
"""
|
|
56
|
+
self._path = path
|
|
57
|
+
self._language = lang
|
|
58
|
+
self._content = content
|
|
59
|
+
self.metadata = metadata or {}
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def file_path(self) -> Path | None:
|
|
63
|
+
"""Get file path being analyzed."""
|
|
64
|
+
return self._path
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def file_content(self) -> str | None:
|
|
68
|
+
"""Get file content being analyzed."""
|
|
69
|
+
if self._content is not None:
|
|
70
|
+
return self._content
|
|
71
|
+
if not self._path or not self._path.exists():
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
self._content = self._path.read_text(encoding="utf-8")
|
|
75
|
+
except (UnicodeDecodeError, OSError):
|
|
76
|
+
self._content = None
|
|
77
|
+
return self._content
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def language(self) -> str:
|
|
81
|
+
"""Get programming language of file."""
|
|
82
|
+
return self._language
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Orchestrator:
|
|
86
|
+
"""Main linter orchestrator coordinating rule execution.
|
|
87
|
+
|
|
88
|
+
Integrates rule registry, configuration loading, ignore patterns, and language
|
|
89
|
+
detection to provide comprehensive linting of files and directories.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, project_root: Path | None = None):
|
|
93
|
+
"""Initialize orchestrator.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
project_root: Root directory of project. Defaults to current directory.
|
|
97
|
+
"""
|
|
98
|
+
self.project_root = project_root or Path.cwd()
|
|
99
|
+
self.registry = RuleRegistry()
|
|
100
|
+
self.config_loader = LinterConfigLoader()
|
|
101
|
+
self.ignore_parser = IgnoreDirectiveParser(self.project_root)
|
|
102
|
+
|
|
103
|
+
# Auto-discover and register all linting rules from src.linters
|
|
104
|
+
self.registry.discover_rules("src.linters")
|
|
105
|
+
|
|
106
|
+
# Load configuration from project root
|
|
107
|
+
config_path = self.project_root / ".thailint.yaml"
|
|
108
|
+
if not config_path.exists():
|
|
109
|
+
config_path = self.project_root / ".thailint.json"
|
|
110
|
+
|
|
111
|
+
self.config = self.config_loader.load(config_path)
|
|
112
|
+
|
|
113
|
+
def lint_file(self, file_path: Path) -> list[Violation]:
|
|
114
|
+
"""Lint a single file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file_path: Path to file to lint.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of violations found in the file.
|
|
121
|
+
"""
|
|
122
|
+
if self.ignore_parser.is_ignored(file_path):
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
language = detect_language(file_path)
|
|
126
|
+
rules = self._get_rules_for_file(file_path, language)
|
|
127
|
+
context = FileLintContext(file_path, language, metadata=self.config)
|
|
128
|
+
|
|
129
|
+
return self._execute_rules(rules, context)
|
|
130
|
+
|
|
131
|
+
def _execute_rules(
|
|
132
|
+
self, rules: list[BaseLintRule], context: BaseLintContext
|
|
133
|
+
) -> list[Violation]:
|
|
134
|
+
"""Execute rules and collect violations.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
rules: List of rules to execute.
|
|
138
|
+
context: Lint context to pass to rules.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of violations found.
|
|
142
|
+
"""
|
|
143
|
+
violations = []
|
|
144
|
+
for rule in rules:
|
|
145
|
+
rule_violations = self._safe_check_rule(rule, context)
|
|
146
|
+
violations.extend(rule_violations)
|
|
147
|
+
return violations
|
|
148
|
+
|
|
149
|
+
def _safe_check_rule(self, rule: BaseLintRule, context: BaseLintContext) -> list[Violation]:
|
|
150
|
+
"""Safely check a rule, returning empty list on error."""
|
|
151
|
+
try:
|
|
152
|
+
return rule.check(context)
|
|
153
|
+
except Exception: # nosec B112
|
|
154
|
+
# Skip rules that fail (defensive programming)
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
def lint_directory(self, dir_path: Path, recursive: bool = True) -> list[Violation]:
|
|
158
|
+
"""Lint all files in a directory.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
dir_path: Path to directory to lint.
|
|
162
|
+
recursive: Whether to traverse subdirectories recursively.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of all violations found across all files.
|
|
166
|
+
"""
|
|
167
|
+
violations = []
|
|
168
|
+
pattern = "**/*" if recursive else "*"
|
|
169
|
+
|
|
170
|
+
for file_path in dir_path.glob(pattern):
|
|
171
|
+
if file_path.is_file():
|
|
172
|
+
violations.extend(self.lint_file(file_path))
|
|
173
|
+
|
|
174
|
+
return violations
|
|
175
|
+
|
|
176
|
+
def _get_rules_for_file(self, file_path: Path, language: str) -> list[BaseLintRule]:
|
|
177
|
+
"""Get rules applicable to this file.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
file_path: Path to file being linted.
|
|
181
|
+
language: Detected programming language.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of rules to execute against this file.
|
|
185
|
+
"""
|
|
186
|
+
# For now, return all registered rules
|
|
187
|
+
# Future: filter by language, configuration, etc.
|
|
188
|
+
return self.registry.list_all()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Programming language detection from file extensions and content
|
|
3
|
+
|
|
4
|
+
Scope: Language identification for routing files to appropriate analyzers and rules
|
|
5
|
+
|
|
6
|
+
Overview: Detects programming language from files using multiple strategies including file
|
|
7
|
+
extension mapping, shebang line parsing for scripts, and content analysis. Provides simple
|
|
8
|
+
extension-to-language mapping for common file types (.py -> python, .js -> javascript,
|
|
9
|
+
.ts -> typescript, .java -> java, .go -> go). Falls back to shebang parsing for extensionless
|
|
10
|
+
scripts by reading first line and checking for language indicators. Returns 'unknown' for
|
|
11
|
+
unrecognized files, allowing the orchestrator to skip or apply language-agnostic rules.
|
|
12
|
+
Enables the multi-language architecture by accurately identifying file types for proper
|
|
13
|
+
rule routing and analyzer selection.
|
|
14
|
+
|
|
15
|
+
Dependencies: pathlib for file path handling and content reading
|
|
16
|
+
|
|
17
|
+
Exports: detect_language(file_path: Path) -> str function, EXTENSION_MAP constant
|
|
18
|
+
|
|
19
|
+
Interfaces: detect_language(file_path: Path) -> str returns language identifier string
|
|
20
|
+
(python, javascript, typescript, java, go, unknown)
|
|
21
|
+
|
|
22
|
+
Implementation: Dictionary-based extension lookup for O(1) detection, first-line shebang
|
|
23
|
+
parsing with substring matching, lazy file reading only when extension unknown
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# Extension to language mapping
|
|
29
|
+
EXTENSION_MAP = {
|
|
30
|
+
".py": "python",
|
|
31
|
+
".js": "javascript",
|
|
32
|
+
".ts": "typescript",
|
|
33
|
+
".tsx": "typescript",
|
|
34
|
+
".jsx": "javascript",
|
|
35
|
+
".java": "java",
|
|
36
|
+
".go": "go",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _detect_from_shebang(file_path: Path) -> str | None:
|
|
41
|
+
"""Detect language from shebang line."""
|
|
42
|
+
try:
|
|
43
|
+
first_line = _read_first_line(file_path)
|
|
44
|
+
return _parse_shebang_language(first_line)
|
|
45
|
+
except (UnicodeDecodeError, OSError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_first_line(file_path: Path) -> str:
|
|
50
|
+
"""Read the first line from a file."""
|
|
51
|
+
return file_path.read_text(encoding="utf-8").split("\n")[0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_shebang_language(line: str) -> str | None:
|
|
55
|
+
"""Parse language from shebang line."""
|
|
56
|
+
if not line.startswith("#!"):
|
|
57
|
+
return None
|
|
58
|
+
if "python" in line:
|
|
59
|
+
return "python"
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def detect_language(file_path: Path) -> str:
|
|
64
|
+
"""Detect programming language from file.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
file_path: Path to file to analyze.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Language identifier (python, javascript, typescript, java, go, unknown).
|
|
71
|
+
"""
|
|
72
|
+
ext = file_path.suffix.lower()
|
|
73
|
+
if ext in EXTENSION_MAP:
|
|
74
|
+
return EXTENSION_MAP[ext]
|
|
75
|
+
|
|
76
|
+
if file_path.exists() and file_path.stat().st_size > 0:
|
|
77
|
+
lang = _detect_from_shebang(file_path)
|
|
78
|
+
if lang:
|
|
79
|
+
return lang
|
|
80
|
+
|
|
81
|
+
return "unknown"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Steve Jackson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|