thailint 0.10.0__py3-none-any.whl → 0.12.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 (76) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +450 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +395 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +36 -0
  46. src/linters/stringly_typed/config.py +190 -0
  47. src/linters/stringly_typed/context_filter.py +451 -0
  48. src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  49. src/linters/stringly_typed/ignore_checker.py +102 -0
  50. src/linters/stringly_typed/ignore_utils.py +51 -0
  51. src/linters/stringly_typed/linter.py +344 -0
  52. src/linters/stringly_typed/python/__init__.py +33 -0
  53. src/linters/stringly_typed/python/analyzer.py +344 -0
  54. src/linters/stringly_typed/python/call_tracker.py +172 -0
  55. src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/linters/stringly_typed/storage.py +630 -0
  63. src/linters/stringly_typed/storage_initializer.py +45 -0
  64. src/linters/stringly_typed/typescript/__init__.py +28 -0
  65. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  66. src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  67. src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  68. src/linters/stringly_typed/violation_generator.py +376 -0
  69. src/orchestrator/core.py +241 -12
  70. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
  71. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
  72. thailint-0.12.0.dist-info/entry_points.txt +4 -0
  73. src/cli.py +0 -2141
  74. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  75. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
  76. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,51 @@
1
+ """
2
+ Purpose: Shared ignore pattern matching utilities
3
+
4
+ Scope: Common ignore pattern checking for stringly-typed linter components
5
+
6
+ Overview: Provides shared utility functions for checking if file paths match ignore patterns.
7
+ Used by both the main linter and violation generator to avoid duplicating ignore pattern
8
+ matching logic. Centralizes the ignore pattern matching algorithm.
9
+
10
+ Dependencies: pathlib.Path, fnmatch
11
+
12
+ Exports: is_ignored function
13
+
14
+ Interfaces: is_ignored(file_path, ignore_patterns) -> bool
15
+
16
+ Implementation: Glob pattern matching with fnmatch for flexible ignore patterns
17
+ """
18
+
19
+ import fnmatch
20
+ from pathlib import Path
21
+
22
+
23
+ def is_ignored(file_path: str | Path, ignore_patterns: list[str]) -> bool:
24
+ """Check if file path matches any ignore pattern.
25
+
26
+ Supports glob patterns like:
27
+ - **/tests/** - matches any file in tests directories
28
+ - **/*_test.py - matches any file ending in _test.py
29
+ - tests/ - simple substring match
30
+
31
+ Args:
32
+ file_path: Path to check (string or Path object)
33
+ ignore_patterns: List of patterns to match against
34
+
35
+ Returns:
36
+ True if file should be ignored
37
+ """
38
+ if not ignore_patterns:
39
+ return False
40
+
41
+ path_str = str(file_path)
42
+
43
+ for pattern in ignore_patterns:
44
+ # Use fnmatch for glob-style patterns
45
+ if fnmatch.fnmatch(path_str, pattern):
46
+ return True
47
+ # Also check if pattern appears as substring (for simple patterns)
48
+ if pattern in path_str:
49
+ return True
50
+
51
+ return False
@@ -0,0 +1,344 @@
1
+ """
2
+ Purpose: Main stringly-typed linter rule with cross-file detection
3
+
4
+ Scope: StringlyTypedRule implementing MultiLanguageLintRule for cross-file pattern detection
5
+
6
+ Overview: Implements stringly-typed linter rule following MultiLanguageLintRule interface with
7
+ cross-file detection using SQLite storage. Orchestrates pattern detection by delegating to
8
+ language-specific analyzers (Python, TypeScript). During check() phase, patterns are collected
9
+ into storage. During finalize() phase, storage is queried for patterns appearing across
10
+ multiple files and violations are generated. Maintains minimal orchestration logic to comply
11
+ with SRP.
12
+
13
+ Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringlyTypedAnalyzer,
14
+ StringlyTypedStorage, StorageInitializer, ViolationGenerator, StringlyTypedConfig
15
+
16
+ Exports: StringlyTypedRule class
17
+
18
+ Interfaces: StringlyTypedRule.check(context) -> list[Violation],
19
+ StringlyTypedRule.finalize() -> list[Violation]
20
+
21
+ Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
22
+ Delegates all logic to helper classes, maintains only orchestration and state.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
31
+ from src.core.linter_utils import load_linter_config
32
+ from src.core.types import Violation
33
+
34
+ from .config import StringlyTypedConfig
35
+ from .ignore_utils import is_ignored
36
+ from .python.analyzer import (
37
+ AnalysisResult,
38
+ ComparisonResult,
39
+ FunctionCallResult,
40
+ PythonStringlyTypedAnalyzer,
41
+ )
42
+ from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
43
+ from .storage_initializer import StorageInitializer
44
+ from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
45
+ from .violation_generator import ViolationGenerator
46
+
47
+
48
+ def compute_string_set_hash(values: set[str]) -> int:
49
+ """Compute consistent hash for a set of strings.
50
+
51
+ Args:
52
+ values: Set of string values to hash
53
+
54
+ Returns:
55
+ Hash value based on sorted, lowercased strings
56
+ """
57
+ return hash(tuple(sorted(s.lower() for s in values)))
58
+
59
+
60
+ def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
61
+ """Check if context and storage are ready for analysis."""
62
+ return bool(context.file_path and context.file_content and storage)
63
+
64
+
65
+ def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
66
+ """Convert AnalysisResult to StoredPattern.
67
+
68
+ Args:
69
+ result: Analysis result from language analyzer
70
+
71
+ Returns:
72
+ StoredPattern for storage
73
+ """
74
+ return StoredPattern(
75
+ file_path=result.file_path,
76
+ line_number=result.line_number,
77
+ column=result.column,
78
+ variable_name=result.variable_name,
79
+ string_set_hash=compute_string_set_hash(result.string_values),
80
+ string_values=sorted(result.string_values),
81
+ pattern_type=result.pattern_type,
82
+ details=result.details,
83
+ )
84
+
85
+
86
+ def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
87
+ """Convert FunctionCallResult to StoredFunctionCall.
88
+
89
+ Args:
90
+ result: Function call result from language analyzer
91
+
92
+ Returns:
93
+ StoredFunctionCall for storage
94
+ """
95
+ return StoredFunctionCall(
96
+ file_path=result.file_path,
97
+ line_number=result.line_number,
98
+ column=result.column,
99
+ function_name=result.function_name,
100
+ param_index=result.param_index,
101
+ string_value=result.string_value,
102
+ )
103
+
104
+
105
+ def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
106
+ """Convert ComparisonResult to StoredComparison.
107
+
108
+ Args:
109
+ result: Comparison result from language analyzer
110
+
111
+ Returns:
112
+ StoredComparison for storage
113
+ """
114
+ return StoredComparison(
115
+ file_path=result.file_path,
116
+ line_number=result.line_number,
117
+ column=result.column,
118
+ variable_name=result.variable_name,
119
+ compared_value=result.compared_value,
120
+ operator=result.operator,
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class StringlyTypedComponents:
126
+ """Component dependencies for stringly-typed linter."""
127
+
128
+ storage_initializer: StorageInitializer
129
+ violation_generator: ViolationGenerator
130
+ python_analyzer: PythonStringlyTypedAnalyzer
131
+ typescript_analyzer: TypeScriptStringlyTypedAnalyzer
132
+
133
+
134
+ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
135
+ """Detects stringly-typed patterns across project files.
136
+
137
+ Uses two-phase pattern:
138
+ 1. check() - Collects patterns into SQLite storage (returns empty list)
139
+ 2. finalize() - Queries storage and generates violations for cross-file patterns
140
+ """
141
+
142
+ def __init__(self) -> None:
143
+ """Initialize the stringly-typed rule with helper components."""
144
+ self._storage: StringlyTypedStorage | None = None
145
+ self._initialized = False
146
+ self._config: StringlyTypedConfig | None = None
147
+
148
+ # Helper components grouped to reduce instance attributes
149
+ self._helpers = StringlyTypedComponents(
150
+ storage_initializer=StorageInitializer(),
151
+ violation_generator=ViolationGenerator(),
152
+ python_analyzer=PythonStringlyTypedAnalyzer(),
153
+ typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
154
+ )
155
+
156
+ @property
157
+ def rule_id(self) -> str:
158
+ """Unique identifier for this rule."""
159
+ return "stringly-typed.repeated-validation"
160
+
161
+ @property
162
+ def rule_name(self) -> str:
163
+ """Human-readable name for this rule."""
164
+ return "Stringly-Typed Pattern"
165
+
166
+ @property
167
+ def description(self) -> str:
168
+ """Description of what this rule checks."""
169
+ return "Detects stringly-typed code patterns that should use enums"
170
+
171
+ def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
172
+ """Load configuration from context.
173
+
174
+ Args:
175
+ context: Lint context with metadata
176
+
177
+ Returns:
178
+ StringlyTypedConfig instance
179
+ """
180
+ return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
181
+
182
+ def _check_python(
183
+ self, context: BaseLintContext, config: StringlyTypedConfig
184
+ ) -> list[Violation]:
185
+ """Analyze Python code and store patterns.
186
+
187
+ Args:
188
+ context: Lint context with file content
189
+ config: Stringly-typed configuration
190
+
191
+ Returns:
192
+ Empty list (violations generated in finalize)
193
+ """
194
+ self._ensure_storage_initialized(context, config)
195
+ self._analyze_python_file(context, config)
196
+ return []
197
+
198
+ def _check_typescript(
199
+ self, context: BaseLintContext, config: StringlyTypedConfig
200
+ ) -> list[Violation]:
201
+ """Analyze TypeScript code and store patterns.
202
+
203
+ Args:
204
+ context: Lint context with file content
205
+ config: Stringly-typed configuration
206
+
207
+ Returns:
208
+ Empty list (violations generated in finalize)
209
+ """
210
+ self._ensure_storage_initialized(context, config)
211
+ self._analyze_typescript_file(context, config)
212
+ return []
213
+
214
+ def _analyze_typescript_file(
215
+ self, context: BaseLintContext, config: StringlyTypedConfig
216
+ ) -> None:
217
+ """Analyze TypeScript file and store patterns.
218
+
219
+ Uses single-parse optimization to avoid duplicate parsing overhead.
220
+
221
+ Args:
222
+ context: Lint context with file content
223
+ config: Stringly-typed configuration
224
+ """
225
+ if not self._should_analyze(context, config):
226
+ return
227
+
228
+ file_path = Path(context.file_path) # type: ignore[arg-type]
229
+ file_content = context.file_content or ""
230
+ self._helpers.typescript_analyzer.config = config
231
+
232
+ # Single-parse optimization: parse once, run both trackers
233
+ call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
234
+ file_content, file_path
235
+ )
236
+
237
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
238
+ self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
239
+
240
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
241
+ self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
242
+
243
+ def _ensure_storage_initialized(
244
+ self, context: BaseLintContext, config: StringlyTypedConfig
245
+ ) -> None:
246
+ """Initialize storage and analyzers on first call.
247
+
248
+ Args:
249
+ context: Lint context
250
+ config: Stringly-typed configuration
251
+ """
252
+ if not self._initialized:
253
+ self._storage = self._helpers.storage_initializer.initialize(context, config)
254
+ self._config = config
255
+ self._initialized = True
256
+
257
+ def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
258
+ """Analyze Python file and store patterns.
259
+
260
+ Args:
261
+ context: Lint context with file content
262
+ config: Stringly-typed configuration
263
+ """
264
+ if not self._should_analyze(context, config):
265
+ return
266
+
267
+ file_path = Path(context.file_path) # type: ignore[arg-type]
268
+ file_content = context.file_content or ""
269
+ self._helpers.python_analyzer.config = config
270
+
271
+ self._store_validation_patterns(file_content, file_path)
272
+ self._store_function_calls(file_content, file_path)
273
+ self._store_comparisons(file_content, file_path)
274
+
275
+ def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
276
+ """Check if file should be analyzed.
277
+
278
+ Args:
279
+ context: Lint context
280
+ config: Configuration
281
+
282
+ Returns:
283
+ True if file should be analyzed
284
+ """
285
+ if not _is_ready_for_analysis(context, self._storage):
286
+ return False
287
+ file_path = Path(context.file_path) # type: ignore[arg-type]
288
+ return not is_ignored(file_path, config.ignore)
289
+
290
+ def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
291
+ """Analyze and store validation patterns.
292
+
293
+ Args:
294
+ file_content: Python source code
295
+ file_path: Path to file
296
+ """
297
+ results = self._helpers.python_analyzer.analyze(file_content, file_path)
298
+ self._storage.add_patterns([_convert_to_stored_pattern(r) for r in results]) # type: ignore[union-attr]
299
+
300
+ def _store_function_calls(self, file_content: str, file_path: Path) -> None:
301
+ """Analyze and store function call patterns.
302
+
303
+ Args:
304
+ file_content: Python source code
305
+ file_path: Path to file
306
+ """
307
+ call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
308
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
309
+ self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
310
+
311
+ def _store_comparisons(self, file_content: str, file_path: Path) -> None:
312
+ """Analyze and store Python comparison patterns.
313
+
314
+ Args:
315
+ file_content: Python source code
316
+ file_path: Path to file
317
+ """
318
+ comparison_results = self._helpers.python_analyzer.analyze_comparisons(
319
+ file_content, file_path
320
+ )
321
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
322
+ self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
323
+
324
+ def finalize(self) -> list[Violation]:
325
+ """Generate violations after all files processed.
326
+
327
+ Returns:
328
+ List of violations for patterns appearing in multiple files
329
+ """
330
+ if not self._storage or not self._config:
331
+ return []
332
+
333
+ # Generate violations from cross-file patterns
334
+ violations = self._helpers.violation_generator.generate_violations(
335
+ self._storage, self.rule_id, self._config
336
+ )
337
+
338
+ # Cleanup and reset state for next run
339
+ self._storage.close()
340
+ self._storage = None
341
+ self._config = None
342
+ self._initialized = False
343
+
344
+ return violations
@@ -0,0 +1,33 @@
1
+ """
2
+ Purpose: Python-specific detection for stringly-typed patterns
3
+
4
+ Scope: Python AST analysis for membership validation, equality chains, and function calls
5
+
6
+ Overview: Exposes Python analysis components for detecting stringly-typed patterns in Python
7
+ source code. Includes validation_detector for finding 'x in ("a", "b")' patterns,
8
+ conditional_detector for finding if/elif chains and match statements, call_tracker for
9
+ finding function calls with string arguments, and analyzer for coordinating detection
10
+ across Python files. Uses AST traversal to identify where plain strings are used instead
11
+ of proper enums or typed alternatives.
12
+
13
+ Dependencies: ast module for Python AST parsing
14
+
15
+ Exports: MembershipValidationDetector, ConditionalPatternDetector, FunctionCallTracker,
16
+ PythonStringlyTypedAnalyzer
17
+
18
+ Interfaces: Detector and analyzer classes for Python stringly-typed pattern detection
19
+
20
+ Implementation: AST NodeVisitor pattern for traversing Python syntax trees
21
+ """
22
+
23
+ from .analyzer import PythonStringlyTypedAnalyzer
24
+ from .call_tracker import FunctionCallTracker
25
+ from .conditional_detector import ConditionalPatternDetector
26
+ from .validation_detector import MembershipValidationDetector
27
+
28
+ __all__ = [
29
+ "ConditionalPatternDetector",
30
+ "FunctionCallTracker",
31
+ "MembershipValidationDetector",
32
+ "PythonStringlyTypedAnalyzer",
33
+ ]