thailint 0.1.5__py3-none-any.whl → 0.5.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 (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,42 @@
1
+ """
2
+ Purpose: Storage initialization for DRY linter
3
+
4
+ Scope: Initializes DuplicateStorage with SQLite storage
5
+
6
+ Overview: Handles storage initialization based on DRY configuration. Creates SQLite storage in
7
+ either memory or tempfile mode based on config.storage_mode. Separates initialization logic
8
+ from main linter rule to maintain SRP compliance.
9
+
10
+ Dependencies: BaseLintContext, DRYConfig, DRYCache, DuplicateStorage
11
+
12
+ Exports: StorageInitializer class
13
+
14
+ Interfaces: StorageInitializer.initialize(context, config) -> DuplicateStorage
15
+
16
+ Implementation: Creates DRYCache with storage_mode, delegates to DuplicateStorage for management
17
+ """
18
+
19
+ from src.core.base import BaseLintContext
20
+
21
+ from .cache import DRYCache
22
+ from .config import DRYConfig
23
+ from .duplicate_storage import DuplicateStorage
24
+
25
+
26
+ class StorageInitializer:
27
+ """Initializes storage for duplicate detection."""
28
+
29
+ def initialize(self, context: BaseLintContext, config: DRYConfig) -> DuplicateStorage:
30
+ """Initialize storage based on configuration.
31
+
32
+ Args:
33
+ context: Lint context
34
+ config: DRY configuration
35
+
36
+ Returns:
37
+ DuplicateStorage instance with SQLite storage
38
+ """
39
+ # Create SQLite storage (in-memory or tempfile based on config)
40
+ cache = DRYCache(storage_mode=config.storage_mode)
41
+
42
+ return DuplicateStorage(cache)
@@ -0,0 +1,169 @@
1
+ """
2
+ Purpose: Tokenization and rolling hash generation for code deduplication
3
+
4
+ Scope: Code normalization, comment stripping, and hash window generation
5
+
6
+ Overview: Implements token-based hashing algorithm (Rabin-Karp) for detecting code duplicates.
7
+ Normalizes source code by stripping comments and whitespace, then generates rolling hash
8
+ windows over consecutive lines. Each window represents a potential duplicate code block.
9
+ Uses Python's built-in hash function for simplicity and performance. Supports both Python
10
+ and JavaScript/TypeScript comment styles.
11
+
12
+ Dependencies: Python built-in hash function
13
+
14
+ Exports: TokenHasher class
15
+
16
+ Interfaces: TokenHasher.tokenize(code: str) -> list[str],
17
+ TokenHasher.rolling_hash(lines: list[str], window_size: int) -> list[tuple]
18
+
19
+ Implementation: Token-based normalization with rolling window algorithm, language-agnostic approach
20
+ """
21
+
22
+
23
+ class TokenHasher:
24
+ """Tokenize code and create rolling hashes for duplicate detection."""
25
+
26
+ def tokenize(self, code: str) -> list[str]:
27
+ """Tokenize code by stripping comments and normalizing whitespace.
28
+
29
+ Args:
30
+ code: Source code string
31
+
32
+ Returns:
33
+ List of normalized code lines (non-empty, comments removed, imports filtered)
34
+ """
35
+ lines = []
36
+ in_multiline_import = False
37
+
38
+ for line in code.split("\n"):
39
+ line = self._normalize_line(line)
40
+ if not line:
41
+ continue
42
+
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
49
+
50
+ lines.append(line)
51
+
52
+ return lines
53
+
54
+ def _normalize_line(self, line: str) -> str:
55
+ """Normalize a line by removing comments and excess whitespace.
56
+
57
+ Args:
58
+ line: Raw source code line
59
+
60
+ Returns:
61
+ Normalized line (empty string if line has no content)
62
+ """
63
+ line = self._strip_comments(line)
64
+ return " ".join(line.split())
65
+
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.
68
+
69
+ Args:
70
+ line: Normalized code line
71
+ in_multiline_import: Whether we're currently inside a multi-line import
72
+
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
78
+
79
+ if in_multiline_import:
80
+ return self._handle_multiline_import_continuation(line)
81
+
82
+ if self._is_import_statement(line):
83
+ return False, True
84
+
85
+ return False, False
86
+
87
+ def _is_multiline_import_start(self, line: str) -> bool:
88
+ """Check if line starts a multi-line import statement.
89
+
90
+ Args:
91
+ line: Normalized code line
92
+
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
+
98
+ def _handle_multiline_import_continuation(self, line: str) -> tuple[bool, bool]:
99
+ """Handle a line that's part of a multi-line import.
100
+
101
+ Args:
102
+ line: Normalized code line inside a multi-line import
103
+
104
+ Returns:
105
+ Tuple of (still_in_import, should_skip)
106
+ """
107
+ closes_import = ")" in line
108
+ return not closes_import, True
109
+
110
+ def _strip_comments(self, line: str) -> str:
111
+ """Remove comments from line (Python # and // style).
112
+
113
+ Args:
114
+ line: Source code line
115
+
116
+ Returns:
117
+ Line with comments removed
118
+ """
119
+ # Python comments
120
+ if "#" in line:
121
+ line = line[: line.index("#")]
122
+
123
+ # JavaScript/TypeScript comments
124
+ if "//" in line:
125
+ line = line[: line.index("//")]
126
+
127
+ return line
128
+
129
+ def _is_import_statement(self, line: str) -> bool:
130
+ """Check if line is an import statement.
131
+
132
+ Args:
133
+ line: Normalized code line
134
+
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")
141
+
142
+ return line.startswith(import_prefixes) or line in import_tokens
143
+
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.
146
+
147
+ Args:
148
+ lines: List of normalized code lines
149
+ window_size: Number of lines per window (min_duplicate_lines)
150
+
151
+ Returns:
152
+ List of tuples: (hash_value, start_line, end_line, code_snippet)
153
+ """
154
+ if len(lines) < window_size:
155
+ return []
156
+
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)
162
+
163
+ # Line numbers are 1-indexed
164
+ start_line = i + 1
165
+ end_line = i + window_size
166
+
167
+ hashes.append((hash_val, start_line, end_line, snippet))
168
+
169
+ return hashes