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.
@@ -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.