thailint 0.11.0__py3-none-any.whl → 0.13.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/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +118 -7
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli/utils.py +29 -9
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/__init__.py +22 -9
- src/linters/stringly_typed/config.py +32 -8
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +102 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +9 -5
- src/linters/stringly_typed/python/analyzer.py +159 -9
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +630 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +405 -0
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.11.0.dist-info/METADATA +0 -1661
- thailint-0.11.0.dist-info/RECORD +0 -150
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
src/linters/dry/token_hasher.py
CHANGED
|
@@ -11,163 +11,163 @@ Overview: Implements token-based hashing algorithm (Rabin-Karp) for detecting co
|
|
|
11
11
|
|
|
12
12
|
Dependencies: Python built-in hash function
|
|
13
13
|
|
|
14
|
-
Exports:
|
|
14
|
+
Exports: tokenize, rolling_hash, normalize_line, should_skip_import_line functions
|
|
15
15
|
|
|
16
|
-
Interfaces:
|
|
17
|
-
|
|
16
|
+
Interfaces: tokenize(code: str) -> list[str],
|
|
17
|
+
rolling_hash(lines: list[str], window_size: int) -> list[tuple],
|
|
18
|
+
normalize_line(line: str) -> str,
|
|
19
|
+
should_skip_import_line(line: str, in_multiline_import: bool) -> tuple
|
|
18
20
|
|
|
19
21
|
Implementation: Token-based normalization with rolling window algorithm, language-agnostic approach
|
|
20
22
|
"""
|
|
21
23
|
|
|
24
|
+
# Pre-compiled import token set for O(1) membership test
|
|
25
|
+
_IMPORT_TOKENS: frozenset[str] = frozenset(("{", "}", "} from"))
|
|
26
|
+
_IMPORT_PREFIXES: tuple[str, ...] = ("import ", "from ", "export ")
|
|
22
27
|
|
|
23
|
-
class TokenHasher: # thailint: ignore[srp] - Methods support single responsibility of code tokenization
|
|
24
|
-
"""Tokenize code and create rolling hashes for duplicate detection."""
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
pass # Stateless hasher for code tokenization
|
|
29
|
+
def tokenize(code: str) -> list[str]:
|
|
30
|
+
"""Tokenize code by stripping comments and normalizing whitespace.
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
Args:
|
|
33
|
+
code: Source code string
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
List of normalized code lines (non-empty, comments removed, imports filtered)
|
|
37
|
+
"""
|
|
38
|
+
lines = []
|
|
39
|
+
in_multiline_import = False
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
in_multiline_import = False
|
|
41
|
+
for line in code.split("\n"):
|
|
42
|
+
line = normalize_line(line)
|
|
43
|
+
if not line:
|
|
44
|
+
continue
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
# Update multi-line import state and check if line should be skipped
|
|
47
|
+
in_multiline_import, should_skip = should_skip_import_line(line, in_multiline_import)
|
|
48
|
+
if should_skip:
|
|
49
|
+
continue
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
in_multiline_import, should_skip = self._should_skip_import_line(
|
|
49
|
-
line, in_multiline_import
|
|
50
|
-
)
|
|
51
|
-
if should_skip:
|
|
52
|
-
continue
|
|
51
|
+
lines.append(line)
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
return lines
|
|
55
54
|
|
|
56
|
-
return lines
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
def normalize_line(line: str) -> str:
|
|
57
|
+
"""Normalize a line by removing comments and excess whitespace.
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
Args:
|
|
60
|
+
line: Raw source code line
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
Returns:
|
|
63
|
+
Normalized line (empty string if line has no content)
|
|
64
|
+
"""
|
|
65
|
+
line = _strip_comments(line)
|
|
66
|
+
return " ".join(line.split())
|
|
69
67
|
|
|
70
|
-
def _should_skip_import_line(self, line: str, in_multiline_import: bool) -> tuple[bool, bool]:
|
|
71
|
-
"""Determine if an import line should be skipped.
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
in_multiline_import: Whether we're currently inside a multi-line import
|
|
69
|
+
def should_skip_import_line(line: str, in_multiline_import: bool) -> tuple[bool, bool]:
|
|
70
|
+
"""Determine if an import line should be skipped.
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if self._is_multiline_import_start(line):
|
|
81
|
-
return True, True
|
|
72
|
+
Args:
|
|
73
|
+
line: Normalized code line
|
|
74
|
+
in_multiline_import: Whether we're currently inside a multi-line import
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple of (new_in_multiline_import_state, should_skip_line)
|
|
78
|
+
"""
|
|
79
|
+
if _is_multiline_import_start(line):
|
|
80
|
+
return True, True
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
if in_multiline_import:
|
|
83
|
+
return _handle_multiline_import_continuation(line)
|
|
88
84
|
|
|
89
|
-
|
|
85
|
+
if _is_import_statement(line):
|
|
86
|
+
return False, True
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
"""Check if line starts a multi-line import statement.
|
|
88
|
+
return False, False
|
|
93
89
|
|
|
94
|
-
Args:
|
|
95
|
-
line: Normalized code line
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"""
|
|
100
|
-
return self._is_import_statement(line) and "(" in line and ")" not in line
|
|
91
|
+
def _is_multiline_import_start(line: str) -> bool:
|
|
92
|
+
"""Check if line starts a multi-line import statement.
|
|
101
93
|
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
Args:
|
|
95
|
+
line: Normalized code line
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
Returns:
|
|
98
|
+
True if line starts a multi-line import (has opening paren but no closing)
|
|
99
|
+
"""
|
|
100
|
+
return _is_import_statement(line) and "(" in line and ")" not in line
|
|
107
101
|
|
|
108
|
-
Returns:
|
|
109
|
-
Tuple of (still_in_import, should_skip)
|
|
110
|
-
"""
|
|
111
|
-
closes_import = ")" in line
|
|
112
|
-
return not closes_import, True
|
|
113
102
|
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
def _handle_multiline_import_continuation(line: str) -> tuple[bool, bool]:
|
|
104
|
+
"""Handle a line that's part of a multi-line import.
|
|
116
105
|
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
Args:
|
|
107
|
+
line: Normalized code line inside a multi-line import
|
|
119
108
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
line = line[: line.index("#")]
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (still_in_import, should_skip)
|
|
111
|
+
"""
|
|
112
|
+
closes_import = ")" in line
|
|
113
|
+
return not closes_import, True
|
|
126
114
|
|
|
127
|
-
# JavaScript/TypeScript comments
|
|
128
|
-
if "//" in line:
|
|
129
|
-
line = line[: line.index("//")]
|
|
130
115
|
|
|
131
|
-
|
|
116
|
+
def _strip_comments(line: str) -> str:
|
|
117
|
+
"""Remove comments from line (Python # and // style).
|
|
132
118
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
_IMPORT_PREFIXES: tuple[str, ...] = ("import ", "from ", "export ")
|
|
119
|
+
Args:
|
|
120
|
+
line: Source code line
|
|
136
121
|
|
|
137
|
-
|
|
138
|
-
|
|
122
|
+
Returns:
|
|
123
|
+
Line with comments removed
|
|
124
|
+
"""
|
|
125
|
+
# Python comments
|
|
126
|
+
if "#" in line:
|
|
127
|
+
line = line[: line.index("#")]
|
|
139
128
|
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
# JavaScript/TypeScript comments
|
|
130
|
+
if "//" in line:
|
|
131
|
+
line = line[: line.index("//")]
|
|
142
132
|
|
|
143
|
-
|
|
144
|
-
True if line is an import statement
|
|
145
|
-
"""
|
|
146
|
-
return line.startswith(self._IMPORT_PREFIXES) or line in self._IMPORT_TOKENS
|
|
133
|
+
return line
|
|
147
134
|
|
|
148
|
-
def rolling_hash(self, lines: list[str], window_size: int) -> list[tuple[int, int, int, str]]:
|
|
149
|
-
"""Create rolling hash windows over code lines.
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
window_size: Number of lines per window (min_duplicate_lines)
|
|
136
|
+
def _is_import_statement(line: str) -> bool:
|
|
137
|
+
"""Check if line is an import statement.
|
|
154
138
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
if len(lines) < window_size:
|
|
159
|
-
return []
|
|
139
|
+
Args:
|
|
140
|
+
line: Normalized code line
|
|
160
141
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
hash_val = hash(snippet)
|
|
142
|
+
Returns:
|
|
143
|
+
True if line is an import statement
|
|
144
|
+
"""
|
|
145
|
+
return line.startswith(_IMPORT_PREFIXES) or line in _IMPORT_TOKENS
|
|
166
146
|
|
|
167
|
-
# Line numbers are 1-indexed
|
|
168
|
-
start_line = i + 1
|
|
169
|
-
end_line = i + window_size
|
|
170
147
|
|
|
171
|
-
|
|
148
|
+
def rolling_hash(lines: list[str], window_size: int) -> list[tuple[int, int, int, str]]:
|
|
149
|
+
"""Create rolling hash windows over code lines.
|
|
172
150
|
|
|
173
|
-
|
|
151
|
+
Args:
|
|
152
|
+
lines: List of normalized code lines
|
|
153
|
+
window_size: Number of lines per window (min_duplicate_lines)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of tuples: (hash_value, start_line, end_line, code_snippet)
|
|
157
|
+
"""
|
|
158
|
+
if len(lines) < window_size:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
hashes = []
|
|
162
|
+
for i in range(len(lines) - window_size + 1):
|
|
163
|
+
window = lines[i : i + window_size]
|
|
164
|
+
snippet = "\n".join(window)
|
|
165
|
+
hash_val = hash(snippet)
|
|
166
|
+
|
|
167
|
+
# Line numbers are 1-indexed
|
|
168
|
+
start_line = i + 1
|
|
169
|
+
end_line = i + window_size
|
|
170
|
+
|
|
171
|
+
hashes.append((hash_val, start_line, end_line, snippet))
|
|
172
|
+
|
|
173
|
+
return hashes
|
|
@@ -19,6 +19,11 @@ Interfaces: TypeScriptDuplicateAnalyzer.analyze(file_path: Path, content: str, c
|
|
|
19
19
|
Implementation: Inherits analyze() workflow from BaseTokenAnalyzer, adds JSDoc comment extraction,
|
|
20
20
|
single statement detection using tree-sitter AST patterns, and interface filtering logic
|
|
21
21
|
|
|
22
|
+
Suppressions:
|
|
23
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
24
|
+
- invalid-name: Node type alias follows tree-sitter naming convention
|
|
25
|
+
- srp.violation: Complex tree-sitter AST analysis algorithm. See SRP Exception below.
|
|
26
|
+
|
|
22
27
|
SRP Exception: TypeScriptDuplicateAnalyzer has 20 methods and 324 lines (exceeds max 8 methods/200 lines)
|
|
23
28
|
Justification: Complex tree-sitter AST analysis algorithm for duplicate code detection with sophisticated
|
|
24
29
|
false positive filtering. Mirrors Python analyzer structure. Methods form tightly coupled algorithm
|
|
@@ -35,11 +40,12 @@ from pathlib import Path
|
|
|
35
40
|
|
|
36
41
|
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
|
|
37
42
|
|
|
43
|
+
from . import token_hasher
|
|
38
44
|
from .base_token_analyzer import BaseTokenAnalyzer
|
|
39
45
|
from .block_filter import BlockFilterRegistry, create_default_registry
|
|
40
46
|
from .cache import CodeBlock
|
|
41
47
|
from .config import DRYConfig
|
|
42
|
-
from .typescript_statement_detector import
|
|
48
|
+
from .typescript_statement_detector import is_single_statement, should_include_block
|
|
43
49
|
|
|
44
50
|
if TREE_SITTER_AVAILABLE:
|
|
45
51
|
from tree_sitter import Node
|
|
@@ -62,7 +68,6 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
|
|
|
62
68
|
"""
|
|
63
69
|
super().__init__()
|
|
64
70
|
self._filter_registry = filter_registry or create_default_registry()
|
|
65
|
-
self._statement_detector = TypeScriptStatementDetector()
|
|
66
71
|
|
|
67
72
|
def analyze(self, file_path: Path, content: str, config: DRYConfig) -> list[CodeBlock]:
|
|
68
73
|
"""Analyze TypeScript/JavaScript file for duplicate code blocks.
|
|
@@ -90,8 +95,8 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
|
|
|
90
95
|
valid_windows = (
|
|
91
96
|
(hash_val, start_line, end_line, snippet)
|
|
92
97
|
for hash_val, start_line, end_line, snippet in windows
|
|
93
|
-
if
|
|
94
|
-
and not
|
|
98
|
+
if should_include_block(content, start_line, end_line)
|
|
99
|
+
and not is_single_statement(content, start_line, end_line)
|
|
95
100
|
)
|
|
96
101
|
return self._build_blocks(valid_windows, file_path, content)
|
|
97
102
|
|
|
@@ -229,11 +234,11 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
|
|
|
229
234
|
Returns:
|
|
230
235
|
Tuple of (new_import_state, normalized_line or None if should skip)
|
|
231
236
|
"""
|
|
232
|
-
normalized =
|
|
237
|
+
normalized = token_hasher.normalize_line(line)
|
|
233
238
|
if not normalized:
|
|
234
239
|
return in_multiline_import, None
|
|
235
240
|
|
|
236
|
-
new_state, should_skip =
|
|
241
|
+
new_state, should_skip = token_hasher.should_skip_import_line(
|
|
237
242
|
normalized, in_multiline_import
|
|
238
243
|
)
|
|
239
244
|
if should_skip:
|
|
@@ -17,6 +17,10 @@ Exports: TypeScriptConstantExtractor class
|
|
|
17
17
|
Interfaces: TypeScriptConstantExtractor.extract(content: str) -> list[ConstantInfo]
|
|
18
18
|
|
|
19
19
|
Implementation: Tree-sitter-based parsing with const declaration filtering and ALL_CAPS regex matching
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
23
|
+
- broad-exception-caught: Defensive parsing for malformed TypeScript code
|
|
20
24
|
"""
|
|
21
25
|
|
|
22
26
|
from typing import Any
|