thailint 0.10.0__py3-none-any.whl → 0.11.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 (62) 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 +343 -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 +375 -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 +23 -0
  46. src/linters/stringly_typed/config.py +165 -0
  47. src/linters/stringly_typed/python/__init__.py +29 -0
  48. src/linters/stringly_typed/python/analyzer.py +198 -0
  49. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  50. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  51. src/linters/stringly_typed/python/constants.py +21 -0
  52. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  53. src/linters/stringly_typed/python/validation_detector.py +186 -0
  54. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  55. src/orchestrator/core.py +241 -12
  56. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/METADATA +2 -2
  57. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/RECORD +60 -28
  58. thailint-0.11.0.dist-info/entry_points.txt +4 -0
  59. src/cli.py +0 -2141
  60. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  61. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
  62. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,98 @@
1
+ """
2
+ Purpose: Build violation messages for duplicate constants
3
+
4
+ Scope: Violation message formatting for constant duplication detection
5
+
6
+ Overview: Formats detailed violation messages for duplicate constant detection. Creates messages
7
+ that include the constant name(s), all file locations with line numbers, and the values
8
+ assigned at each location. Distinguishes between exact matches (same constant name) and
9
+ fuzzy matches (similar names like API_TIMEOUT and TIMEOUT_API). Provides actionable guidance
10
+ to consolidate constants into a shared module.
11
+
12
+ Dependencies: ConstantGroup from constant module, Violation from core.types
13
+
14
+ Exports: ConstantViolationBuilder class
15
+
16
+ Interfaces: ConstantViolationBuilder.build_violations(groups, rule_id) -> list[Violation]
17
+
18
+ Implementation: Message template formatting with location enumeration and fuzzy match indication
19
+ """
20
+
21
+ from src.core.types import Severity, Violation
22
+
23
+ from .constant import ConstantGroup, ConstantLocation
24
+
25
+ # Maximum other locations to show in violation message
26
+ MAX_DISPLAYED_LOCATIONS = 3
27
+
28
+
29
+ class ConstantViolationBuilder:
30
+ """Builds violation messages for duplicate constants."""
31
+
32
+ def __init__(self, min_occurrences: int = 2) -> None:
33
+ """Initialize with minimum occurrence threshold."""
34
+ self.min_occurrences = min_occurrences
35
+
36
+ def build_violations(self, groups: list[ConstantGroup], rule_id: str) -> list[Violation]:
37
+ """Build violations from constant groups."""
38
+ violations = []
39
+ for group in groups:
40
+ if group.file_count >= self.min_occurrences:
41
+ violations.extend(self._violations_for_group(group, rule_id))
42
+ return violations
43
+
44
+ def _violations_for_group(self, group: ConstantGroup, rule_id: str) -> list[Violation]:
45
+ """Create violations for all locations in a group."""
46
+ return [
47
+ Violation(
48
+ rule_id=rule_id,
49
+ file_path=str(loc.file_path),
50
+ line=loc.line_number,
51
+ column=1,
52
+ message=self._format_message(group, loc),
53
+ severity=Severity.ERROR,
54
+ )
55
+ for loc in group.locations
56
+ ]
57
+
58
+ def _format_message(self, group: ConstantGroup, current: ConstantLocation) -> str:
59
+ """Format the violation message based on match type."""
60
+ others = _get_other_locations(group, current)
61
+ locations_text = _format_locations_text(others)
62
+ if group.is_fuzzy_match:
63
+ names_str = " ≈ ".join(f"'{n}'" for n in sorted(group.all_names))
64
+ return (
65
+ f"Similar constants found: {names_str} in {group.file_count} files. "
66
+ f"{locations_text} "
67
+ f"These appear to represent the same concept - consider standardizing the name."
68
+ )
69
+ return (
70
+ f"Duplicate constant '{group.canonical_name}' defined in {group.file_count} files. "
71
+ f"{locations_text} "
72
+ f"Consider consolidating to a shared constants module."
73
+ )
74
+
75
+
76
+ def _get_other_locations(group: ConstantGroup, current: ConstantLocation) -> list[ConstantLocation]:
77
+ """Get locations excluding current (module-level helper)."""
78
+ return [
79
+ loc
80
+ for loc in group.locations
81
+ if loc.file_path != current.file_path or loc.line_number != current.line_number
82
+ ]
83
+
84
+
85
+ def _format_locations_text(others: list[ConstantLocation]) -> str:
86
+ """Format other locations as text (module-level helper)."""
87
+ if not others:
88
+ return ""
89
+ parts = [_format_single_location(loc) for loc in others[:MAX_DISPLAYED_LOCATIONS]]
90
+ result = "Also found in: " + ", ".join(parts)
91
+ extra = len(others) - MAX_DISPLAYED_LOCATIONS
92
+ return result + (f" and {extra} more." if extra > 0 else ".")
93
+
94
+
95
+ def _format_single_location(loc: ConstantLocation) -> str:
96
+ """Format a single location for display (module-level helper)."""
97
+ value_str = f" = {loc.value}" if loc.value else ""
98
+ return f"{loc.file_path.name}:{loc.line_number} ({loc.name}{value_str})"
src/linters/dry/linter.py CHANGED
@@ -1,15 +1,18 @@
1
1
  """
2
2
  Purpose: Main DRY linter rule implementation with stateful caching
3
3
 
4
- Scope: DRYRule class implementing BaseLintRule interface for duplicate code detection
4
+ Scope: DRYRule class implementing BaseLintRule interface for duplicate code and constant detection
5
5
 
6
6
  Overview: Implements DRY linter rule following BaseLintRule interface with stateful caching design.
7
7
  Orchestrates duplicate detection by delegating to specialized classes: ConfigLoader for config,
8
8
  StorageInitializer for storage setup, FileAnalyzer for file analysis, and ViolationGenerator
9
- for violation creation. Maintains minimal orchestration logic to comply with SRP (8 methods total).
9
+ for violation creation. Also supports duplicate constant detection (opt-in) to identify when
10
+ the same constant is defined in multiple files. Maintains minimal orchestration logic to comply
11
+ with SRP.
10
12
 
11
13
  Dependencies: BaseLintRule, BaseLintContext, ConfigLoader, StorageInitializer, FileAnalyzer,
12
- DuplicateStorage, ViolationGenerator
14
+ DuplicateStorage, ViolationGenerator, PythonConstantExtractor, TypeScriptConstantExtractor,
15
+ ConstantMatcher, ConstantViolationBuilder
13
16
 
14
17
  Exports: DRYRule class
15
18
 
@@ -22,7 +25,6 @@ from __future__ import annotations
22
25
 
23
26
  from dataclasses import dataclass
24
27
  from pathlib import Path
25
- from typing import TYPE_CHECKING
26
28
 
27
29
  from src.core.base import BaseLintContext, BaseLintRule
28
30
  from src.core.linter_utils import should_process_file
@@ -30,18 +32,20 @@ from src.core.types import Violation
30
32
 
31
33
  from .config import DRYConfig
32
34
  from .config_loader import ConfigLoader
35
+ from .constant import ConstantInfo
36
+ from .constant_matcher import ConstantMatcher
37
+ from .constant_violation_builder import ConstantViolationBuilder
33
38
  from .duplicate_storage import DuplicateStorage
34
39
  from .file_analyzer import FileAnalyzer
35
40
  from .inline_ignore import InlineIgnoreParser
41
+ from .python_constant_extractor import PythonConstantExtractor
36
42
  from .storage_initializer import StorageInitializer
43
+ from .typescript_constant_extractor import TypeScriptConstantExtractor
37
44
  from .violation_generator import ViolationGenerator
38
45
 
39
- if TYPE_CHECKING:
40
- from .cache import CodeBlock
41
-
42
46
 
43
47
  @dataclass
44
- class DRYComponents:
48
+ class DRYComponents: # pylint: disable=too-many-instance-attributes
45
49
  """Component dependencies for DRY linter."""
46
50
 
47
51
  config_loader: ConfigLoader
@@ -49,6 +53,10 @@ class DRYComponents:
49
53
  file_analyzer: FileAnalyzer
50
54
  violation_generator: ViolationGenerator
51
55
  inline_ignore: InlineIgnoreParser
56
+ python_extractor: PythonConstantExtractor
57
+ typescript_extractor: TypeScriptConstantExtractor
58
+ constant_matcher: ConstantMatcher
59
+ constant_violation_builder: ConstantViolationBuilder
52
60
 
53
61
 
54
62
  class DRYRule(BaseLintRule):
@@ -61,6 +69,9 @@ class DRYRule(BaseLintRule):
61
69
  self._config: DRYConfig | None = None
62
70
  self._file_analyzer: FileAnalyzer | None = None
63
71
 
72
+ # Collected constants for cross-file detection: list of (file_path, ConstantInfo)
73
+ self._constants: list[tuple[Path, ConstantInfo]] = []
74
+
64
75
  # Helper components grouped to reduce instance attributes
65
76
  self._helpers = DRYComponents(
66
77
  config_loader=ConfigLoader(),
@@ -68,6 +79,10 @@ class DRYRule(BaseLintRule):
68
79
  file_analyzer=FileAnalyzer(), # Placeholder, will be replaced with configured one
69
80
  violation_generator=ViolationGenerator(),
70
81
  inline_ignore=InlineIgnoreParser(),
82
+ python_extractor=PythonConstantExtractor(),
83
+ typescript_extractor=TypeScriptConstantExtractor(),
84
+ constant_matcher=ConstantMatcher(),
85
+ constant_violation_builder=ConstantViolationBuilder(),
71
86
  )
72
87
 
73
88
  @property
@@ -86,14 +101,7 @@ class DRYRule(BaseLintRule):
86
101
  return "Detects duplicate code blocks across the project"
87
102
 
88
103
  def check(self, context: BaseLintContext) -> list[Violation]:
89
- """Analyze file and store blocks (collection phase).
90
-
91
- Args:
92
- context: Lint context with file information
93
-
94
- Returns:
95
- Empty list (violations returned in finalize())
96
- """
104
+ """Analyze file and store blocks (collection phase)."""
97
105
  if not should_process_file(context):
98
106
  return []
99
107
 
@@ -101,18 +109,18 @@ class DRYRule(BaseLintRule):
101
109
  if not config.enabled:
102
110
  return []
103
111
 
104
- # Store config for finalize()
105
- if self._config is None:
106
- self._config = config
112
+ self._config = self._config or config
113
+ self._process_file(context, config)
114
+ return []
107
115
 
108
- # Parse inline ignore directives from this file
116
+ def _process_file(self, context: BaseLintContext, config: DRYConfig) -> None:
117
+ """Process a single file for duplicates and constants."""
109
118
  file_path = Path(context.file_path) # type: ignore[arg-type]
110
119
  self._helpers.inline_ignore.parse_file(file_path, context.file_content or "")
111
-
112
120
  self._ensure_storage_initialized(context, config)
113
121
  self._analyze_and_store(context, config)
114
-
115
- return []
122
+ if config.detect_duplicate_constants:
123
+ self._extract_and_store_constants(context)
116
124
 
117
125
  def _ensure_storage_initialized(self, context: BaseLintContext, config: DRYConfig) -> None:
118
126
  """Initialize storage and file analyzer on first call."""
@@ -124,40 +132,73 @@ class DRYRule(BaseLintRule):
124
132
 
125
133
  def _analyze_and_store(self, context: BaseLintContext, config: DRYConfig) -> None:
126
134
  """Analyze file and store blocks."""
127
- # Guaranteed by _should_process_file check
128
- if context.file_path is None or context.file_content is None:
129
- return # Should never happen due to should_process_file check
130
-
131
- if not self._file_analyzer:
132
- return # Should never happen after initialization
133
-
134
- file_path = Path(context.file_path)
135
- blocks = self._file_analyzer.analyze(
136
- file_path, context.file_content, context.language, config
135
+ if not self._can_analyze(context):
136
+ return
137
+ file_path = Path(context.file_path) # type: ignore[arg-type]
138
+ blocks = self._file_analyzer.analyze( # type: ignore[union-attr]
139
+ file_path,
140
+ context.file_content, # type: ignore[arg-type]
141
+ context.language,
142
+ config,
137
143
  )
138
-
139
144
  if blocks:
140
- self._store_blocks(file_path, blocks)
145
+ self._storage.add_blocks(file_path, blocks) # type: ignore[union-attr]
146
+
147
+ def _can_analyze(self, context: BaseLintContext) -> bool:
148
+ """Check if context is ready for analysis."""
149
+ return (
150
+ context.file_path is not None
151
+ and context.file_content is not None
152
+ and self._file_analyzer is not None
153
+ and self._storage is not None
154
+ )
141
155
 
142
- def _store_blocks(self, file_path: Path, blocks: list[CodeBlock]) -> None:
143
- """Store blocks in SQLite if storage available."""
144
- if self._storage:
145
- self._storage.add_blocks(file_path, blocks)
156
+ def _extract_and_store_constants(self, context: BaseLintContext) -> None:
157
+ """Extract constants from file and store for cross-file detection."""
158
+ if context.file_path is None or context.file_content is None:
159
+ return
160
+ file_path = Path(context.file_path)
161
+ extractor = _get_extractor_for_language(context.language, self._helpers)
162
+ if extractor:
163
+ self._constants.extend((file_path, c) for c in extractor.extract(context.file_content))
146
164
 
147
165
  def finalize(self) -> list[Violation]:
148
- """Generate violations after all files processed.
149
-
150
- Returns:
151
- List of all violations found across all files
152
- """
166
+ """Generate violations after all files processed."""
153
167
  if not self._storage or not self._config:
154
168
  return []
155
-
156
169
  violations = self._helpers.violation_generator.generate_violations(
157
170
  self._storage, self.rule_id, self._config, self._helpers.inline_ignore
158
171
  )
159
-
160
- # Clear inline ignore cache for next run
172
+ if self._config.detect_duplicate_constants and self._constants:
173
+ violations.extend(
174
+ _generate_constant_violations(
175
+ self._constants, self._config, self._helpers, self.rule_id
176
+ )
177
+ )
161
178
  self._helpers.inline_ignore.clear()
162
-
179
+ self._constants = []
163
180
  return violations
181
+
182
+
183
+ def _get_extractor_for_language(
184
+ language: str | None, helpers: DRYComponents
185
+ ) -> PythonConstantExtractor | TypeScriptConstantExtractor | None:
186
+ """Get the appropriate constant extractor for a language."""
187
+ extractors: dict[str, PythonConstantExtractor | TypeScriptConstantExtractor] = {
188
+ "python": helpers.python_extractor,
189
+ "typescript": helpers.typescript_extractor,
190
+ "javascript": helpers.typescript_extractor,
191
+ }
192
+ return extractors.get(language or "")
193
+
194
+
195
+ def _generate_constant_violations(
196
+ constants: list[tuple[Path, ConstantInfo]],
197
+ config: DRYConfig,
198
+ helpers: DRYComponents,
199
+ rule_id: str,
200
+ ) -> list[Violation]:
201
+ """Generate violations for duplicate constants."""
202
+ groups = helpers.constant_matcher.find_groups(constants)
203
+ helpers.constant_violation_builder.min_occurrences = config.min_constant_occurrences
204
+ return helpers.constant_violation_builder.build_violations(groups, rule_id)