Coverage for .tox/py312/lib/python3.12/site-packages/pylint_sort_functions/checker.py: 100%
50 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 04:45 +0200
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 04:45 +0200
1"""Main checker class for enforcing function and method sorting.
3The FunctionSortChecker is used by PyLint itself, not by end users directly.
4PyLint discovers this checker via the plugin entry point and manages its lifecycle.
6For detailed information about the sorting rules and algorithm, see docs/sorting.rst.
8How it works:
9 1. PyLint loads the plugin and calls register() function (the plugin entry point
10 defined in __init__.py and configured in pyproject.toml)
11 2. register() creates a FunctionSortChecker instance and gives it to PyLint
12 3. PyLint walks the AST (Abstract Syntax Tree) of user code
13 4. For each AST node, PyLint calls corresponding visit_* methods on this checker
14 (we only implement visit_module and visit_classdef from the many available)
15 5. The checker analyzes nodes and calls self.add_message() when issues are found
17User Experience:
18 $ pylint --load-plugins=pylint_sort_functions mycode.py
19 # PyLint automatically uses this checker and reports any sorting violations
21The visitor pattern: PyLint calls visit_module() for modules and visit_classdef()
22for class definitions. Each method analyzes the code structure and reports issues.
23"""
25from pathlib import Path
26from typing import TYPE_CHECKING, Any
28from astroid import nodes # type: ignore[import-untyped]
29from pylint.checkers import BaseChecker
31from pylint_sort_functions import messages, utils
33if TYPE_CHECKING:
34 pass
37class FunctionSortChecker(BaseChecker):
38 """Checker to enforce alphabetical sorting of functions and methods.
40 Inherits from PyLint's BaseChecker which provides the visitor pattern
41 infrastructure. PyLint will automatically call our visit_* methods as it
42 traverses the AST.
43 """
45 name = "function-sort" # Identifier used by PyLint for this checker
46 msgs: dict[str, Any] = messages.MESSAGES # Message definitions from messages.py
47 options = (
48 (
49 "public-api-patterns",
50 {
51 "default": [
52 "main",
53 "run",
54 "execute",
55 "start",
56 "stop",
57 "setup",
58 "teardown",
59 ],
60 "type": "csv",
61 "metavar": "<pattern1,pattern2,...>",
62 "help": (
63 "List of function names to always treat as public API. "
64 "These functions will not be flagged for privacy even if only used "
65 "internally. Useful for entry points and framework callbacks."
66 ),
67 },
68 ),
69 (
70 "enable-privacy-detection",
71 {
72 "default": True,
73 "type": "yn",
74 "metavar": "<y or n>",
75 "help": (
76 "Enable detection of functions that should be made private "
77 "based on usage analysis."
78 ),
79 },
80 ),
81 )
83 # Public methods
85 def visit_classdef(self, node: nodes.ClassDef) -> None:
86 """Visit a class definition to check method sorting.
88 Called by PyLint for each class definition in the code.
90 :param node: The class definition AST node to analyze
91 :type node: nodes.ClassDef
92 """
93 methods = utils.get_methods_from_class(node)
94 if not utils.are_methods_sorted(methods):
95 # Report unsorted methods - see docs/usage.rst for message details
96 self.add_message("unsorted-methods", node=node, args=(node.name,))
98 if not utils.are_functions_properly_separated(methods):
99 # Report mixed visibility - see docs/usage.rst for suppression options
100 self.add_message(
101 "mixed-function-visibility",
102 node=node,
103 args=(f"class {node.name}",),
104 )
106 def visit_module(self, node: nodes.Module) -> None:
107 """Visit a module node to check function sorting and privacy.
109 Called by PyLint once for each Python module (file) being analyzed.
111 :param node: The module AST node to analyze
112 :type node: nodes.Module
113 """
114 functions = utils.get_functions_from_node(node)
115 if not utils.are_functions_sorted(functions):
116 # Report unsorted functions - see docs/usage.rst for configuration
117 self.add_message("unsorted-functions", node=node, args=("module",))
119 if not utils.are_functions_properly_separated(functions):
120 # Report mixed visibility - see docs/usage.rst for severity levels
121 self.add_message("mixed-function-visibility", node=node, args=("module",))
123 # Check if any public functions should be private
124 self._check_function_privacy(functions, node)
126 # Private methods
128 def _check_function_privacy(
129 self, functions: list[nodes.FunctionDef], node: nodes.Module
130 ) -> None:
131 """Check if any public functions should be private using import analysis.
133 :param functions: List of functions to check
134 :type functions: list[nodes.FunctionDef]
135 :param node: The module node
136 :type node: nodes.Module
137 """
138 # Check if privacy detection is enabled
139 if not self.linter.config.enable_privacy_detection:
140 return
142 module_path = self._get_module_path()
143 if not module_path:
144 # Fallback to heuristic approach when path info unavailable
145 self._check_function_privacy_heuristic(functions, node)
146 return
148 project_root = self._get_project_root(module_path)
149 if not project_root:
150 # Fallback to heuristic approach when project root can't be determined
151 self._check_function_privacy_heuristic(functions, node)
152 return
154 # Get configured public API patterns
155 public_patterns = set(self.linter.config.public_api_patterns)
157 # Use import analysis for more accurate detection
158 for func in functions:
159 if utils.should_function_be_private(
160 func, module_path, project_root, public_patterns
161 ):
162 # Report function that should be private
163 # See docs/usage.rst for privacy detection feature
164 self.add_message(
165 "function-should-be-private", node=func, args=(func.name,)
166 )
168 def _check_function_privacy_heuristic(
169 self,
170 functions: list[nodes.FunctionDef],
171 node: nodes.Module, # pylint: disable=unused-argument
172 ) -> None:
173 """Check function privacy using heuristic approach (fallback).
175 Used when import analysis is not available due to missing path information.
177 :param functions: List of functions to check
178 :type functions: list[nodes.FunctionDef]
179 :param node: The module node
180 :type node: nodes.Module
181 """
182 # Skip privacy check in heuristic mode - we can't determine without paths
183 # This fallback mode is rarely used (only when linter has no file info)
184 pass # pragma: no cover
186 def _get_module_path(self) -> Path | None:
187 """Get the current module's file path from the linter.
189 :returns: Path to the module file, or None if not available
190 :rtype: Path | None
191 """
192 # Defensive check: ensure linter has current_file attribute
193 # (version compatibility)
194 if hasattr(self.linter, "current_file") and self.linter.current_file:
195 return Path(self.linter.current_file).resolve()
196 return None
198 def _get_project_root(self, module_path: Path) -> Path | None:
199 """Find the project root directory by looking for common project markers.
201 :param module_path: Path to the current module
202 :type module_path: Path
203 :returns: Project root path, or module's parent directory as fallback
204 :rtype: Path | None
205 """
206 # Common project markers that indicate a project root
207 project_markers = [
208 "pyproject.toml",
209 "setup.py",
210 "setup.cfg",
211 ".git",
212 "requirements.txt",
213 "Pipfile",
214 "poetry.lock",
215 ]
217 current = module_path.parent
218 while current != current.parent:
219 # Check if any project marker exists in current directory
220 if any((current / marker).exists() for marker in project_markers):
221 return current
222 current = current.parent
224 # Fallback: use the module's parent directory
225 # This handles cases where we're testing in isolated directories
226 return module_path.parent