1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/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)
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, ())
# pylint: disable = too-many-try-statements
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
|