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.
- src/__init__.py +1 -0
- src/cli/__init__.py +27 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +478 -0
- src/cli/linters/__init__.py +58 -0
- src/cli/linters/code_patterns.py +372 -0
- src/cli/linters/code_smells.py +343 -0
- src/cli/linters/documentation.py +155 -0
- src/cli/linters/shared.py +89 -0
- src/cli/linters/structure.py +313 -0
- src/cli/linters/structure_quality.py +316 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +375 -0
- src/cli_main.py +34 -0
- src/core/types.py +13 -0
- src/core/violation_utils.py +69 -0
- src/linter_config/ignore.py +32 -16
- src/linters/collection_pipeline/linter.py +2 -2
- src/linters/dry/block_filter.py +97 -1
- src/linters/dry/cache.py +94 -6
- src/linters/dry/config.py +47 -10
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +214 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/linter.py +89 -48
- src/linters/dry/python_analyzer.py +12 -415
- src/linters/dry/python_constant_extractor.py +101 -0
- src/linters/dry/single_statement_detector.py +415 -0
- src/linters/dry/token_hasher.py +5 -5
- src/linters/dry/typescript_analyzer.py +5 -354
- src/linters/dry/typescript_constant_extractor.py +134 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +66 -0
- src/linters/file_header/linter.py +2 -2
- src/linters/file_placement/linter.py +2 -2
- src/linters/file_placement/pattern_matcher.py +19 -5
- src/linters/magic_numbers/linter.py +8 -67
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/nesting/linter.py +12 -9
- src/linters/print_statements/linter.py +7 -24
- src/linters/srp/class_analyzer.py +9 -9
- src/linters/srp/heuristics.py +2 -2
- src/linters/srp/linter.py +2 -2
- src/linters/stateless_class/linter.py +2 -2
- src/linters/stringly_typed/__init__.py +23 -0
- src/linters/stringly_typed/config.py +165 -0
- src/linters/stringly_typed/python/__init__.py +29 -0
- src/linters/stringly_typed/python/analyzer.py +198 -0
- src/linters/stringly_typed/python/condition_extractor.py +131 -0
- src/linters/stringly_typed/python/conditional_detector.py +176 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +88 -0
- src/linters/stringly_typed/python/validation_detector.py +186 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/orchestrator/core.py +241 -12
- {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/METADATA +2 -2
- {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/RECORD +60 -28
- thailint-0.11.0.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -2141
- thailint-0.10.0.dist-info/entry_points.txt +0 -4
- {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
self._config = self._config or config
|
|
113
|
+
self._process_file(context, config)
|
|
114
|
+
return []
|
|
107
115
|
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
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
|
|
143
|
-
"""
|
|
144
|
-
if
|
|
145
|
-
|
|
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
|
-
|
|
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)
|