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.
Files changed (68) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +498 -141
  6. src/config.py +6 -31
  7. src/core/base.py +12 -0
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +99 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +262 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +218 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +130 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +126 -0
  26. src/linters/dry/file_analyzer.py +127 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +170 -0
  29. src/linters/dry/python_analyzer.py +517 -0
  30. src/linters/dry/storage_initializer.py +51 -0
  31. src/linters/dry/token_hasher.py +115 -0
  32. src/linters/dry/typescript_analyzer.py +590 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +91 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_placement/config_loader.py +86 -0
  37. src/linters/file_placement/directory_matcher.py +80 -0
  38. src/linters/file_placement/linter.py +252 -472
  39. src/linters/file_placement/path_resolver.py +61 -0
  40. src/linters/file_placement/pattern_matcher.py +55 -0
  41. src/linters/file_placement/pattern_validator.py +106 -0
  42. src/linters/file_placement/rule_checker.py +229 -0
  43. src/linters/file_placement/violation_factory.py +177 -0
  44. src/linters/nesting/config.py +13 -3
  45. src/linters/nesting/linter.py +76 -152
  46. src/linters/nesting/typescript_analyzer.py +38 -102
  47. src/linters/nesting/typescript_function_extractor.py +130 -0
  48. src/linters/nesting/violation_builder.py +139 -0
  49. src/linters/srp/__init__.py +99 -0
  50. src/linters/srp/class_analyzer.py +113 -0
  51. src/linters/srp/config.py +76 -0
  52. src/linters/srp/heuristics.py +89 -0
  53. src/linters/srp/linter.py +225 -0
  54. src/linters/srp/metrics_evaluator.py +47 -0
  55. src/linters/srp/python_analyzer.py +72 -0
  56. src/linters/srp/typescript_analyzer.py +75 -0
  57. src/linters/srp/typescript_metrics_calculator.py +90 -0
  58. src/linters/srp/violation_builder.py +117 -0
  59. src/orchestrator/core.py +42 -7
  60. src/utils/__init__.py +4 -0
  61. src/utils/project_root.py +84 -0
  62. {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/METADATA +414 -63
  63. thailint-0.2.0.dist-info/RECORD +75 -0
  64. src/.ai/layout.yaml +0 -48
  65. thailint-0.1.5.dist-info/RECORD +0 -28
  66. {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/LICENSE +0 -0
  67. {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/WHEEL +0 -0
  68. {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
- # 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"
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
- self.config = self.config_loader.load(config_path)
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
- context = FileLintContext(file_path, language, metadata=self.config)
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,4 @@
1
+ """Utils package for shared utilities.
2
+
3
+ This package provides common utility functions used across the linter framework.
4
+ """
@@ -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