thailint 0.2.0__py3-none-any.whl → 0.15.3__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 +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -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 +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -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 +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- 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 +100 -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 +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- 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 +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -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 +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
src/linters/dry/token_hasher.py
CHANGED
|
@@ -11,105 +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:
|
|
24
|
-
"""Tokenize code and create rolling hashes for duplicate detection."""
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
def tokenize(code: str) -> list[str]:
|
|
30
|
+
"""Tokenize code by stripping comments and normalizing whitespace.
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
Args:
|
|
33
|
+
code: Source code string
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
List of normalized code lines (non-empty, comments removed, imports filtered)
|
|
37
|
+
"""
|
|
38
|
+
lines = []
|
|
39
|
+
in_multiline_import = False
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
for line in code.split("\n"):
|
|
42
|
+
line = normalize_line(line)
|
|
43
|
+
if not line:
|
|
44
|
+
continue
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
if not line:
|
|
46
|
-
continue
|
|
51
|
+
lines.append(line)
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
if self._is_import_statement(line):
|
|
50
|
-
continue
|
|
53
|
+
return lines
|
|
51
54
|
|
|
52
|
-
lines.append(line)
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
def normalize_line(line: str) -> str:
|
|
57
|
+
"""Normalize a line by removing comments and excess whitespace.
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
Args:
|
|
60
|
+
line: Raw source code line
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
Returns:
|
|
63
|
+
Normalized line (empty string if line has no content)
|
|
64
|
+
"""
|
|
65
|
+
line = _strip_comments(line)
|
|
66
|
+
return " ".join(line.split())
|
|
61
67
|
|
|
62
|
-
Returns:
|
|
63
|
-
Line with comments removed
|
|
64
|
-
"""
|
|
65
|
-
# Python comments
|
|
66
|
-
if "#" in line:
|
|
67
|
-
line = line[: line.index("#")]
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
line = line[: line.index("//")]
|
|
69
|
+
def should_skip_import_line(line: str, in_multiline_import: bool) -> tuple[bool, bool]:
|
|
70
|
+
"""Determine if an import line should be skipped.
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
Args:
|
|
73
|
+
line: Normalized code line
|
|
74
|
+
in_multiline_import: Whether we're currently inside a multi-line import
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
if in_multiline_import:
|
|
83
|
+
return _handle_multiline_import_continuation(line)
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"""
|
|
84
|
-
# Check all import/export patterns
|
|
85
|
-
import_prefixes = ("import ", "from ", "export ")
|
|
86
|
-
import_tokens = ("{", "}", "} from")
|
|
85
|
+
if _is_import_statement(line):
|
|
86
|
+
return False, True
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
return False, False
|
|
89
89
|
|
|
90
|
-
def rolling_hash(self, lines: list[str], window_size: int) -> list[tuple[int, int, int, str]]:
|
|
91
|
-
"""Create rolling hash windows over code lines.
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
window_size: Number of lines per window (min_duplicate_lines)
|
|
91
|
+
def _is_multiline_import_start(line: str) -> bool:
|
|
92
|
+
"""Check if line starts a multi-line import statement.
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"""
|
|
100
|
-
if len(lines) < window_size:
|
|
101
|
-
return []
|
|
94
|
+
Args:
|
|
95
|
+
line: Normalized code line
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
hash_val = hash(snippet)
|
|
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
|
|
108
101
|
|
|
109
|
-
# Line numbers are 1-indexed
|
|
110
|
-
start_line = i + 1
|
|
111
|
-
end_line = i + window_size
|
|
112
102
|
|
|
113
|
-
|
|
103
|
+
def _handle_multiline_import_continuation(line: str) -> tuple[bool, bool]:
|
|
104
|
+
"""Handle a line that's part of a multi-line import.
|
|
114
105
|
|
|
115
|
-
|
|
106
|
+
Args:
|
|
107
|
+
line: Normalized code line inside a multi-line import
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (still_in_import, should_skip)
|
|
111
|
+
"""
|
|
112
|
+
closes_import = ")" in line
|
|
113
|
+
return not closes_import, True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _strip_comments(line: str) -> str:
|
|
117
|
+
"""Remove comments from line (Python # and // style).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
line: Source code line
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Line with comments removed
|
|
124
|
+
"""
|
|
125
|
+
# Python comments
|
|
126
|
+
if "#" in line:
|
|
127
|
+
line = line[: line.index("#")]
|
|
128
|
+
|
|
129
|
+
# JavaScript/TypeScript comments
|
|
130
|
+
if "//" in line:
|
|
131
|
+
line = line[: line.index("//")]
|
|
132
|
+
|
|
133
|
+
return line
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _is_import_statement(line: str) -> bool:
|
|
137
|
+
"""Check if line is an import statement.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
line: Normalized code line
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if line is an import statement
|
|
144
|
+
"""
|
|
145
|
+
return line.startswith(_IMPORT_PREFIXES) or line in _IMPORT_TOKENS
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def rolling_hash(lines: list[str], window_size: int) -> list[tuple[int, int, int, str]]:
|
|
149
|
+
"""Create rolling hash windows over code lines.
|
|
150
|
+
|
|
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
|