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.
- src/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
src/core/linter_utils.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared utility functions for linter framework patterns
|
|
3
|
+
|
|
4
|
+
Scope: Common config loading, metadata access, and context validation utilities for all linters
|
|
5
|
+
|
|
6
|
+
Overview: Provides reusable helper functions to eliminate duplication across linter implementations.
|
|
7
|
+
Includes utilities for loading configuration from context metadata with language-specific overrides,
|
|
8
|
+
extracting metadata fields safely with type validation, and validating context state. Standardizes
|
|
9
|
+
common patterns used by srp, nesting, dry, and file_placement linters. Reduces boilerplate code
|
|
10
|
+
while maintaining type safety and proper error handling.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintContext from src.core.base
|
|
13
|
+
|
|
14
|
+
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
|
|
15
|
+
|
|
16
|
+
Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
|
|
17
|
+
|
|
18
|
+
Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Protocol, TypeVar
|
|
22
|
+
|
|
23
|
+
from src.core.base import BaseLintContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Protocol for config classes that support from_dict
|
|
27
|
+
class ConfigProtocol(Protocol):
|
|
28
|
+
"""Protocol for configuration classes with from_dict class method."""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(
|
|
32
|
+
cls, config_dict: dict[str, Any], language: str | None = None
|
|
33
|
+
) -> "ConfigProtocol":
|
|
34
|
+
"""Create config instance from dictionary."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Type variable for config classes
|
|
38
|
+
ConfigType = TypeVar("ConfigType", bound=ConfigProtocol) # pylint: disable=invalid-name
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_metadata(context: BaseLintContext) -> dict[str, Any]:
|
|
42
|
+
"""Get metadata dictionary from context with safe fallback.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
context: Lint context containing optional metadata
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Metadata dictionary, or empty dict if not available
|
|
49
|
+
"""
|
|
50
|
+
metadata = getattr(context, "metadata", None)
|
|
51
|
+
if metadata is None or not isinstance(metadata, dict):
|
|
52
|
+
return {}
|
|
53
|
+
return dict(metadata) # Explicit cast to satisfy type checker
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_metadata_value(context: BaseLintContext, key: str, default: Any = None) -> Any:
|
|
57
|
+
"""Get specific value from context metadata with safe fallback.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
context: Lint context containing optional metadata
|
|
61
|
+
key: Metadata key to retrieve
|
|
62
|
+
default: Default value if key not found
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Metadata value or default
|
|
66
|
+
"""
|
|
67
|
+
metadata = get_metadata(context)
|
|
68
|
+
return metadata.get(key, default)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_language(context: BaseLintContext) -> str | None:
|
|
72
|
+
"""Get language from context.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
context: Lint context containing optional language
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Language string or None
|
|
79
|
+
"""
|
|
80
|
+
return getattr(context, "language", None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_project_root(context: BaseLintContext) -> str | None:
|
|
84
|
+
"""Get project root from context metadata.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
context: Lint context containing optional metadata
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Project root path or None
|
|
91
|
+
"""
|
|
92
|
+
metadata = get_metadata(context)
|
|
93
|
+
project_root = metadata.get("project_root")
|
|
94
|
+
return str(project_root) if project_root is not None else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_linter_config(
|
|
98
|
+
context: BaseLintContext,
|
|
99
|
+
config_key: str,
|
|
100
|
+
config_class: type[ConfigType],
|
|
101
|
+
) -> ConfigType:
|
|
102
|
+
"""Load linter configuration from context metadata with language-specific overrides.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
context: Lint context containing metadata
|
|
106
|
+
config_key: Key to look up in metadata (e.g., "srp", "nesting", "dry")
|
|
107
|
+
config_class: Configuration class with from_dict() class method
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Configuration instance (uses default config if metadata unavailable)
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
config = load_linter_config(context, "srp", SRPConfig)
|
|
114
|
+
"""
|
|
115
|
+
metadata = get_metadata(context)
|
|
116
|
+
config_dict = metadata.get(config_key, {})
|
|
117
|
+
|
|
118
|
+
if not isinstance(config_dict, dict):
|
|
119
|
+
return config_class()
|
|
120
|
+
|
|
121
|
+
# Get language for language-specific thresholds
|
|
122
|
+
language = get_language(context)
|
|
123
|
+
|
|
124
|
+
# Call from_dict with language if config class supports it
|
|
125
|
+
# This works for SRPConfig, NestingConfig, etc.
|
|
126
|
+
try:
|
|
127
|
+
result = config_class.from_dict(config_dict, language=language)
|
|
128
|
+
return result # type: ignore[return-value]
|
|
129
|
+
except TypeError:
|
|
130
|
+
# Fallback for config classes that don't support language parameter
|
|
131
|
+
result_fallback = config_class.from_dict(config_dict)
|
|
132
|
+
return result_fallback # type: ignore[return-value]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def has_file_content(context: BaseLintContext) -> bool:
|
|
136
|
+
"""Check if context has file content available.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
context: Lint context to check
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if file_content is not None
|
|
143
|
+
"""
|
|
144
|
+
return context.file_content is not None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def has_file_path(context: BaseLintContext) -> bool:
|
|
148
|
+
"""Check if context has file path available.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
context: Lint context to check
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if file_path is not None
|
|
155
|
+
"""
|
|
156
|
+
return context.file_path is not None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def should_process_file(context: BaseLintContext) -> bool:
|
|
160
|
+
"""Check if file should be processed (has both content and path).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
context: Lint context to check
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if file has both content and path available
|
|
167
|
+
"""
|
|
168
|
+
return has_file_content(context) and has_file_path(context)
|
src/core/registry.py
CHANGED
|
@@ -3,36 +3,24 @@ Purpose: Rule registry with automatic plugin discovery and registration
|
|
|
3
3
|
|
|
4
4
|
Scope: Dynamic rule management and discovery across all linter plugin packages
|
|
5
5
|
|
|
6
|
-
Overview: Implements
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
rules simply by creating a class in the appropriate package structure without modifying any
|
|
14
|
-
framework code. The registry handles import errors gracefully and supports both package-level
|
|
15
|
-
and module-level discovery patterns.
|
|
16
|
-
|
|
17
|
-
Dependencies: importlib for dynamic module loading, inspect for class introspection,
|
|
18
|
-
pkgutil for package traversal, BaseLintRule for type checking
|
|
6
|
+
Overview: Implements rule registry that maintains a collection of registered linting rules indexed
|
|
7
|
+
by rule_id. Provides methods to register individual rules, retrieve rules by identifier, list
|
|
8
|
+
all available rules, and discover rules from packages using the RuleDiscovery helper. Enables
|
|
9
|
+
the extensible plugin architecture by allowing rules to be added dynamically without framework
|
|
10
|
+
modifications. Validates rule uniqueness and handles registration errors gracefully.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintRule, RuleDiscovery
|
|
19
13
|
|
|
20
14
|
Exports: RuleRegistry class with register(), get(), list_all(), and discover_rules() methods
|
|
21
15
|
|
|
22
16
|
Interfaces: register(rule: BaseLintRule) -> None, get(rule_id: str) -> BaseLintRule | None,
|
|
23
17
|
list_all() -> list[BaseLintRule], discover_rules(package_path: str) -> int
|
|
24
18
|
|
|
25
|
-
Implementation:
|
|
26
|
-
subclass detection for BaseLintRule, abstract class filtering, graceful error handling for
|
|
27
|
-
failed imports, duplicate rule_id validation
|
|
19
|
+
Implementation: Dictionary-based registry with RuleDiscovery delegation, duplicate validation
|
|
28
20
|
"""
|
|
29
21
|
|
|
30
|
-
import importlib
|
|
31
|
-
import inspect
|
|
32
|
-
import pkgutil
|
|
33
|
-
from typing import Any
|
|
34
|
-
|
|
35
22
|
from .base import BaseLintRule
|
|
23
|
+
from .rule_discovery import RuleDiscovery
|
|
36
24
|
|
|
37
25
|
|
|
38
26
|
class RuleRegistry:
|
|
@@ -45,6 +33,7 @@ class RuleRegistry:
|
|
|
45
33
|
def __init__(self) -> None:
|
|
46
34
|
"""Initialize empty registry."""
|
|
47
35
|
self._rules: dict[str, BaseLintRule] = {}
|
|
36
|
+
self._discovery = RuleDiscovery()
|
|
48
37
|
|
|
49
38
|
def register(self, rule: BaseLintRule) -> None:
|
|
50
39
|
"""Register a new rule.
|
|
@@ -93,78 +82,14 @@ class RuleRegistry:
|
|
|
93
82
|
Returns:
|
|
94
83
|
Number of rules discovered and registered.
|
|
95
84
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
except ImportError:
|
|
99
|
-
return 0
|
|
100
|
-
|
|
101
|
-
if not hasattr(package, "__path__"):
|
|
102
|
-
return self._discover_from_module(package_path)
|
|
103
|
-
|
|
104
|
-
return self._discover_from_package_modules(package_path, package)
|
|
105
|
-
|
|
106
|
-
def _discover_from_package_modules(self, package_path: str, package: Any) -> int:
|
|
107
|
-
"""Discover rules from all modules in a package."""
|
|
108
|
-
discovered_count = 0
|
|
109
|
-
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
110
|
-
full_module_name = f"{package_path}.{module_name}"
|
|
111
|
-
discovered_count += self._try_discover_from_module(full_module_name)
|
|
112
|
-
return discovered_count
|
|
113
|
-
|
|
114
|
-
def _try_discover_from_module(self, module_name: str) -> int:
|
|
115
|
-
"""Try to discover rules from a module, return 0 on error."""
|
|
116
|
-
try:
|
|
117
|
-
return self._discover_from_module(module_name)
|
|
118
|
-
except (ImportError, AttributeError):
|
|
119
|
-
return 0
|
|
120
|
-
|
|
121
|
-
def _discover_from_module(self, module_path: str) -> int:
|
|
122
|
-
"""Discover rules from a specific module.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
module_path: Full module path to search.
|
|
85
|
+
discovered_rules = self._discovery.discover_from_package(package_path)
|
|
86
|
+
return sum(1 for rule in discovered_rules if self._try_register(rule))
|
|
126
87
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"""
|
|
88
|
+
def _try_register(self, rule: BaseLintRule) -> bool:
|
|
89
|
+
"""Try to register a rule, return True if successful."""
|
|
130
90
|
try:
|
|
131
|
-
|
|
132
|
-
except (ImportError, AttributeError):
|
|
133
|
-
return 0
|
|
134
|
-
|
|
135
|
-
return self._register_rules_from_module(module)
|
|
136
|
-
|
|
137
|
-
def _register_rules_from_module(self, module: Any) -> int:
|
|
138
|
-
"""Register all rule classes from a module."""
|
|
139
|
-
discovered_count = 0
|
|
140
|
-
for _name, obj in inspect.getmembers(module):
|
|
141
|
-
if not self._is_rule_class(obj):
|
|
142
|
-
continue
|
|
143
|
-
if self._try_register_rule_class(obj):
|
|
144
|
-
discovered_count += 1
|
|
145
|
-
return discovered_count
|
|
146
|
-
|
|
147
|
-
def _try_register_rule_class(self, rule_class: Any) -> bool:
|
|
148
|
-
"""Try to instantiate and register a rule class."""
|
|
149
|
-
try:
|
|
150
|
-
rule_instance = rule_class()
|
|
151
|
-
self.register(rule_instance)
|
|
91
|
+
self.register(rule)
|
|
152
92
|
return True
|
|
153
|
-
except
|
|
93
|
+
except ValueError:
|
|
94
|
+
# Rule already registered, skip
|
|
154
95
|
return False
|
|
155
|
-
|
|
156
|
-
def _is_rule_class(self, obj: Any) -> bool:
|
|
157
|
-
"""Check if an object is a valid rule class.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
obj: Object to check.
|
|
161
|
-
|
|
162
|
-
Returns:
|
|
163
|
-
True if obj is a concrete BaseLintRule subclass.
|
|
164
|
-
"""
|
|
165
|
-
return (
|
|
166
|
-
inspect.isclass(obj)
|
|
167
|
-
and issubclass(obj, BaseLintRule)
|
|
168
|
-
and obj is not BaseLintRule # Don't instantiate the base class
|
|
169
|
-
and not inspect.isabstract(obj) # Don't instantiate abstract classes
|
|
170
|
-
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Automatic rule discovery for plugin-based linter architecture
|
|
3
|
+
|
|
4
|
+
Scope: Discovers and validates linting rules from Python packages
|
|
5
|
+
|
|
6
|
+
Overview: Provides automatic rule discovery functionality for the linter framework. Scans Python
|
|
7
|
+
packages for classes inheriting from BaseLintRule, filters out abstract base classes, validates
|
|
8
|
+
rule classes, and attempts instantiation. Handles import errors gracefully to support partial
|
|
9
|
+
package installations. Enables plugin architecture by discovering rules without explicit registration.
|
|
10
|
+
|
|
11
|
+
Dependencies: importlib, inspect, pkgutil, BaseLintRule
|
|
12
|
+
|
|
13
|
+
Exports: RuleDiscovery
|
|
14
|
+
|
|
15
|
+
Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
|
|
16
|
+
|
|
17
|
+
Implementation: Package traversal with pkgutil, class introspection with inspect, error handling
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import inspect
|
|
22
|
+
import pkgutil
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .base import BaseLintRule
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RuleDiscovery:
|
|
29
|
+
"""Discovers linting rules from Python packages."""
|
|
30
|
+
|
|
31
|
+
def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
|
|
32
|
+
"""Discover rules from a package and its modules.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of discovered rule instances
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
package = importlib.import_module(package_path)
|
|
42
|
+
except ImportError:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
if not hasattr(package, "__path__"):
|
|
46
|
+
return self._discover_from_module(package_path)
|
|
47
|
+
|
|
48
|
+
return self._discover_from_package_modules(package_path, package)
|
|
49
|
+
|
|
50
|
+
def _discover_from_package_modules(self, package_path: str, package: Any) -> list[BaseLintRule]:
|
|
51
|
+
"""Discover rules from all modules in a package.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
package_path: Package path
|
|
55
|
+
package: Imported package object
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of discovered rules
|
|
59
|
+
"""
|
|
60
|
+
rules = []
|
|
61
|
+
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
62
|
+
full_module_name = f"{package_path}.{module_name}"
|
|
63
|
+
module_rules = self._try_discover_from_module(full_module_name)
|
|
64
|
+
rules.extend(module_rules)
|
|
65
|
+
return rules
|
|
66
|
+
|
|
67
|
+
def _try_discover_from_module(self, module_name: str) -> list[BaseLintRule]:
|
|
68
|
+
"""Try to discover rules from a module, return empty list on error.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
module_name: Full module name
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of discovered rules (empty on error)
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return self._discover_from_module(module_name)
|
|
78
|
+
except (ImportError, AttributeError):
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def _discover_from_module(self, module_path: str) -> list[BaseLintRule]:
|
|
82
|
+
"""Discover rules from a specific module.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
module_path: Full module path to search
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of discovered rule instances
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
module = importlib.import_module(module_path)
|
|
92
|
+
except (ImportError, AttributeError):
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
rules = []
|
|
96
|
+
for _name, obj in inspect.getmembers(module):
|
|
97
|
+
if not self._is_rule_class(obj):
|
|
98
|
+
continue
|
|
99
|
+
rule_instance = self._try_instantiate_rule(obj)
|
|
100
|
+
if rule_instance:
|
|
101
|
+
rules.append(rule_instance)
|
|
102
|
+
return rules
|
|
103
|
+
|
|
104
|
+
def _try_instantiate_rule(self, rule_class: type[BaseLintRule]) -> BaseLintRule | None:
|
|
105
|
+
"""Try to instantiate a rule class.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
rule_class: Rule class to instantiate
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Rule instance or None on error
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return rule_class()
|
|
115
|
+
except (TypeError, AttributeError):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _is_rule_class(self, obj: Any) -> bool:
|
|
119
|
+
"""Check if an object is a valid rule class.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
obj: Object to check
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if obj is a concrete BaseLintRule subclass
|
|
126
|
+
"""
|
|
127
|
+
return (
|
|
128
|
+
inspect.isclass(obj)
|
|
129
|
+
and issubclass(obj, BaseLintRule)
|
|
130
|
+
and obj is not BaseLintRule
|
|
131
|
+
and not inspect.isabstract(obj)
|
|
132
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Base violation builder class for consistent violation creation across all linters
|
|
3
|
+
|
|
4
|
+
Scope: Core violation building functionality used by all linter violation builders
|
|
5
|
+
|
|
6
|
+
Overview: Provides base classes and data structures for violation creation across all linters.
|
|
7
|
+
Defines ViolationInfo dataclass containing all required and optional violation fields,
|
|
8
|
+
and BaseViolationBuilder class with common build() method. Eliminates duplicate violation
|
|
9
|
+
construction patterns across file_placement, nesting, and srp linters. Ensures consistent
|
|
10
|
+
violation creation with proper defaults for column and severity fields. Linter-specific
|
|
11
|
+
builders extend this base class to inherit common construction logic while maintaining
|
|
12
|
+
their domain-specific message generation and suggestion logic.
|
|
13
|
+
|
|
14
|
+
Dependencies: dataclasses, src.core.types (Violation, Severity)
|
|
15
|
+
|
|
16
|
+
Exports: ViolationInfo dataclass, BaseViolationBuilder class
|
|
17
|
+
|
|
18
|
+
Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
|
|
19
|
+
BaseViolationBuilder.build(info: ViolationInfo) -> Violation
|
|
20
|
+
|
|
21
|
+
Implementation: Uses dataclass for type-safe violation info, base class provides build()
|
|
22
|
+
method that constructs Violation objects with proper defaults
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
from src.core.types import Severity, Violation
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ViolationInfo:
|
|
32
|
+
"""Information needed to build a violation.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
rule_id: Identifier for the rule that was violated
|
|
36
|
+
file_path: Path to the file containing the violation
|
|
37
|
+
line: Line number where violation occurs (1-indexed)
|
|
38
|
+
message: Description of the violation
|
|
39
|
+
column: Column number where violation occurs (0-indexed, default=1)
|
|
40
|
+
severity: Severity level of the violation (default=ERROR)
|
|
41
|
+
suggestion: Optional suggestion for fixing the violation
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
rule_id: str
|
|
45
|
+
file_path: str
|
|
46
|
+
line: int
|
|
47
|
+
message: str
|
|
48
|
+
column: int = 1
|
|
49
|
+
severity: Severity = Severity.ERROR
|
|
50
|
+
suggestion: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BaseViolationBuilder:
|
|
54
|
+
"""Base class for building violations with consistent structure.
|
|
55
|
+
|
|
56
|
+
Provides common build() method for creating Violation objects from ViolationInfo.
|
|
57
|
+
Linter-specific builders extend this class to add their domain-specific violation
|
|
58
|
+
creation methods while inheriting the common construction logic.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def build(self, info: ViolationInfo) -> Violation:
|
|
62
|
+
"""Build a Violation from ViolationInfo.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
info: ViolationInfo containing all violation details
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Violation object with all fields populated
|
|
69
|
+
"""
|
|
70
|
+
return Violation(
|
|
71
|
+
rule_id=info.rule_id,
|
|
72
|
+
file_path=info.file_path,
|
|
73
|
+
line=info.line,
|
|
74
|
+
column=info.column,
|
|
75
|
+
message=info.message,
|
|
76
|
+
severity=info.severity,
|
|
77
|
+
suggestion=info.suggestion,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def build_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
81
|
+
self,
|
|
82
|
+
rule_id: str,
|
|
83
|
+
file_path: str,
|
|
84
|
+
line: int,
|
|
85
|
+
message: str,
|
|
86
|
+
column: int = 1,
|
|
87
|
+
severity: Severity = Severity.ERROR,
|
|
88
|
+
suggestion: str | None = None,
|
|
89
|
+
) -> Violation:
|
|
90
|
+
"""Build a Violation directly from parameters.
|
|
91
|
+
|
|
92
|
+
Note: Pylint too-many-arguments disabled. This convenience method mirrors the
|
|
93
|
+
ViolationInfo dataclass fields (7 parameters, 3 with defaults). The alternative
|
|
94
|
+
would require every caller to create ViolationInfo objects manually, reducing
|
|
95
|
+
readability. This is a standard builder pattern where all parameters are
|
|
96
|
+
inherently related (Violation fields).
|
|
97
|
+
|
|
98
|
+
This is a convenience method that combines ViolationInfo creation and build()
|
|
99
|
+
to reduce duplication in violation builder methods.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
rule_id: Identifier for the rule that was violated
|
|
103
|
+
file_path: Path to the file containing the violation
|
|
104
|
+
line: Line number where violation occurs (1-indexed)
|
|
105
|
+
message: Description of the violation
|
|
106
|
+
column: Column number where violation occurs (0-indexed, default=1)
|
|
107
|
+
severity: Severity level of the violation (default=ERROR)
|
|
108
|
+
suggestion: Optional suggestion for fixing the violation
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Violation object with all fields populated
|
|
112
|
+
"""
|
|
113
|
+
info = ViolationInfo(
|
|
114
|
+
rule_id=rule_id,
|
|
115
|
+
file_path=file_path,
|
|
116
|
+
line=line,
|
|
117
|
+
message=message,
|
|
118
|
+
column=column,
|
|
119
|
+
severity=severity,
|
|
120
|
+
suggestion=suggestion,
|
|
121
|
+
)
|
|
122
|
+
return self.build(info)
|