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.
Files changed (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -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 the plugin discovery system that enables the extensible architecture by
7
- automatically finding and registering linting rules from specified packages without requiring
8
- explicit registration code. The RuleRegistry maintains a collection of discovered rules indexed
9
- by rule_id, providing methods to register individual rules, retrieve rules by identifier, and
10
- list all available rules. Auto-discovery works by scanning Python packages for classes that
11
- inherit from BaseLintRule, filtering out abstract base classes, instantiating concrete rule
12
- classes, and registering them for use by the orchestrator. This enables developers to add new
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: Package scanning with pkgutil.iter_modules(), class introspection with inspect,
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
- try:
97
- package = importlib.import_module(package_path)
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
- Returns:
128
- Number of rules discovered from this module.
129
- """
88
+ def _try_register(self, rule: BaseLintRule) -> bool:
89
+ """Try to register a rule, return True if successful."""
130
90
  try:
131
- module = importlib.import_module(module_path)
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 (TypeError, AttributeError, ValueError):
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)