thailint 0.8.0__py3-none-any.whl → 0.10.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 (46) hide show
  1. src/cli.py +242 -0
  2. src/config.py +2 -3
  3. src/core/base.py +4 -0
  4. src/core/rule_discovery.py +143 -84
  5. src/core/violation_builder.py +75 -15
  6. src/linter_config/loader.py +43 -11
  7. src/linters/collection_pipeline/__init__.py +90 -0
  8. src/linters/collection_pipeline/config.py +63 -0
  9. src/linters/collection_pipeline/continue_analyzer.py +100 -0
  10. src/linters/collection_pipeline/detector.py +130 -0
  11. src/linters/collection_pipeline/linter.py +437 -0
  12. src/linters/collection_pipeline/suggestion_builder.py +63 -0
  13. src/linters/dry/block_filter.py +6 -8
  14. src/linters/dry/block_grouper.py +4 -0
  15. src/linters/dry/cache_query.py +4 -0
  16. src/linters/dry/python_analyzer.py +34 -18
  17. src/linters/dry/token_hasher.py +5 -1
  18. src/linters/dry/typescript_analyzer.py +61 -31
  19. src/linters/dry/violation_builder.py +4 -0
  20. src/linters/dry/violation_filter.py +4 -0
  21. src/linters/file_header/bash_parser.py +4 -0
  22. src/linters/file_header/linter.py +7 -11
  23. src/linters/file_placement/directory_matcher.py +4 -0
  24. src/linters/file_placement/linter.py +28 -8
  25. src/linters/file_placement/pattern_matcher.py +4 -0
  26. src/linters/file_placement/pattern_validator.py +4 -0
  27. src/linters/magic_numbers/context_analyzer.py +4 -0
  28. src/linters/magic_numbers/typescript_analyzer.py +4 -0
  29. src/linters/nesting/python_analyzer.py +4 -0
  30. src/linters/nesting/typescript_function_extractor.py +4 -0
  31. src/linters/print_statements/typescript_analyzer.py +4 -0
  32. src/linters/srp/class_analyzer.py +4 -0
  33. src/linters/srp/heuristics.py +4 -3
  34. src/linters/srp/linter.py +2 -3
  35. src/linters/srp/python_analyzer.py +55 -20
  36. src/linters/srp/typescript_metrics_calculator.py +83 -47
  37. src/linters/srp/violation_builder.py +4 -0
  38. src/linters/stateless_class/__init__.py +25 -0
  39. src/linters/stateless_class/config.py +58 -0
  40. src/linters/stateless_class/linter.py +355 -0
  41. src/linters/stateless_class/python_analyzer.py +299 -0
  42. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/METADATA +226 -3
  43. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/RECORD +46 -36
  44. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/WHEEL +0 -0
  45. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/entry_points.txt +0 -0
  46. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/licenses/LICENSE +0 -0
src/cli.py CHANGED
@@ -1895,5 +1895,247 @@ def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-man
1895
1895
  sys.exit(1 if method_property_violations else 0)
1896
1896
 
1897
1897
 
1898
+ # =============================================================================
1899
+ # Stateless Class Linter Command
1900
+ # =============================================================================
1901
+
1902
+
1903
+ def _setup_stateless_class_orchestrator(
1904
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
1905
+ ):
1906
+ """Set up orchestrator for stateless-class command."""
1907
+ from src.orchestrator.core import Orchestrator
1908
+ from src.utils.project_root import get_project_root
1909
+
1910
+ if project_root is None:
1911
+ first_path = path_objs[0] if path_objs else Path.cwd()
1912
+ search_start = first_path if first_path.is_dir() else first_path.parent
1913
+ project_root = get_project_root(search_start)
1914
+
1915
+ orchestrator = Orchestrator(project_root=project_root)
1916
+
1917
+ if config_file:
1918
+ _load_config_file(orchestrator, config_file, verbose)
1919
+
1920
+ return orchestrator
1921
+
1922
+
1923
+ def _run_stateless_class_lint(orchestrator, path_objs: list[Path], recursive: bool):
1924
+ """Execute stateless-class lint on files or directories."""
1925
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
1926
+ return [v for v in all_violations if "stateless-class" in v.rule_id]
1927
+
1928
+
1929
+ @cli.command("stateless-class")
1930
+ @click.argument("paths", nargs=-1, type=click.Path())
1931
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
1932
+ @format_option
1933
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
1934
+ @click.pass_context
1935
+ def stateless_class(
1936
+ ctx,
1937
+ paths: tuple[str, ...],
1938
+ config_file: str | None,
1939
+ format: str,
1940
+ recursive: bool,
1941
+ ):
1942
+ """Check for stateless classes that should be module functions.
1943
+
1944
+ Detects Python classes that have no constructor (__init__), no instance
1945
+ state, and 2+ methods - indicating they should be refactored to module-level
1946
+ functions instead of using a class as a namespace.
1947
+
1948
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
1949
+
1950
+ Examples:
1951
+
1952
+ \b
1953
+ # Check current directory (all files recursively)
1954
+ thai-lint stateless-class
1955
+
1956
+ \b
1957
+ # Check specific directory
1958
+ thai-lint stateless-class src/
1959
+
1960
+ \b
1961
+ # Check single file
1962
+ thai-lint stateless-class src/utils.py
1963
+
1964
+ \b
1965
+ # Check multiple files
1966
+ thai-lint stateless-class src/utils.py src/helpers.py
1967
+
1968
+ \b
1969
+ # Get JSON output
1970
+ thai-lint stateless-class --format json .
1971
+
1972
+ \b
1973
+ # Get SARIF output for CI/CD integration
1974
+ thai-lint stateless-class --format sarif src/
1975
+
1976
+ \b
1977
+ # Use custom config file
1978
+ thai-lint stateless-class --config .thailint.yaml src/
1979
+ """
1980
+ verbose = ctx.obj.get("verbose", False)
1981
+ project_root = _get_project_root_from_context(ctx)
1982
+
1983
+ if not paths:
1984
+ paths = (".",)
1985
+
1986
+ path_objs = [Path(p) for p in paths]
1987
+
1988
+ try:
1989
+ _execute_stateless_class_lint(
1990
+ path_objs, config_file, format, recursive, verbose, project_root
1991
+ )
1992
+ except Exception as e:
1993
+ _handle_linting_error(e, verbose)
1994
+
1995
+
1996
+ def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
1997
+ path_objs, config_file, format, recursive, verbose, project_root=None
1998
+ ):
1999
+ """Execute stateless-class lint."""
2000
+ _validate_paths_exist(path_objs)
2001
+ orchestrator = _setup_stateless_class_orchestrator(
2002
+ path_objs, config_file, verbose, project_root
2003
+ )
2004
+ stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
2005
+
2006
+ if verbose:
2007
+ logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
2008
+
2009
+ format_violations(stateless_class_violations, format)
2010
+ sys.exit(1 if stateless_class_violations else 0)
2011
+
2012
+
2013
+ # Collection Pipeline command helper functions
2014
+
2015
+
2016
+ def _setup_pipeline_orchestrator(
2017
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
2018
+ ):
2019
+ """Set up orchestrator for pipeline command."""
2020
+ from src.orchestrator.core import Orchestrator
2021
+ from src.utils.project_root import get_project_root
2022
+
2023
+ # Use provided project_root or fall back to auto-detection
2024
+ if project_root is None:
2025
+ first_path = path_objs[0] if path_objs else Path.cwd()
2026
+ search_start = first_path if first_path.is_dir() else first_path.parent
2027
+ project_root = get_project_root(search_start)
2028
+
2029
+ orchestrator = Orchestrator(project_root=project_root)
2030
+
2031
+ if config_file:
2032
+ _load_config_file(orchestrator, config_file, verbose)
2033
+
2034
+ return orchestrator
2035
+
2036
+
2037
+ def _apply_pipeline_config_override(orchestrator, min_continues: int | None, verbose: bool):
2038
+ """Apply min_continues override to orchestrator config."""
2039
+ if min_continues is None:
2040
+ return
2041
+
2042
+ if "collection_pipeline" not in orchestrator.config:
2043
+ orchestrator.config["collection_pipeline"] = {}
2044
+
2045
+ orchestrator.config["collection_pipeline"]["min_continues"] = min_continues
2046
+ if verbose:
2047
+ logger.debug(f"Overriding min_continues to {min_continues}")
2048
+
2049
+
2050
+ def _run_pipeline_lint(orchestrator, path_objs: list[Path], recursive: bool):
2051
+ """Execute collection-pipeline lint on files or directories."""
2052
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
2053
+ return [v for v in all_violations if "collection-pipeline" in v.rule_id]
2054
+
2055
+
2056
+ @cli.command("pipeline")
2057
+ @click.argument("paths", nargs=-1, type=click.Path())
2058
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
2059
+ @format_option
2060
+ @click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
2061
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
2062
+ @click.pass_context
2063
+ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
2064
+ ctx,
2065
+ paths: tuple[str, ...],
2066
+ config_file: str | None,
2067
+ format: str,
2068
+ min_continues: int | None,
2069
+ recursive: bool,
2070
+ ):
2071
+ """Check for collection pipeline anti-patterns in code.
2072
+
2073
+ Detects for loops with embedded if/continue filtering patterns that could
2074
+ be refactored to use collection pipelines (generator expressions, filter()).
2075
+
2076
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
2077
+
2078
+ Examples:
2079
+
2080
+ \b
2081
+ # Check current directory (all Python files recursively)
2082
+ thai-lint pipeline
2083
+
2084
+ \b
2085
+ # Check specific directory
2086
+ thai-lint pipeline src/
2087
+
2088
+ \b
2089
+ # Check single file
2090
+ thai-lint pipeline src/app.py
2091
+
2092
+ \b
2093
+ # Only flag loops with 2+ continue guards
2094
+ thai-lint pipeline --min-continues 2 src/
2095
+
2096
+ \b
2097
+ # Get JSON output
2098
+ thai-lint pipeline --format json .
2099
+
2100
+ \b
2101
+ # Get SARIF output for CI/CD integration
2102
+ thai-lint pipeline --format sarif src/
2103
+
2104
+ \b
2105
+ # Use custom config file
2106
+ thai-lint pipeline --config .thailint.yaml src/
2107
+ """
2108
+ verbose = ctx.obj.get("verbose", False)
2109
+ project_root = _get_project_root_from_context(ctx)
2110
+
2111
+ if not paths:
2112
+ paths = (".",)
2113
+
2114
+ path_objs = [Path(p) for p in paths]
2115
+
2116
+ try:
2117
+ _execute_pipeline_lint(
2118
+ path_objs, config_file, format, min_continues, recursive, verbose, project_root
2119
+ )
2120
+ except Exception as e:
2121
+ _handle_linting_error(e, verbose)
2122
+
2123
+
2124
+ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
2125
+ path_objs, config_file, format, min_continues, recursive, verbose, project_root=None
2126
+ ):
2127
+ """Execute collection-pipeline lint."""
2128
+ _validate_paths_exist(path_objs)
2129
+ orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
2130
+ _apply_pipeline_config_override(orchestrator, min_continues, verbose)
2131
+ pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive)
2132
+
2133
+ if verbose:
2134
+ logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
2135
+
2136
+ format_violations(pipeline_violations, format)
2137
+ sys.exit(1 if pipeline_violations else 0)
2138
+
2139
+
1898
2140
  if __name__ == "__main__":
1899
2141
  cli()
src/config.py CHANGED
@@ -103,9 +103,8 @@ def _load_from_explicit_path(config_path: Path) -> dict[str, Any]:
103
103
 
104
104
  def _load_from_default_locations() -> dict[str, Any]:
105
105
  """Load config from default locations."""
106
- for location in CONFIG_LOCATIONS:
107
- if not location.exists():
108
- continue
106
+ existing_locations = (loc for loc in CONFIG_LOCATIONS if loc.exists())
107
+ for location in existing_locations:
109
108
  loaded_config = _try_load_from_location(location)
110
109
  if loaded_config:
111
110
  return loaded_config
src/core/base.py CHANGED
@@ -151,6 +151,10 @@ class MultiLanguageLintRule(BaseLintRule):
151
151
  - _load_config(context) for configuration loading
152
152
  """
153
153
 
154
+ def __init__(self) -> None:
155
+ """Initialize the multi-language lint rule."""
156
+ pass # Base class for multi-language linters
157
+
154
158
  def check(self, context: BaseLintContext) -> list[Violation]:
155
159
  """Check for violations with automatic language dispatch.
156
160
 
@@ -10,7 +10,7 @@ Overview: Provides automatic rule discovery functionality for the linter framewo
10
10
 
11
11
  Dependencies: importlib, inspect, pkgutil, BaseLintRule
12
12
 
13
- Exports: RuleDiscovery
13
+ Exports: discover_from_package function, RuleDiscovery class (compat)
14
14
 
15
15
  Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
16
16
 
@@ -20,113 +20,172 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
20
20
  import importlib
21
21
  import inspect
22
22
  import pkgutil
23
+ from types import ModuleType
23
24
  from typing import Any
24
25
 
25
26
  from .base import BaseLintRule
26
27
 
27
28
 
28
- class RuleDiscovery:
29
- """Discovers linting rules from Python packages."""
29
+ def discover_from_package(package_path: str) -> list[BaseLintRule]:
30
+ """Discover rules from a package and its modules.
30
31
 
31
- def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
32
- """Discover rules from a package and its modules.
32
+ Args:
33
+ package_path: Python package path (e.g., 'src.linters')
33
34
 
34
- Args:
35
- package_path: Python package path (e.g., 'src.linters')
35
+ Returns:
36
+ List of discovered rule instances
37
+ """
38
+ try:
39
+ package = importlib.import_module(package_path)
40
+ except ImportError:
41
+ return []
36
42
 
37
- Returns:
38
- List of discovered rule instances
39
- """
40
- try:
41
- package = importlib.import_module(package_path)
42
- except ImportError:
43
- return []
43
+ if not hasattr(package, "__path__"):
44
+ return _discover_from_module(package_path)
44
45
 
45
- if not hasattr(package, "__path__"):
46
- return self._discover_from_module(package_path)
46
+ return _discover_from_package_modules(package_path, package)
47
47
 
48
- return self._discover_from_package_modules(package_path, package)
49
48
 
50
- def _discover_from_package_modules(self, package_path: str, package: Any) -> list[BaseLintRule]:
51
- """Discover rules from all modules in a package.
49
+ def _discover_from_package_modules(package_path: str, package: Any) -> list[BaseLintRule]:
50
+ """Discover rules from all modules in a package.
52
51
 
53
- Args:
54
- package_path: Package path
55
- package: Imported package object
52
+ Args:
53
+ package_path: Package path
54
+ package: Imported package object
56
55
 
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
56
+ Returns:
57
+ List of discovered rules
58
+ """
59
+ rules = []
60
+ for _, module_name, _ in pkgutil.iter_modules(package.__path__):
61
+ full_module_name = f"{package_path}.{module_name}"
62
+ module_rules = _try_discover_from_module(full_module_name)
63
+ rules.extend(module_rules)
64
+ return rules
66
65
 
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
66
 
70
- Args:
71
- module_name: Full module name
67
+ def _try_discover_from_module(module_name: str) -> list[BaseLintRule]:
68
+ """Try to discover rules from a module, return empty list on error.
72
69
 
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 []
70
+ Args:
71
+ module_name: Full module name
80
72
 
81
- def _discover_from_module(self, module_path: str) -> list[BaseLintRule]:
82
- """Discover rules from a specific module.
73
+ Returns:
74
+ List of discovered rules (empty on error)
75
+ """
76
+ try:
77
+ return _discover_from_module(module_name)
78
+ except (ImportError, AttributeError):
79
+ return []
83
80
 
84
- Args:
85
- module_path: Full module path to search
86
81
 
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.
82
+ def _discover_from_module(module_path: str) -> list[BaseLintRule]:
83
+ """Discover rules from a specific module.
106
84
 
107
- Args:
108
- rule_class: Rule class to instantiate
85
+ Args:
86
+ module_path: Full module path to search
109
87
 
110
- Returns:
111
- Rule instance or None on error
112
- """
113
- try:
114
- return rule_class()
115
- except (TypeError, AttributeError):
116
- return None
88
+ Returns:
89
+ List of discovered rule instances
90
+ """
91
+ module = _try_import_module(module_path)
92
+ if module is None:
93
+ return []
94
+ return _extract_rules_from_module(module)
95
+
96
+
97
+ def _try_import_module(module_path: str) -> ModuleType | None:
98
+ """Try to import a module, returning None on failure.
99
+
100
+ Args:
101
+ module_path: Full module path to import
102
+
103
+ Returns:
104
+ Module object or None if import fails
105
+ """
106
+ try:
107
+ return importlib.import_module(module_path)
108
+ except (ImportError, AttributeError):
109
+ return None
117
110
 
118
- def _is_rule_class(self, obj: Any) -> bool:
119
- """Check if an object is a valid rule class.
111
+
112
+ def _extract_rules_from_module(module: ModuleType) -> list[BaseLintRule]:
113
+ """Extract rule instances from a module.
114
+
115
+ Args:
116
+ module: Imported module to scan
117
+
118
+ Returns:
119
+ List of discovered rule instances
120
+ """
121
+ rule_classes = [obj for _name, obj in inspect.getmembers(module) if _is_rule_class(obj)]
122
+ return _instantiate_rules(rule_classes)
123
+
124
+
125
+ def _instantiate_rules(rule_classes: list[type[BaseLintRule]]) -> list[BaseLintRule]:
126
+ """Instantiate a list of rule classes.
127
+
128
+ Args:
129
+ rule_classes: List of rule classes to instantiate
130
+
131
+ Returns:
132
+ List of successfully instantiated rules
133
+ """
134
+ instances = (_try_instantiate_rule(cls) for cls in rule_classes)
135
+ return [inst for inst in instances if inst is not None]
136
+
137
+
138
+ def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
139
+ """Try to instantiate a rule class.
140
+
141
+ Args:
142
+ rule_class: Rule class to instantiate
143
+
144
+ Returns:
145
+ Rule instance or None on error
146
+ """
147
+ try:
148
+ return rule_class()
149
+ except (TypeError, AttributeError):
150
+ return None
151
+
152
+
153
+ def _is_rule_class(obj: Any) -> bool:
154
+ """Check if an object is a valid rule class.
155
+
156
+ Args:
157
+ obj: Object to check
158
+
159
+ Returns:
160
+ True if obj is a concrete BaseLintRule subclass
161
+ """
162
+ return (
163
+ inspect.isclass(obj)
164
+ and issubclass(obj, BaseLintRule)
165
+ and obj is not BaseLintRule
166
+ and not inspect.isabstract(obj)
167
+ )
168
+
169
+
170
+ # Legacy class wrapper for backward compatibility
171
+ class RuleDiscovery:
172
+ """Discovers linting rules from Python packages.
173
+
174
+ Note: This class is a thin wrapper around module-level functions
175
+ for backward compatibility.
176
+ """
177
+
178
+ def __init__(self) -> None:
179
+ """Initialize the discovery service."""
180
+ pass # No state needed
181
+
182
+ def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
183
+ """Discover rules from a package and its modules.
120
184
 
121
185
  Args:
122
- obj: Object to check
186
+ package_path: Python package path (e.g., 'src.linters')
123
187
 
124
188
  Returns:
125
- True if obj is a concrete BaseLintRule subclass
189
+ List of discovered rule instances
126
190
  """
127
- return (
128
- inspect.isclass(obj)
129
- and issubclass(obj, BaseLintRule)
130
- and obj is not BaseLintRule
131
- and not inspect.isabstract(obj)
132
- )
191
+ return discover_from_package(package_path)
@@ -13,13 +13,14 @@ Overview: Provides base classes and data structures for violation creation acros
13
13
 
14
14
  Dependencies: dataclasses, src.core.types (Violation, Severity)
15
15
 
16
- Exports: ViolationInfo dataclass, BaseViolationBuilder class
16
+ Exports: ViolationInfo dataclass, build_violation function, build_violation_from_params function,
17
+ BaseViolationBuilder class (compat)
17
18
 
18
19
  Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
19
- BaseViolationBuilder.build(info: ViolationInfo) -> Violation
20
+ build_violation(info: ViolationInfo) -> Violation
20
21
 
21
- Implementation: Uses dataclass for type-safe violation info, base class provides build()
22
- method that constructs Violation objects with proper defaults
22
+ Implementation: Uses dataclass for type-safe violation info, functions provide build logic
23
+ that constructs Violation objects with proper defaults
23
24
  """
24
25
 
25
26
  from dataclasses import dataclass
@@ -50,14 +51,82 @@ class ViolationInfo:
50
51
  suggestion: str | None = None
51
52
 
52
53
 
54
+ def build_violation(info: ViolationInfo) -> Violation:
55
+ """Build a Violation from ViolationInfo.
56
+
57
+ Args:
58
+ info: ViolationInfo containing all violation details
59
+
60
+ Returns:
61
+ Violation object with all fields populated
62
+ """
63
+ return Violation(
64
+ rule_id=info.rule_id,
65
+ file_path=info.file_path,
66
+ line=info.line,
67
+ column=info.column,
68
+ message=info.message,
69
+ severity=info.severity,
70
+ suggestion=info.suggestion,
71
+ )
72
+
73
+
74
+ def build_violation_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
75
+ rule_id: str,
76
+ file_path: str,
77
+ line: int,
78
+ message: str,
79
+ column: int = 1,
80
+ severity: Severity = Severity.ERROR,
81
+ suggestion: str | None = None,
82
+ ) -> Violation:
83
+ """Build a Violation directly from parameters.
84
+
85
+ Note: Pylint too-many-arguments disabled. This convenience function mirrors the
86
+ ViolationInfo dataclass fields (7 parameters, 3 with defaults). The alternative
87
+ would require every caller to create ViolationInfo objects manually, reducing
88
+ readability.
89
+
90
+ Args:
91
+ rule_id: Identifier for the rule that was violated
92
+ file_path: Path to the file containing the violation
93
+ line: Line number where violation occurs (1-indexed)
94
+ message: Description of the violation
95
+ column: Column number where violation occurs (0-indexed, default=1)
96
+ severity: Severity level of the violation (default=ERROR)
97
+ suggestion: Optional suggestion for fixing the violation
98
+
99
+ Returns:
100
+ Violation object with all fields populated
101
+ """
102
+ info = ViolationInfo(
103
+ rule_id=rule_id,
104
+ file_path=file_path,
105
+ line=line,
106
+ message=message,
107
+ column=column,
108
+ severity=severity,
109
+ suggestion=suggestion,
110
+ )
111
+ return build_violation(info)
112
+
113
+
114
+ # Legacy class wrapper for backward compatibility
53
115
  class BaseViolationBuilder:
54
116
  """Base class for building violations with consistent structure.
55
117
 
56
118
  Provides common build() method for creating Violation objects from ViolationInfo.
57
119
  Linter-specific builders extend this class to add their domain-specific violation
58
120
  creation methods while inheriting the common construction logic.
121
+
122
+ Note: This class is a thin wrapper around module-level functions
123
+ for backward compatibility.
59
124
  """
60
125
 
126
+ def __init__(self) -> None:
127
+ """Initialize the builder."""
128
+ pass # No state needed
129
+
61
130
  def build(self, info: ViolationInfo) -> Violation:
62
131
  """Build a Violation from ViolationInfo.
63
132
 
@@ -67,15 +136,7 @@ class BaseViolationBuilder:
67
136
  Returns:
68
137
  Violation object with all fields populated
69
138
  """
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
- )
139
+ return build_violation(info)
79
140
 
80
141
  def build_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
81
142
  self,
@@ -110,7 +171,7 @@ class BaseViolationBuilder:
110
171
  Returns:
111
172
  Violation object with all fields populated
112
173
  """
113
- info = ViolationInfo(
174
+ return build_violation_from_params(
114
175
  rule_id=rule_id,
115
176
  file_path=file_path,
116
177
  line=line,
@@ -119,4 +180,3 @@ class BaseViolationBuilder:
119
180
  severity=severity,
120
181
  suggestion=suggestion,
121
182
  )
122
- return self.build(info)