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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -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: TokenHasher class
14
+ Exports: tokenize, rolling_hash, normalize_line, should_skip_import_line functions
15
15
 
16
- Interfaces: TokenHasher.tokenize(code: str) -> list[str],
17
- TokenHasher.rolling_hash(lines: list[str], window_size: int) -> list[tuple]
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
- def tokenize(self, code: str) -> list[str]:
27
- """Tokenize code by stripping comments and normalizing whitespace.
29
+ def tokenize(code: str) -> list[str]:
30
+ """Tokenize code by stripping comments and normalizing whitespace.
28
31
 
29
- Args:
30
- code: Source code string
32
+ Args:
33
+ code: Source code string
31
34
 
32
- Returns:
33
- List of normalized code lines (non-empty, comments removed, imports filtered)
34
- """
35
- lines = []
36
- in_multiline_import = False
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
- for line in code.split("\n"):
39
- line = self._normalize_line(line)
40
- if not line:
41
- continue
41
+ for line in code.split("\n"):
42
+ line = normalize_line(line)
43
+ if not line:
44
+ continue
42
45
 
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
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
- lines.append(line)
51
+ lines.append(line)
51
52
 
52
- return lines
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
- Args:
58
- line: Raw source code line
56
+ def normalize_line(line: str) -> str:
57
+ """Normalize a line by removing comments and excess whitespace.
59
58
 
60
- Returns:
61
- Normalized line (empty string if line has no content)
62
- """
63
- line = self._strip_comments(line)
64
- return " ".join(line.split())
59
+ Args:
60
+ line: Raw source code line
65
61
 
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.
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
- 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
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
- if in_multiline_import:
80
- return self._handle_multiline_import_continuation(line)
72
+ Args:
73
+ line: Normalized code line
74
+ in_multiline_import: Whether we're currently inside a multi-line import
81
75
 
82
- if self._is_import_statement(line):
83
- return False, True
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
- return False, False
82
+ if in_multiline_import:
83
+ return _handle_multiline_import_continuation(line)
86
84
 
87
- def _is_multiline_import_start(self, line: str) -> bool:
88
- """Check if line starts a multi-line import statement.
85
+ if _is_import_statement(line):
86
+ return False, True
89
87
 
90
- Args:
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
- def _handle_multiline_import_continuation(self, line: str) -> tuple[bool, bool]:
99
- """Handle a line that's part of a multi-line import.
91
+ def _is_multiline_import_start(line: str) -> bool:
92
+ """Check if line starts a multi-line import statement.
100
93
 
101
- Args:
102
- line: Normalized code line inside a multi-line import
94
+ Args:
95
+ line: Normalized code line
103
96
 
104
- Returns:
105
- Tuple of (still_in_import, should_skip)
106
- """
107
- closes_import = ")" in line
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
- Args:
114
- line: Source code line
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
- Returns:
117
- Line with comments removed
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
- # JavaScript/TypeScript comments
124
- if "//" in line:
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
- return line
128
115
 
129
- def _is_import_statement(self, line: str) -> bool:
130
- """Check if line is an import statement.
116
+ def _strip_comments(line: str) -> str:
117
+ """Remove comments from line (Python # and // style).
131
118
 
132
- Args:
133
- line: Normalized code line
119
+ Args:
120
+ line: Source code line
134
121
 
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")
122
+ Returns:
123
+ Line with comments removed
124
+ """
125
+ # Python comments
126
+ if "#" in line:
127
+ line = line[: line.index("#")]
141
128
 
142
- return line.startswith(import_prefixes) or line in import_tokens
129
+ # JavaScript/TypeScript comments
130
+ if "//" in line:
131
+ line = line[: line.index("//")]
143
132
 
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.
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
- Returns:
152
- List of tuples: (hash_value, start_line, end_line, code_snippet)
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
- 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)
139
+ Args:
140
+ line: Normalized code line
162
141
 
163
- # Line numbers are 1-indexed
164
- start_line = i + 1
165
- end_line = i + window_size
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
- return hashes
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