thailint 0.1.5__py3-none-any.whl → 0.5.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/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Storage initialization for DRY linter
|
|
3
|
+
|
|
4
|
+
Scope: Initializes DuplicateStorage with SQLite storage
|
|
5
|
+
|
|
6
|
+
Overview: Handles storage initialization based on DRY configuration. Creates SQLite storage in
|
|
7
|
+
either memory or tempfile mode based on config.storage_mode. Separates initialization logic
|
|
8
|
+
from main linter rule to maintain SRP compliance.
|
|
9
|
+
|
|
10
|
+
Dependencies: BaseLintContext, DRYConfig, DRYCache, DuplicateStorage
|
|
11
|
+
|
|
12
|
+
Exports: StorageInitializer class
|
|
13
|
+
|
|
14
|
+
Interfaces: StorageInitializer.initialize(context, config) -> DuplicateStorage
|
|
15
|
+
|
|
16
|
+
Implementation: Creates DRYCache with storage_mode, delegates to DuplicateStorage for management
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from src.core.base import BaseLintContext
|
|
20
|
+
|
|
21
|
+
from .cache import DRYCache
|
|
22
|
+
from .config import DRYConfig
|
|
23
|
+
from .duplicate_storage import DuplicateStorage
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StorageInitializer:
|
|
27
|
+
"""Initializes storage for duplicate detection."""
|
|
28
|
+
|
|
29
|
+
def initialize(self, context: BaseLintContext, config: DRYConfig) -> DuplicateStorage:
|
|
30
|
+
"""Initialize storage based on configuration.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
context: Lint context
|
|
34
|
+
config: DRY configuration
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
DuplicateStorage instance with SQLite storage
|
|
38
|
+
"""
|
|
39
|
+
# Create SQLite storage (in-memory or tempfile based on config)
|
|
40
|
+
cache = DRYCache(storage_mode=config.storage_mode)
|
|
41
|
+
|
|
42
|
+
return DuplicateStorage(cache)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Tokenization and rolling hash generation for code deduplication
|
|
3
|
+
|
|
4
|
+
Scope: Code normalization, comment stripping, and hash window generation
|
|
5
|
+
|
|
6
|
+
Overview: Implements token-based hashing algorithm (Rabin-Karp) for detecting code duplicates.
|
|
7
|
+
Normalizes source code by stripping comments and whitespace, then generates rolling hash
|
|
8
|
+
windows over consecutive lines. Each window represents a potential duplicate code block.
|
|
9
|
+
Uses Python's built-in hash function for simplicity and performance. Supports both Python
|
|
10
|
+
and JavaScript/TypeScript comment styles.
|
|
11
|
+
|
|
12
|
+
Dependencies: Python built-in hash function
|
|
13
|
+
|
|
14
|
+
Exports: TokenHasher class
|
|
15
|
+
|
|
16
|
+
Interfaces: TokenHasher.tokenize(code: str) -> list[str],
|
|
17
|
+
TokenHasher.rolling_hash(lines: list[str], window_size: int) -> list[tuple]
|
|
18
|
+
|
|
19
|
+
Implementation: Token-based normalization with rolling window algorithm, language-agnostic approach
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TokenHasher:
|
|
24
|
+
"""Tokenize code and create rolling hashes for duplicate detection."""
|
|
25
|
+
|
|
26
|
+
def tokenize(self, code: str) -> list[str]:
|
|
27
|
+
"""Tokenize code by stripping comments and normalizing whitespace.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
code: Source code string
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of normalized code lines (non-empty, comments removed, imports filtered)
|
|
34
|
+
"""
|
|
35
|
+
lines = []
|
|
36
|
+
in_multiline_import = False
|
|
37
|
+
|
|
38
|
+
for line in code.split("\n"):
|
|
39
|
+
line = self._normalize_line(line)
|
|
40
|
+
if not line:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# Update multi-line import state and check if line should be skipped
|
|
44
|
+
in_multiline_import, should_skip = self._should_skip_import_line(
|
|
45
|
+
line, in_multiline_import
|
|
46
|
+
)
|
|
47
|
+
if should_skip:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
lines.append(line)
|
|
51
|
+
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
def _normalize_line(self, line: str) -> str:
|
|
55
|
+
"""Normalize a line by removing comments and excess whitespace.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
line: Raw source code line
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Normalized line (empty string if line has no content)
|
|
62
|
+
"""
|
|
63
|
+
line = self._strip_comments(line)
|
|
64
|
+
return " ".join(line.split())
|
|
65
|
+
|
|
66
|
+
def _should_skip_import_line(self, line: str, in_multiline_import: bool) -> tuple[bool, bool]:
|
|
67
|
+
"""Determine if an import line should be skipped.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
line: Normalized code line
|
|
71
|
+
in_multiline_import: Whether we're currently inside a multi-line import
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (new_in_multiline_import_state, should_skip_line)
|
|
75
|
+
"""
|
|
76
|
+
if self._is_multiline_import_start(line):
|
|
77
|
+
return True, True
|
|
78
|
+
|
|
79
|
+
if in_multiline_import:
|
|
80
|
+
return self._handle_multiline_import_continuation(line)
|
|
81
|
+
|
|
82
|
+
if self._is_import_statement(line):
|
|
83
|
+
return False, True
|
|
84
|
+
|
|
85
|
+
return False, False
|
|
86
|
+
|
|
87
|
+
def _is_multiline_import_start(self, line: str) -> bool:
|
|
88
|
+
"""Check if line starts a multi-line import statement.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
line: Normalized code line
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if line starts a multi-line import (has opening paren but no closing)
|
|
95
|
+
"""
|
|
96
|
+
return self._is_import_statement(line) and "(" in line and ")" not in line
|
|
97
|
+
|
|
98
|
+
def _handle_multiline_import_continuation(self, line: str) -> tuple[bool, bool]:
|
|
99
|
+
"""Handle a line that's part of a multi-line import.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
line: Normalized code line inside a multi-line import
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (still_in_import, should_skip)
|
|
106
|
+
"""
|
|
107
|
+
closes_import = ")" in line
|
|
108
|
+
return not closes_import, True
|
|
109
|
+
|
|
110
|
+
def _strip_comments(self, line: str) -> str:
|
|
111
|
+
"""Remove comments from line (Python # and // style).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
line: Source code line
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Line with comments removed
|
|
118
|
+
"""
|
|
119
|
+
# Python comments
|
|
120
|
+
if "#" in line:
|
|
121
|
+
line = line[: line.index("#")]
|
|
122
|
+
|
|
123
|
+
# JavaScript/TypeScript comments
|
|
124
|
+
if "//" in line:
|
|
125
|
+
line = line[: line.index("//")]
|
|
126
|
+
|
|
127
|
+
return line
|
|
128
|
+
|
|
129
|
+
def _is_import_statement(self, line: str) -> bool:
|
|
130
|
+
"""Check if line is an import statement.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
line: Normalized code line
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if line is an import statement
|
|
137
|
+
"""
|
|
138
|
+
# Check all import/export patterns
|
|
139
|
+
import_prefixes = ("import ", "from ", "export ")
|
|
140
|
+
import_tokens = ("{", "}", "} from")
|
|
141
|
+
|
|
142
|
+
return line.startswith(import_prefixes) or line in import_tokens
|
|
143
|
+
|
|
144
|
+
def rolling_hash(self, lines: list[str], window_size: int) -> list[tuple[int, int, int, str]]:
|
|
145
|
+
"""Create rolling hash windows over code lines.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
lines: List of normalized code lines
|
|
149
|
+
window_size: Number of lines per window (min_duplicate_lines)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of tuples: (hash_value, start_line, end_line, code_snippet)
|
|
153
|
+
"""
|
|
154
|
+
if len(lines) < window_size:
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
hashes = []
|
|
158
|
+
for i in range(len(lines) - window_size + 1):
|
|
159
|
+
window = lines[i : i + window_size]
|
|
160
|
+
snippet = "\n".join(window)
|
|
161
|
+
hash_val = hash(snippet)
|
|
162
|
+
|
|
163
|
+
# Line numbers are 1-indexed
|
|
164
|
+
start_line = i + 1
|
|
165
|
+
end_line = i + window_size
|
|
166
|
+
|
|
167
|
+
hashes.append((hash_val, start_line, end_line, snippet))
|
|
168
|
+
|
|
169
|
+
return hashes
|