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
@@ -0,0 +1,140 @@
1
+ """
2
+ Purpose: Inline ignore directive parsing for DRY linter
3
+
4
+ Scope: Parses and tracks inline ignore directives in source files
5
+
6
+ Overview: Parses source code for inline ignore directives (# dry: ignore-block, # dry: ignore-next).
7
+ Tracks line ranges that should be ignored based on directives. Used by violation_generator
8
+ to filter violations that fall within ignored ranges. Supports both block-level ignores
9
+ (entire block) and next-line ignores (next statement only).
10
+
11
+ Dependencies: None (standalone utility)
12
+
13
+ Exports: InlineIgnoreParser class
14
+
15
+ Interfaces: InlineIgnoreParser.parse(content) -> dict, should_ignore(file_path, line) -> bool
16
+
17
+ Implementation: Regex-based comment parsing, line range tracking
18
+ """
19
+
20
+ import re
21
+ from pathlib import Path
22
+
23
+
24
+ class InlineIgnoreParser:
25
+ """Parses inline ignore directives from source code."""
26
+
27
+ def __init__(self) -> None:
28
+ """Initialize parser with ignore ranges tracking."""
29
+ self._ignore_ranges: dict[str, list[tuple[int, int]]] = {}
30
+
31
+ def parse_file(self, file_path: Path, content: str) -> None:
32
+ """Parse file for ignore directives and store ranges.
33
+
34
+ Args:
35
+ file_path: Path to the file
36
+ content: File content to parse
37
+ """
38
+ lines = content.split("\n")
39
+ ranges = self._extract_ignore_ranges(lines)
40
+
41
+ if ranges:
42
+ self._ignore_ranges[str(file_path)] = ranges
43
+
44
+ def _extract_ignore_ranges(self, lines: list[str]) -> list[tuple[int, int]]:
45
+ """Extract ignore ranges from lines.
46
+
47
+ Args:
48
+ lines: List of lines to process
49
+
50
+ Returns:
51
+ List of (start, end) tuples for ignore ranges
52
+ """
53
+ ranges = []
54
+
55
+ for i, line in enumerate(lines, start=1):
56
+ ignore_range = self._parse_ignore_directive(line, i, len(lines))
57
+ if ignore_range:
58
+ ranges.append(ignore_range)
59
+
60
+ return ranges
61
+
62
+ def _parse_ignore_directive(
63
+ self, line: str, line_num: int, total_lines: int
64
+ ) -> tuple[int, int] | None:
65
+ """Parse a single line for ignore directives.
66
+
67
+ Args:
68
+ line: Line content
69
+ line_num: Current line number
70
+ total_lines: Total number of lines
71
+
72
+ Returns:
73
+ (start, end) tuple if directive found, None otherwise
74
+ """
75
+ # Check for ignore-block directive
76
+ if re.search(r"#\s*dry:\s*ignore-block", line):
77
+ start = line_num + 1
78
+ end = min(line_num + 10, total_lines)
79
+ return (start, end)
80
+
81
+ # Check for ignore-next directive
82
+ if re.search(r"#\s*dry:\s*ignore-next", line):
83
+ return (line_num + 1, line_num + 1)
84
+
85
+ return None
86
+
87
+ def should_ignore(self, file_path: str, line: int, end_line: int | None = None) -> bool:
88
+ """Check if a line or range should be ignored.
89
+
90
+ Args:
91
+ file_path: Path to the file
92
+ line: Starting line number to check
93
+ end_line: Optional ending line number (for range check)
94
+
95
+ Returns:
96
+ True if line/range should be ignored
97
+ """
98
+ ranges = self._ignore_ranges.get(str(Path(file_path)), [])
99
+ if not ranges:
100
+ return False
101
+
102
+ if end_line is not None:
103
+ return self._check_range_overlap(line, end_line, ranges)
104
+
105
+ return self._check_single_line(line, ranges)
106
+
107
+ def _check_range_overlap(self, line: int, end_line: int, ranges: list[tuple[int, int]]) -> bool:
108
+ """Check if range overlaps with any ignore range.
109
+
110
+ Args:
111
+ line: Starting line number
112
+ end_line: Ending line number
113
+ ranges: List of ignore ranges
114
+
115
+ Returns:
116
+ True if ranges overlap
117
+ """
118
+ for ign_start, ign_end in ranges:
119
+ if line <= ign_end and end_line >= ign_start:
120
+ return True
121
+ return False
122
+
123
+ def _check_single_line(self, line: int, ranges: list[tuple[int, int]]) -> bool:
124
+ """Check if single line is in any ignore range.
125
+
126
+ Args:
127
+ line: Line number to check
128
+ ranges: List of ignore ranges
129
+
130
+ Returns:
131
+ True if line is in any range
132
+ """
133
+ for start, end in ranges:
134
+ if start <= line <= end:
135
+ return True
136
+ return False
137
+
138
+ def clear(self) -> None:
139
+ """Clear all stored ignore ranges."""
140
+ self._ignore_ranges.clear()
@@ -0,0 +1,170 @@
1
+ """
2
+ Purpose: Main DRY linter rule implementation with stateful caching
3
+
4
+ Scope: DRYRule class implementing BaseLintRule interface for duplicate code detection
5
+
6
+ Overview: Implements DRY linter rule following BaseLintRule interface with stateful caching design.
7
+ Orchestrates duplicate detection by delegating to specialized classes: ConfigLoader for config,
8
+ StorageInitializer for storage setup, FileAnalyzer for file analysis, and ViolationGenerator
9
+ for violation creation. Maintains minimal orchestration logic to comply with SRP (8 methods total).
10
+
11
+ Dependencies: BaseLintRule, BaseLintContext, ConfigLoader, StorageInitializer, FileAnalyzer,
12
+ DuplicateStorage, ViolationGenerator
13
+
14
+ Exports: DRYRule class
15
+
16
+ Interfaces: DRYRule.check(context) -> list[Violation], finalize() -> list[Violation]
17
+
18
+ Implementation: Delegates all logic to helper classes, maintains only orchestration and state
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING
26
+
27
+ from src.core.base import BaseLintContext, BaseLintRule
28
+ from src.core.linter_utils import should_process_file
29
+ from src.core.types import Violation
30
+
31
+ from .config import DRYConfig
32
+ from .config_loader import ConfigLoader
33
+ from .duplicate_storage import DuplicateStorage
34
+ from .file_analyzer import FileAnalyzer
35
+ from .inline_ignore import InlineIgnoreParser
36
+ from .storage_initializer import StorageInitializer
37
+ from .violation_generator import ViolationGenerator
38
+
39
+ if TYPE_CHECKING:
40
+ from .cache import CodeBlock, DRYCache
41
+
42
+
43
+ @dataclass
44
+ class DRYComponents:
45
+ """Component dependencies for DRY linter."""
46
+
47
+ config_loader: ConfigLoader
48
+ storage_initializer: StorageInitializer
49
+ file_analyzer: FileAnalyzer
50
+ violation_generator: ViolationGenerator
51
+ inline_ignore: InlineIgnoreParser
52
+
53
+
54
+ class DRYRule(BaseLintRule):
55
+ """Detects duplicate code across project files."""
56
+
57
+ def __init__(self) -> None:
58
+ """Initialize the DRY rule with helper components."""
59
+ self._storage: DuplicateStorage | None = None
60
+ self._initialized = False
61
+ self._config: DRYConfig | None = None
62
+ self._file_analyzer: FileAnalyzer | None = None
63
+
64
+ # Helper components grouped to reduce instance attributes
65
+ self._helpers = DRYComponents(
66
+ config_loader=ConfigLoader(),
67
+ storage_initializer=StorageInitializer(),
68
+ file_analyzer=FileAnalyzer(), # Placeholder, will be replaced with configured one
69
+ violation_generator=ViolationGenerator(),
70
+ inline_ignore=InlineIgnoreParser(),
71
+ )
72
+
73
+ @property
74
+ def rule_id(self) -> str:
75
+ """Unique identifier for this rule."""
76
+ return "dry.duplicate-code"
77
+
78
+ @property
79
+ def rule_name(self) -> str:
80
+ """Human-readable name for this rule."""
81
+ return "Duplicate Code"
82
+
83
+ @property
84
+ def description(self) -> str:
85
+ """Description of what this rule checks."""
86
+ return "Detects duplicate code blocks across the project"
87
+
88
+ def check(self, context: BaseLintContext) -> list[Violation]:
89
+ """Analyze file and store blocks (collection phase).
90
+
91
+ Args:
92
+ context: Lint context with file information
93
+
94
+ Returns:
95
+ Empty list (violations returned in finalize())
96
+ """
97
+ if not should_process_file(context):
98
+ return []
99
+
100
+ config = self._helpers.config_loader.load_config(context)
101
+ if not config.enabled:
102
+ return []
103
+
104
+ # Store config for finalize()
105
+ if self._config is None:
106
+ self._config = config
107
+
108
+ # Parse inline ignore directives from this file
109
+ file_path = Path(context.file_path) # type: ignore[arg-type]
110
+ self._helpers.inline_ignore.parse_file(file_path, context.file_content or "")
111
+
112
+ self._ensure_storage_initialized(context, config)
113
+ self._analyze_and_store(context, config)
114
+
115
+ return []
116
+
117
+ def _ensure_storage_initialized(self, context: BaseLintContext, config: DRYConfig) -> None:
118
+ """Initialize storage and file analyzer on first call."""
119
+ if not self._initialized:
120
+ self._storage = self._helpers.storage_initializer.initialize(context, config)
121
+ # Create file analyzer with config for filter configuration
122
+ self._file_analyzer = FileAnalyzer(config)
123
+ self._initialized = True
124
+
125
+ def _analyze_and_store(self, context: BaseLintContext, config: DRYConfig) -> None:
126
+ """Analyze file and store blocks."""
127
+ # Guaranteed by _should_process_file check
128
+ if context.file_path is None or context.file_content is None:
129
+ return # Should never happen due to should_process_file check
130
+
131
+ if not self._file_analyzer:
132
+ return # Should never happen after initialization
133
+
134
+ file_path = Path(context.file_path)
135
+ cache = self._get_cache()
136
+ blocks = self._file_analyzer.analyze_or_load(
137
+ file_path, context.file_content, context.language, config, cache
138
+ )
139
+
140
+ if blocks:
141
+ self._store_blocks(file_path, blocks)
142
+
143
+ def _get_cache(self) -> DRYCache | None:
144
+ """Get cache from storage if available."""
145
+ if not self._storage:
146
+ return None
147
+ return self._storage._cache # pylint: disable=protected-access
148
+
149
+ def _store_blocks(self, file_path: Path, blocks: list[CodeBlock]) -> None:
150
+ """Store blocks in memory if storage available."""
151
+ if self._storage:
152
+ self._storage.add_blocks_to_memory(file_path, blocks)
153
+
154
+ def finalize(self) -> list[Violation]:
155
+ """Generate violations after all files processed.
156
+
157
+ Returns:
158
+ List of all violations found across all files
159
+ """
160
+ if not self._storage or not self._config:
161
+ return []
162
+
163
+ violations = self._helpers.violation_generator.generate_violations(
164
+ self._storage, self.rule_id, self._config, self._helpers.inline_ignore
165
+ )
166
+
167
+ # Clear inline ignore cache for next run
168
+ self._helpers.inline_ignore.clear()
169
+
170
+ return violations