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.
Files changed (129) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +118 -7
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -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: 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: # thailint: ignore[srp] - Methods support single responsibility of code tokenization
24
- """Tokenize code and create rolling hashes for duplicate detection."""
25
28
 
26
- def __init__(self) -> None:
27
- """Initialize the token hasher."""
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
- def tokenize(self, code: str) -> list[str]:
31
- """Tokenize code by stripping comments and normalizing whitespace.
32
+ Args:
33
+ code: Source code string
32
34
 
33
- Args:
34
- code: Source code string
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
- Returns:
37
- List of normalized code lines (non-empty, comments removed, imports filtered)
38
- """
39
- lines = []
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
- for line in code.split("\n"):
43
- line = self._normalize_line(line)
44
- if not line:
45
- 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
46
50
 
47
- # Update multi-line import state and check if line should be skipped
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
- lines.append(line)
53
+ return lines
55
54
 
56
- return lines
57
55
 
58
- def _normalize_line(self, line: str) -> str:
59
- """Normalize a line by removing comments and excess whitespace.
56
+ def normalize_line(line: str) -> str:
57
+ """Normalize a line by removing comments and excess whitespace.
60
58
 
61
- Args:
62
- line: Raw source code line
59
+ Args:
60
+ line: Raw source code line
63
61
 
64
- Returns:
65
- Normalized line (empty string if line has no content)
66
- """
67
- line = self._strip_comments(line)
68
- return " ".join(line.split())
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
- Args:
74
- line: Normalized code line
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
- Returns:
78
- Tuple of (new_in_multiline_import_state, should_skip_line)
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
- if in_multiline_import:
84
- return self._handle_multiline_import_continuation(line)
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
- if self._is_import_statement(line):
87
- return False, True
82
+ if in_multiline_import:
83
+ return _handle_multiline_import_continuation(line)
88
84
 
89
- return False, False
85
+ if _is_import_statement(line):
86
+ return False, True
90
87
 
91
- def _is_multiline_import_start(self, line: str) -> bool:
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
- Returns:
98
- True if line starts a multi-line import (has opening paren but no closing)
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
- def _handle_multiline_import_continuation(self, line: str) -> tuple[bool, bool]:
103
- """Handle a line that's part of a multi-line import.
94
+ Args:
95
+ line: Normalized code line
104
96
 
105
- Args:
106
- line: Normalized code line inside a multi-line import
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
- def _strip_comments(self, line: str) -> str:
115
- """Remove comments from line (Python # and // style).
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
- Args:
118
- line: Source code line
106
+ Args:
107
+ line: Normalized code line inside a multi-line import
119
108
 
120
- Returns:
121
- Line with comments removed
122
- """
123
- # Python 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
- # JavaScript/TypeScript comments
128
- if "//" in line:
129
- line = line[: line.index("//")]
130
115
 
131
- return line
116
+ def _strip_comments(line: str) -> str:
117
+ """Remove comments from line (Python # and // style).
132
118
 
133
- # Pre-compiled import token set for O(1) membership test
134
- _IMPORT_TOKENS: frozenset[str] = frozenset(("{", "}", "} from"))
135
- _IMPORT_PREFIXES: tuple[str, ...] = ("import ", "from ", "export ")
119
+ Args:
120
+ line: Source code line
136
121
 
137
- def _is_import_statement(self, line: str) -> bool:
138
- """Check if line is an import statement.
122
+ Returns:
123
+ Line with comments removed
124
+ """
125
+ # Python comments
126
+ if "#" in line:
127
+ line = line[: line.index("#")]
139
128
 
140
- Args:
141
- line: Normalized code line
129
+ # JavaScript/TypeScript comments
130
+ if "//" in line:
131
+ line = line[: line.index("//")]
142
132
 
143
- Returns:
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
- Args:
152
- lines: List of normalized code lines
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
- Returns:
156
- List of tuples: (hash_value, start_line, end_line, code_snippet)
157
- """
158
- if len(lines) < window_size:
159
- return []
139
+ Args:
140
+ line: Normalized code line
160
141
 
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)
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
- hashes.append((hash_val, start_line, end_line, snippet))
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
- return hashes
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 TypeScriptStatementDetector
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 self._statement_detector.should_include_block(content, start_line, end_line)
94
- and not self._statement_detector.is_single_statement(content, start_line, end_line)
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 = self._hasher._normalize_line(line) # pylint: disable=protected-access
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 = self._hasher._should_skip_import_line( # pylint: disable=protected-access
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