| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE |
| # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt |
| |
| from __future__ import annotations |
| |
| import sys |
| import traceback |
| from collections import defaultdict |
| from collections.abc import Sequence |
| from typing import TYPE_CHECKING, Callable |
| |
| from astroid import nodes |
| |
| if TYPE_CHECKING: |
| from pylint.checkers.base_checker import BaseChecker |
| from pylint.lint import PyLinter |
| |
| # Callable parameter type NodeNG not completely correct. |
| # Due to contravariance of Callable parameter types, |
| # it should be a Union of all NodeNG subclasses. |
| # However, since the methods are only retrieved with |
| # getattr(checker, member) and thus are inferred as Any, |
| # NodeNG will work too. |
| AstCallback = Callable[[nodes.NodeNG], None] |
| |
| |
| class ASTWalker: |
| def __init__(self, linter: PyLinter) -> None: |
| # callbacks per node types |
| self.nbstatements = 0 |
| self.visit_events: defaultdict[str, list[AstCallback]] = defaultdict(list) |
| self.leave_events: defaultdict[str, list[AstCallback]] = defaultdict(list) |
| self.linter = linter |
| self.exception_msg = False |
| |
| def _is_method_enabled(self, method: AstCallback) -> bool: |
| if not hasattr(method, "checks_msgs"): |
| return True |
| return any(self.linter.is_message_enabled(m) for m in method.checks_msgs) # type: ignore[attr-defined] |
| |
| def add_checker(self, checker: BaseChecker) -> None: |
| """Walk to the checker's dir and collect visit and leave methods.""" |
| vcids: set[str] = set() |
| lcids: set[str] = set() |
| visits = self.visit_events |
| leaves = self.leave_events |
| for member in dir(checker): |
| cid = member[6:] |
| if cid == "default": |
| continue |
| if member.startswith("visit_"): |
| v_meth = getattr(checker, member) |
| # don't use visit_methods with no activated message: |
| if self._is_method_enabled(v_meth): |
| visits[cid].append(v_meth) |
| vcids.add(cid) |
| elif member.startswith("leave_"): |
| l_meth = getattr(checker, member) |
| # don't use leave_methods with no activated message: |
| if self._is_method_enabled(l_meth): |
| leaves[cid].append(l_meth) |
| lcids.add(cid) |
| visit_default = getattr(checker, "visit_default", None) |
| if visit_default: |
| for cls in nodes.ALL_NODE_CLASSES: |
| cid = cls.__name__.lower() |
| if cid not in vcids: |
| visits[cid].append(visit_default) |
| # For now, we have no "leave_default" method in Pylint |
| |
| def walk(self, astroid: nodes.NodeNG) -> None: |
| """Call visit events of astroid checkers for the given node, recurse on |
| its children, then leave events. |
| """ |
| cid = astroid.__class__.__name__.lower() |
| |
| # Detect if the node is a new name for a deprecated alias. |
| # In this case, favour the methods for the deprecated |
| # alias if any, in order to maintain backwards |
| # compatibility. |
| visit_events: Sequence[AstCallback] = self.visit_events.get(cid, ()) |
| leave_events: Sequence[AstCallback] = self.leave_events.get(cid, ()) |
| |
| try: |
| if astroid.is_statement: |
| self.nbstatements += 1 |
| # generate events for this node on each checker |
| for callback in visit_events: |
| callback(astroid) |
| # recurse on children |
| for child in astroid.get_children(): |
| self.walk(child) |
| for callback in leave_events: |
| callback(astroid) |
| except Exception: |
| if self.exception_msg is False: |
| file = getattr(astroid.root(), "file", None) |
| print( |
| f"Exception on node {repr(astroid)} in file '{file}'", |
| file=sys.stderr, |
| ) |
| traceback.print_exc() |
| self.exception_msg = True |
| raise |