thailint 0.5.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 +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- 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 +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- 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 +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- 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 +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- 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/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -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/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- 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 +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/linters/dry/token_hasher.py
CHANGED
|
@@ -11,159 +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
|
-
|
|
36
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
List of normalized code lines (non-empty, comments removed, imports filtered)
|
|
37
|
+
"""
|
|
38
|
+
lines = []
|
|
39
|
+
in_multiline_import = False
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
for line in code.split("\n"):
|
|
42
|
+
line = normalize_line(line)
|
|
43
|
+
if not line:
|
|
44
|
+
continue
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if should_skip:
|
|
48
|
-
continue
|
|
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
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
lines.append(line)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
return lines
|
|
53
54
|
|
|
54
|
-
def _normalize_line(self, line: str) -> str:
|
|
55
|
-
"""Normalize a line by removing comments and excess whitespace.
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
def normalize_line(line: str) -> str:
|
|
57
|
+
"""Normalize a line by removing comments and excess whitespace.
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
line = self._strip_comments(line)
|
|
64
|
-
return " ".join(line.split())
|
|
59
|
+
Args:
|
|
60
|
+
line: Raw source code line
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
Returns:
|
|
63
|
+
Normalized line (empty string if line has no content)
|
|
64
|
+
"""
|
|
65
|
+
line = _strip_comments(line)
|
|
66
|
+
return " ".join(line.split())
|
|
68
67
|
|
|
69
|
-
Args:
|
|
70
|
-
line: Normalized code line
|
|
71
|
-
in_multiline_import: Whether we're currently inside a multi-line import
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""
|
|
76
|
-
if self._is_multiline_import_start(line):
|
|
77
|
-
return True, True
|
|
69
|
+
def should_skip_import_line(line: str, in_multiline_import: bool) -> tuple[bool, bool]:
|
|
70
|
+
"""Determine if an import line should be skipped.
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
Args:
|
|
73
|
+
line: Normalized code line
|
|
74
|
+
in_multiline_import: Whether we're currently inside a multi-line import
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
81
|
|
|
85
|
-
|
|
82
|
+
if in_multiline_import:
|
|
83
|
+
return _handle_multiline_import_continuation(line)
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if _is_import_statement(line):
|
|
86
|
+
return False, True
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
line: Normalized code line
|
|
88
|
+
return False, False
|
|
92
89
|
|
|
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
90
|
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
def _is_multiline_import_start(line: str) -> bool:
|
|
92
|
+
"""Check if line starts a multi-line import statement.
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
Args:
|
|
95
|
+
line: Normalized code line
|
|
103
96
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return not closes_import, True
|
|
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
|
|
109
101
|
|
|
110
|
-
def _strip_comments(self, line: str) -> str:
|
|
111
|
-
"""Remove comments from line (Python # and // style).
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
def _handle_multiline_import_continuation(line: str) -> tuple[bool, bool]:
|
|
104
|
+
"""Handle a line that's part of a multi-line import.
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"""
|
|
119
|
-
# Python comments
|
|
120
|
-
if "#" in line:
|
|
121
|
-
line = line[: line.index("#")]
|
|
106
|
+
Args:
|
|
107
|
+
line: Normalized code line inside a multi-line import
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
return line
|
|
128
115
|
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
def _strip_comments(line: str) -> str:
|
|
117
|
+
"""Remove comments from line (Python # and // style).
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
|
|
119
|
+
Args:
|
|
120
|
+
line: Source code line
|
|
134
121
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
122
|
+
Returns:
|
|
123
|
+
Line with comments removed
|
|
124
|
+
"""
|
|
125
|
+
# Python comments
|
|
126
|
+
if "#" in line:
|
|
127
|
+
line = line[: line.index("#")]
|
|
141
128
|
|
|
142
|
-
|
|
129
|
+
# JavaScript/TypeScript comments
|
|
130
|
+
if "//" in line:
|
|
131
|
+
line = line[: line.index("//")]
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
"""Create rolling hash windows over code lines.
|
|
133
|
+
return line
|
|
146
134
|
|
|
147
|
-
Args:
|
|
148
|
-
lines: List of normalized code lines
|
|
149
|
-
window_size: Number of lines per window (min_duplicate_lines)
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"""
|
|
154
|
-
if len(lines) < window_size:
|
|
155
|
-
return []
|
|
136
|
+
def _is_import_statement(line: str) -> bool:
|
|
137
|
+
"""Check if line is an import statement.
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
window = lines[i : i + window_size]
|
|
160
|
-
snippet = "\n".join(window)
|
|
161
|
-
hash_val = hash(snippet)
|
|
139
|
+
Args:
|
|
140
|
+
line: Normalized code line
|
|
162
141
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
hashes.append((hash_val, start_line, end_line, snippet))
|
|
168
147
|
|
|
169
|
-
|
|
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
|