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.
- src/cli.py +242 -0
- src/config.py +2 -3
- src/core/base.py +4 -0
- src/core/rule_discovery.py +143 -84
- src/core/violation_builder.py +75 -15
- src/linter_config/loader.py +43 -11
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/config.py +63 -0
- src/linters/collection_pipeline/continue_analyzer.py +100 -0
- src/linters/collection_pipeline/detector.py +130 -0
- src/linters/collection_pipeline/linter.py +437 -0
- src/linters/collection_pipeline/suggestion_builder.py +63 -0
- src/linters/dry/block_filter.py +6 -8
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/python_analyzer.py +34 -18
- src/linters/dry/token_hasher.py +5 -1
- src/linters/dry/typescript_analyzer.py +61 -31
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/linter.py +7 -11
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +28 -8
- src/linters/file_placement/pattern_matcher.py +4 -0
- src/linters/file_placement/pattern_validator.py +4 -0
- src/linters/magic_numbers/context_analyzer.py +4 -0
- src/linters/magic_numbers/typescript_analyzer.py +4 -0
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_function_extractor.py +4 -0
- src/linters/print_statements/typescript_analyzer.py +4 -0
- src/linters/srp/class_analyzer.py +4 -0
- src/linters/srp/heuristics.py +4 -3
- src/linters/srp/linter.py +2 -3
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +83 -47
- src/linters/srp/violation_builder.py +4 -0
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +355 -0
- src/linters/stateless_class/python_analyzer.py +299 -0
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/METADATA +226 -3
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/RECORD +46 -36
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/WHEEL +0 -0
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
107
|
-
|
|
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
|
|
src/core/rule_discovery.py
CHANGED
|
@@ -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
|
-
|
|
29
|
-
"""
|
|
29
|
+
def discover_from_package(package_path: str) -> list[BaseLintRule]:
|
|
30
|
+
"""Discover rules from a package and its modules.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
Args:
|
|
33
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
Args:
|
|
53
|
+
package_path: Package path
|
|
54
|
+
package: Imported package object
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
108
|
-
|
|
85
|
+
Args:
|
|
86
|
+
module_path: Full module path to search
|
|
109
87
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
186
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
123
187
|
|
|
124
188
|
Returns:
|
|
125
|
-
|
|
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)
|
src/core/violation_builder.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
20
|
+
build_violation(info: ViolationInfo) -> Violation
|
|
20
21
|
|
|
21
|
-
Implementation: Uses dataclass for type-safe violation info,
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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)
|