thailint 0.4.3__tar.gz → 0.4.4__tar.gz

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 (83) hide show
  1. {thailint-0.4.3 → thailint-0.4.4}/PKG-INFO +4 -2
  2. {thailint-0.4.3 → thailint-0.4.4}/pyproject.toml +1 -1
  3. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/python_analyzer.py +61 -30
  4. {thailint-0.4.3 → thailint-0.4.4}/src/orchestrator/core.py +12 -2
  5. {thailint-0.4.3 → thailint-0.4.4}/CHANGELOG.md +0 -0
  6. {thailint-0.4.3 → thailint-0.4.4}/LICENSE +0 -0
  7. {thailint-0.4.3 → thailint-0.4.4}/README.md +0 -0
  8. {thailint-0.4.3 → thailint-0.4.4}/src/__init__.py +0 -0
  9. {thailint-0.4.3 → thailint-0.4.4}/src/analyzers/__init__.py +0 -0
  10. {thailint-0.4.3 → thailint-0.4.4}/src/analyzers/typescript_base.py +0 -0
  11. {thailint-0.4.3 → thailint-0.4.4}/src/api.py +0 -0
  12. {thailint-0.4.3 → thailint-0.4.4}/src/cli.py +0 -0
  13. {thailint-0.4.3 → thailint-0.4.4}/src/config.py +0 -0
  14. {thailint-0.4.3 → thailint-0.4.4}/src/core/__init__.py +0 -0
  15. {thailint-0.4.3 → thailint-0.4.4}/src/core/base.py +0 -0
  16. {thailint-0.4.3 → thailint-0.4.4}/src/core/cli_utils.py +0 -0
  17. {thailint-0.4.3 → thailint-0.4.4}/src/core/config_parser.py +0 -0
  18. {thailint-0.4.3 → thailint-0.4.4}/src/core/linter_utils.py +0 -0
  19. {thailint-0.4.3 → thailint-0.4.4}/src/core/registry.py +0 -0
  20. {thailint-0.4.3 → thailint-0.4.4}/src/core/rule_discovery.py +0 -0
  21. {thailint-0.4.3 → thailint-0.4.4}/src/core/types.py +0 -0
  22. {thailint-0.4.3 → thailint-0.4.4}/src/core/violation_builder.py +0 -0
  23. {thailint-0.4.3 → thailint-0.4.4}/src/linter_config/__init__.py +0 -0
  24. {thailint-0.4.3 → thailint-0.4.4}/src/linter_config/ignore.py +0 -0
  25. {thailint-0.4.3 → thailint-0.4.4}/src/linter_config/loader.py +0 -0
  26. {thailint-0.4.3 → thailint-0.4.4}/src/linters/__init__.py +0 -0
  27. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/__init__.py +0 -0
  28. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/base_token_analyzer.py +0 -0
  29. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/block_filter.py +0 -0
  30. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/block_grouper.py +0 -0
  31. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/cache.py +0 -0
  32. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/cache_query.py +0 -0
  33. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/config.py +0 -0
  34. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/config_loader.py +0 -0
  35. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/deduplicator.py +0 -0
  36. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/duplicate_storage.py +0 -0
  37. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/file_analyzer.py +0 -0
  38. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/inline_ignore.py +0 -0
  39. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/linter.py +0 -0
  40. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/storage_initializer.py +0 -0
  41. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/token_hasher.py +0 -0
  42. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/typescript_analyzer.py +0 -0
  43. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/violation_builder.py +0 -0
  44. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/violation_filter.py +0 -0
  45. {thailint-0.4.3 → thailint-0.4.4}/src/linters/dry/violation_generator.py +0 -0
  46. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/__init__.py +0 -0
  47. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/config_loader.py +0 -0
  48. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/directory_matcher.py +0 -0
  49. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/linter.py +0 -0
  50. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/path_resolver.py +0 -0
  51. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/pattern_matcher.py +0 -0
  52. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/pattern_validator.py +0 -0
  53. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/rule_checker.py +0 -0
  54. {thailint-0.4.3 → thailint-0.4.4}/src/linters/file_placement/violation_factory.py +0 -0
  55. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/__init__.py +0 -0
  56. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/config.py +0 -0
  57. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/context_analyzer.py +0 -0
  58. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/linter.py +0 -0
  59. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/python_analyzer.py +0 -0
  60. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  61. {thailint-0.4.3 → thailint-0.4.4}/src/linters/magic_numbers/violation_builder.py +0 -0
  62. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/__init__.py +0 -0
  63. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/config.py +0 -0
  64. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/linter.py +0 -0
  65. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/python_analyzer.py +0 -0
  66. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/typescript_analyzer.py +0 -0
  67. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/typescript_function_extractor.py +0 -0
  68. {thailint-0.4.3 → thailint-0.4.4}/src/linters/nesting/violation_builder.py +0 -0
  69. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/__init__.py +0 -0
  70. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/class_analyzer.py +0 -0
  71. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/config.py +0 -0
  72. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/heuristics.py +0 -0
  73. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/linter.py +0 -0
  74. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/metrics_evaluator.py +0 -0
  75. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/python_analyzer.py +0 -0
  76. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/typescript_analyzer.py +0 -0
  77. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  78. {thailint-0.4.3 → thailint-0.4.4}/src/linters/srp/violation_builder.py +0 -0
  79. {thailint-0.4.3 → thailint-0.4.4}/src/orchestrator/__init__.py +0 -0
  80. {thailint-0.4.3 → thailint-0.4.4}/src/orchestrator/language_detector.py +0 -0
  81. {thailint-0.4.3 → thailint-0.4.4}/src/templates/thailint_config_template.yaml +0 -0
  82. {thailint-0.4.3 → thailint-0.4.4}/src/utils/__init__.py +0 -0
  83. {thailint-0.4.3 → thailint-0.4.4}/src/utils/project_root.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Keywords: linter,ai,code-quality,static-analysis,file-placement,governance,multi-language,cli,docker,python
7
8
  Author: Steve Jackson
8
9
  Requires-Python: >=3.11,<4.0
@@ -15,6 +16,7 @@ Classifier: Programming Language :: Python :: 3
15
16
  Classifier: Programming Language :: Python :: 3.11
16
17
  Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Programming Language :: Python :: 3 :: Only
19
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
22
  Classifier: Topic :: Software Development :: Quality Assurance
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.4.3"
20
+ version = "0.4.4"
21
21
  description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
22
22
  authors = ["Steve Jackson"]
23
23
  license = "MIT"
@@ -62,6 +62,9 @@ class PythonDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.violat
62
62
  """
63
63
  super().__init__()
64
64
  self._filter_registry = filter_registry or create_default_registry()
65
+ # Performance optimization: Cache parsed AST to avoid re-parsing for each hash window
66
+ self._cached_ast: ast.Module | None = None
67
+ self._cached_content: str | None = None
65
68
 
66
69
  def analyze(self, file_path: Path, content: str, config: DRYConfig) -> list[CodeBlock]:
67
70
  """Analyze Python file for duplicate code blocks, excluding docstrings.
@@ -74,37 +77,46 @@ class PythonDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.violat
74
77
  Returns:
75
78
  List of CodeBlock instances with hash values
76
79
  """
77
- # Get docstring line ranges
78
- docstring_ranges = self._get_docstring_ranges_from_content(content)
80
+ # Performance optimization: Parse AST once and cache for _is_single_statement_in_source() calls
81
+ self._cached_ast = self._parse_content_safe(content)
82
+ self._cached_content = content
79
83
 
80
- # Tokenize with line number tracking
81
- lines_with_numbers = self._tokenize_with_line_numbers(content, docstring_ranges)
84
+ try:
85
+ # Get docstring line ranges
86
+ docstring_ranges = self._get_docstring_ranges_from_content(content)
82
87
 
83
- # Generate rolling hash windows
84
- windows = self._rolling_hash_with_tracking(lines_with_numbers, config.min_duplicate_lines)
88
+ # Tokenize with line number tracking
89
+ lines_with_numbers = self._tokenize_with_line_numbers(content, docstring_ranges)
85
90
 
86
- blocks = []
87
- for hash_val, start_line, end_line, snippet in windows:
88
- # Skip blocks that are single logical statements
89
- # Check the original source code, not the normalized snippet
90
- if self._is_single_statement_in_source(content, start_line, end_line):
91
- continue
91
+ # Generate rolling hash windows
92
+ windows = self._rolling_hash_with_tracking(lines_with_numbers, config.min_duplicate_lines)
92
93
 
93
- block = CodeBlock(
94
- file_path=file_path,
95
- start_line=start_line,
96
- end_line=end_line,
97
- snippet=snippet,
98
- hash_value=hash_val,
99
- )
94
+ blocks = []
95
+ for hash_val, start_line, end_line, snippet in windows:
96
+ # Skip blocks that are single logical statements
97
+ # Check the original source code, not the normalized snippet
98
+ if self._is_single_statement_in_source(content, start_line, end_line):
99
+ continue
100
100
 
101
- # Apply extensible filters (keyword arguments, imports, etc.)
102
- if self._filter_registry.should_filter_block(block, content):
103
- continue
101
+ block = CodeBlock(
102
+ file_path=file_path,
103
+ start_line=start_line,
104
+ end_line=end_line,
105
+ snippet=snippet,
106
+ hash_value=hash_val,
107
+ )
108
+
109
+ # Apply extensible filters (keyword arguments, imports, etc.)
110
+ if self._filter_registry.should_filter_block(block, content):
111
+ continue
104
112
 
105
- blocks.append(block)
113
+ blocks.append(block)
106
114
 
107
- return blocks
115
+ return blocks
116
+ finally:
117
+ # Clear cache after analysis to avoid memory leaks
118
+ self._cached_ast = None
119
+ self._cached_content = None
108
120
 
109
121
  def _get_docstring_ranges_from_content(self, content: str) -> set[int]:
110
122
  """Extract line numbers that are part of docstrings.
@@ -225,10 +237,19 @@ class PythonDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.violat
225
237
  return hashes
226
238
 
227
239
  def _is_single_statement_in_source(self, content: str, start_line: int, end_line: int) -> bool:
228
- """Check if a line range in the original source is a single logical statement."""
229
- tree = self._parse_content_safe(content)
230
- if tree is None:
231
- return False
240
+ """Check if a line range in the original source is a single logical statement.
241
+
242
+ Performance optimization: Uses cached AST if available (set by analyze() method)
243
+ to avoid re-parsing the entire file for each hash window check.
244
+ """
245
+ # Use cached AST if available and content matches
246
+ if self._cached_ast is not None and content == self._cached_content:
247
+ tree = self._cached_ast
248
+ else:
249
+ # Fallback: parse content (used by tests or standalone calls)
250
+ tree = self._parse_content_safe(content)
251
+ if tree is None:
252
+ return False
232
253
 
233
254
  return self._check_overlapping_nodes(tree, start_line, end_line)
234
255
 
@@ -241,9 +262,19 @@ class PythonDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.violat
241
262
  return None
242
263
 
243
264
  def _check_overlapping_nodes(self, tree: ast.Module, start_line: int, end_line: int) -> bool:
244
- """Check if any AST node overlaps and matches single-statement pattern."""
265
+ """Check if any AST node overlaps and matches single-statement pattern.
266
+
267
+ Performance optimization: Pre-filter nodes by line range before expensive pattern checks.
268
+ """
245
269
  for node in ast.walk(tree):
246
- if self._node_overlaps_and_matches(node, start_line, end_line):
270
+ # Quick line range check to skip nodes that don't overlap
271
+ if not hasattr(node, "lineno") or not hasattr(node, "end_lineno"):
272
+ continue
273
+ if node.end_lineno < start_line or node.lineno > end_line:
274
+ continue # No overlap, skip expensive pattern matching
275
+
276
+ # Node overlaps - check if it matches single-statement pattern
277
+ if self._is_single_statement_pattern(node, start_line, end_line):
247
278
  return True
248
279
  return False
249
280
 
@@ -101,8 +101,9 @@ class Orchestrator:
101
101
  self.config_loader = LinterConfigLoader()
102
102
  self.ignore_parser = IgnoreDirectiveParser(self.project_root)
103
103
 
104
- # Auto-discover and register all linting rules from src.linters
105
- self.registry.discover_rules("src.linters")
104
+ # Performance optimization: Defer rule discovery until first file is linted
105
+ # This eliminates ~0.077s overhead for commands that don't need rules (--help, config, etc.)
106
+ self._rules_discovered = False
106
107
 
107
108
  # Use provided config or load from project root
108
109
  if config is not None:
@@ -208,6 +209,12 @@ class Orchestrator:
208
209
 
209
210
  return violations
210
211
 
212
+ def _ensure_rules_discovered(self) -> None:
213
+ """Ensure rules have been discovered and registered (lazy initialization)."""
214
+ if not self._rules_discovered:
215
+ self.registry.discover_rules("src.linters")
216
+ self._rules_discovered = True
217
+
211
218
  def _get_rules_for_file(self, file_path: Path, language: str) -> list[BaseLintRule]:
212
219
  """Get rules applicable to this file.
213
220
 
@@ -218,6 +225,9 @@ class Orchestrator:
218
225
  Returns:
219
226
  List of rules to execute against this file.
220
227
  """
228
+ # Lazy initialization: discover rules on first lint operation
229
+ self._ensure_rules_discovered()
230
+
221
231
  # For now, return all registered rules
222
232
  # Future: filter by language, configuration, etc.
223
233
  return self.registry.list_all()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes