thailint 0.12.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.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +4 -0
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,13 +20,20 @@ Interfaces: StatelessClassRule.check(context) -> list[Violation]
|
|
|
20
20
|
|
|
21
21
|
Implementation: Composition pattern delegating analysis to specialized analyzer with
|
|
22
22
|
config loading and comprehensive ignore checking
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- B101: Type narrowing assertion after _should_analyze guard (can't fail)
|
|
26
|
+
- srp,dry: Rule class coordinates analyzer, config, and ignore checking. Method count
|
|
27
|
+
exceeds limit due to comprehensive 5-level ignore system support.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
from pathlib import Path
|
|
26
31
|
|
|
27
32
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
33
|
+
from src.core.constants import HEADER_SCAN_LINES, IgnoreDirective, Language
|
|
28
34
|
from src.core.types import Severity, Violation
|
|
29
35
|
from src.linter_config.ignore import get_ignore_parser
|
|
36
|
+
from src.linter_config.rule_matcher import rule_matches
|
|
30
37
|
|
|
31
38
|
from .config import StatelessClassConfig
|
|
32
39
|
from .python_analyzer import ClassInfo, StatelessClassAnalyzer
|
|
@@ -67,20 +74,29 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
67
74
|
return []
|
|
68
75
|
|
|
69
76
|
config = self._load_config(context)
|
|
70
|
-
if not config.enabled:
|
|
71
|
-
return []
|
|
72
|
-
|
|
73
|
-
if self._is_file_ignored(context, config):
|
|
77
|
+
if not config.enabled or self._should_skip_file(context, config):
|
|
74
78
|
return []
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
# _should_analyze ensures file_content is set
|
|
81
|
+
assert context.file_content is not None # nosec B101
|
|
78
82
|
|
|
79
83
|
analyzer = StatelessClassAnalyzer(min_methods=config.min_methods)
|
|
80
|
-
stateless_classes = analyzer.analyze(context.file_content)
|
|
84
|
+
stateless_classes = analyzer.analyze(context.file_content)
|
|
81
85
|
|
|
82
86
|
return self._filter_ignored_violations(stateless_classes, context)
|
|
83
87
|
|
|
88
|
+
def _should_skip_file(self, context: BaseLintContext, config: StatelessClassConfig) -> bool:
|
|
89
|
+
"""Check if file should be skipped due to ignore patterns or directives.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
context: Lint context
|
|
93
|
+
config: Configuration
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if file should be skipped
|
|
97
|
+
"""
|
|
98
|
+
return self._is_file_ignored(context, config) or self._has_file_level_ignore(context)
|
|
99
|
+
|
|
84
100
|
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
85
101
|
"""Check if context should be analyzed.
|
|
86
102
|
|
|
@@ -90,7 +106,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
90
106
|
Returns:
|
|
91
107
|
True if should analyze
|
|
92
108
|
"""
|
|
93
|
-
return context.language ==
|
|
109
|
+
return context.language == Language.PYTHON and context.file_content is not None
|
|
94
110
|
|
|
95
111
|
def _load_config(self, context: BaseLintContext) -> StatelessClassConfig:
|
|
96
112
|
"""Load configuration from context.
|
|
@@ -129,10 +145,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
129
145
|
return False
|
|
130
146
|
|
|
131
147
|
file_path = Path(context.file_path)
|
|
132
|
-
for pattern in config.ignore
|
|
133
|
-
if self._matches_pattern(file_path, pattern):
|
|
134
|
-
return True
|
|
135
|
-
return False
|
|
148
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
136
149
|
|
|
137
150
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
138
151
|
"""Check if file path matches a glob pattern.
|
|
@@ -162,12 +175,9 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
162
175
|
if not context.file_content:
|
|
163
176
|
return False
|
|
164
177
|
|
|
165
|
-
# Check first
|
|
166
|
-
lines = context.file_content.splitlines()[:
|
|
167
|
-
for line in lines
|
|
168
|
-
if self._is_file_ignore_directive(line):
|
|
169
|
-
return True
|
|
170
|
-
return False
|
|
178
|
+
# Check first lines for ignore-file directive
|
|
179
|
+
lines = context.file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
180
|
+
return any(self._is_file_ignore_directive(line) for line in lines)
|
|
171
181
|
|
|
172
182
|
def _is_file_ignore_directive(self, line: str) -> bool:
|
|
173
183
|
"""Check if line is a file-level ignore directive.
|
|
@@ -218,23 +228,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
218
228
|
Returns:
|
|
219
229
|
True if pattern matches this rule
|
|
220
230
|
"""
|
|
221
|
-
|
|
222
|
-
pattern_lower = rule_pattern.lower()
|
|
223
|
-
|
|
224
|
-
# Exact match
|
|
225
|
-
if rule_id_lower == pattern_lower:
|
|
226
|
-
return True
|
|
227
|
-
|
|
228
|
-
# Prefix match: stateless-class matches stateless-class.violation
|
|
229
|
-
if rule_id_lower.startswith(pattern_lower + "."):
|
|
230
|
-
return True
|
|
231
|
-
|
|
232
|
-
# Wildcard match: stateless-class.* matches stateless-class.violation
|
|
233
|
-
if pattern_lower.endswith("*"):
|
|
234
|
-
prefix = pattern_lower[:-1]
|
|
235
|
-
return rule_id_lower.startswith(prefix)
|
|
236
|
-
|
|
237
|
-
return False
|
|
231
|
+
return rule_matches(self.rule_id, rule_pattern)
|
|
238
232
|
|
|
239
233
|
def _filter_ignored_violations(
|
|
240
234
|
self, classes: list[ClassInfo], context: BaseLintContext
|
|
@@ -330,7 +324,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
330
324
|
return True
|
|
331
325
|
|
|
332
326
|
# Rule-specific ignore
|
|
333
|
-
return self._matches_rule_ignore(line,
|
|
327
|
+
return self._matches_rule_ignore(line, IgnoreDirective.IGNORE)
|
|
334
328
|
|
|
335
329
|
def _create_violation(self, info: ClassInfo, context: BaseLintContext) -> Violation:
|
|
336
330
|
"""Create violation from class info.
|
|
@@ -145,10 +145,10 @@ def _has_constructor(class_node: ast.ClassDef) -> bool:
|
|
|
145
145
|
True if class has constructor
|
|
146
146
|
"""
|
|
147
147
|
constructor_names = ("__init__", "__new__")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
return any(
|
|
149
|
+
isinstance(item, ast.FunctionDef) and item.name in constructor_names
|
|
150
|
+
for item in class_node.body
|
|
151
|
+
)
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def _is_exception_case(class_node: ast.ClassDef) -> bool:
|
|
@@ -174,10 +174,7 @@ def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
|
|
|
174
174
|
Returns:
|
|
175
175
|
True if inherits from ABC or Protocol
|
|
176
176
|
"""
|
|
177
|
-
for base in class_node.bases
|
|
178
|
-
if _get_base_name(base) in ("ABC", "Protocol"):
|
|
179
|
-
return True
|
|
180
|
-
return False
|
|
177
|
+
return any(_get_base_name(base) in ("ABC", "Protocol") for base in class_node.bases)
|
|
181
178
|
|
|
182
179
|
|
|
183
180
|
def _get_base_name(base: ast.expr) -> str:
|
|
@@ -205,10 +202,7 @@ def _has_class_attributes(class_node: ast.ClassDef) -> bool:
|
|
|
205
202
|
Returns:
|
|
206
203
|
True if class has class attributes
|
|
207
204
|
"""
|
|
208
|
-
for item in class_node.body
|
|
209
|
-
if isinstance(item, (ast.Assign, ast.AnnAssign)):
|
|
210
|
-
return True
|
|
211
|
-
return False
|
|
205
|
+
return any(isinstance(item, (ast.Assign, ast.AnnAssign)) for item in class_node.body)
|
|
212
206
|
|
|
213
207
|
|
|
214
208
|
def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
@@ -220,10 +214,10 @@ def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
|
220
214
|
Returns:
|
|
221
215
|
True if any method assigns to self
|
|
222
216
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
return any(
|
|
218
|
+
isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item)
|
|
219
|
+
for item in class_node.body
|
|
220
|
+
)
|
|
227
221
|
|
|
228
222
|
|
|
229
223
|
def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
@@ -235,10 +229,7 @@ def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
|
235
229
|
Returns:
|
|
236
230
|
True if method assigns to self
|
|
237
231
|
"""
|
|
238
|
-
for node in ast.walk(method)
|
|
239
|
-
if _is_self_attribute_assignment(node):
|
|
240
|
-
return True
|
|
241
|
-
return False
|
|
232
|
+
return any(_is_self_attribute_assignment(node) for node in ast.walk(method))
|
|
242
233
|
|
|
243
234
|
|
|
244
235
|
def _is_self_attribute_assignment(node: ast.AST) -> bool:
|
|
@@ -18,11 +18,10 @@ Exports: StringlyTypedConfig dataclass, default constants
|
|
|
18
18
|
Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
|
|
19
19
|
|
|
20
20
|
Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
|
|
21
|
-
loading from dictionary with language-specific override support
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
the established pattern in DRYConfig which has the same suppression.
|
|
21
|
+
loading from dictionary with language-specific override support
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-instance-attributes: Configuration dataclass with cohesive detection settings
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
27
|
from dataclasses import dataclass, field
|