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.
Files changed (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -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: 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 = []
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
- for line in code.split("\n"):
38
- # Remove comments (language-specific logic can be added)
39
- line = self._strip_comments(line)
41
+ for line in code.split("\n"):
42
+ line = normalize_line(line)
43
+ if not line:
44
+ continue
40
45
 
41
- # Normalize whitespace (collapse to single space)
42
- line = " ".join(line.split())
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
- # Skip empty lines
45
- if not line:
46
- continue
51
+ lines.append(line)
47
52
 
48
- # Skip import statements (common false positive)
49
- if self._is_import_statement(line):
50
- continue
53
+ return lines
51
54
 
52
- lines.append(line)
53
55
 
54
- return lines
56
+ def normalize_line(line: str) -> str:
57
+ """Normalize a line by removing comments and excess whitespace.
55
58
 
56
- def _strip_comments(self, line: str) -> str:
57
- """Remove comments from line (Python # and // style).
59
+ Args:
60
+ line: Raw source code line
58
61
 
59
- Args:
60
- line: Source code line
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
- # JavaScript/TypeScript comments
70
- if "//" in line:
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
- return line
72
+ Args:
73
+ line: Normalized code line
74
+ in_multiline_import: Whether we're currently inside a multi-line import
74
75
 
75
- def _is_import_statement(self, line: str) -> bool:
76
- """Check if line is an import statement.
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
- Args:
79
- line: Normalized code line
82
+ if in_multiline_import:
83
+ return _handle_multiline_import_continuation(line)
80
84
 
81
- Returns:
82
- True if line is an import statement
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
- return line.startswith(import_prefixes) or line in import_tokens
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
- Args:
94
- lines: List of normalized code lines
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
- Returns:
98
- List of tuples: (hash_value, start_line, end_line, code_snippet)
99
- """
100
- if len(lines) < window_size:
101
- return []
94
+ Args:
95
+ line: Normalized code line
102
96
 
103
- hashes = []
104
- for i in range(len(lines) - window_size + 1):
105
- window = lines[i : i + window_size]
106
- snippet = "\n".join(window)
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
- hashes.append((hash_val, start_line, end_line, snippet))
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
- return hashes
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