19
19
from pylint .lint import PyLinter
20
20
21
21
22
+ # List of builtin classes which match self
23
+ # https://docs.python.org/3/reference/compound_stmts.html#class-patterns
24
+ MATCH_CLASS_SELF_NAMES = {
25
+ "builtins.bool" ,
26
+ "builtins.bytearray" ,
27
+ "builtins.bytes" ,
28
+ "builtins.dict" ,
29
+ "builtins.float" ,
30
+ "builtins.frozenset" ,
31
+ "builtins.int" ,
32
+ "builtins.list" ,
33
+ "builtins.set" ,
34
+ "builtins.str" ,
35
+ "builtins.tuple" ,
36
+ }
37
+
38
+
22
39
class MatchStatementChecker (BaseChecker ):
23
40
name = "match_statements"
24
41
msgs = {
@@ -46,6 +63,19 @@ class MatchStatementChecker(BaseChecker):
46
63
"Emitted when there is more than one sub-pattern for a specific "
47
64
"attribute in a class pattern." ,
48
65
),
66
+ "R1905" : (
67
+ "Use '%s' instead" ,
68
+ "match-class-bind-self" ,
69
+ "Match class patterns are faster if the name binding happens "
70
+ "for the whole pattern and any lookup for `__match_args__` "
71
+ "can be avoided." ,
72
+ ),
73
+ "R1906" : (
74
+ "Use keyword attributes instead of positional ones" ,
75
+ "match-class-positional-attributes" ,
76
+ "Keyword attributes are more explicit and slightly faster "
77
+ "since CPython can skip the `__match_args__` lookup." ,
78
+ ),
49
79
}
50
80
51
81
@only_required_for_messages ("invalid-match-args-definition" )
@@ -86,6 +116,26 @@ def visit_match(self, node: nodes.Match) -> None:
86
116
confidence = HIGH ,
87
117
)
88
118
119
+ @only_required_for_messages ("match-class-bind-self" )
120
+ def visit_matchas (self , node : nodes .MatchAs ) -> None :
121
+ match node :
122
+ case nodes .MatchAs (
123
+ parent = nodes .MatchClass (cls = nodes .Name () as cls_name ),
124
+ name = nodes .AssignName (name = name ),
125
+ pattern = None ,
126
+ ):
127
+ inferred = safe_infer (cls_name )
128
+ if (
129
+ isinstance (inferred , nodes .ClassDef )
130
+ and inferred .qname () in MATCH_CLASS_SELF_NAMES
131
+ ):
132
+ self .add_message (
133
+ "match-class-bind-self" ,
134
+ node = node ,
135
+ args = (f"{ cls_name .name } () as { name } " ,),
136
+ confidence = HIGH ,
137
+ )
138
+
89
139
@staticmethod
90
140
def get_match_args_for_class (node : nodes .NodeNG ) -> list [str ] | None :
91
141
"""Infer __match_args__ from class name."""
@@ -95,6 +145,8 @@ def get_match_args_for_class(node: nodes.NodeNG) -> list[str] | None:
95
145
try :
96
146
match_args = inferred .getattr ("__match_args__" )
97
147
except astroid .exceptions .NotFoundError :
148
+ if inferred .qname () in MATCH_CLASS_SELF_NAMES :
149
+ return ["<self>" ]
98
150
return None
99
151
100
152
match match_args :
@@ -124,13 +176,27 @@ def check_duplicate_sub_patterns(
124
176
attrs .add (name )
125
177
126
178
@only_required_for_messages (
179
+ "match-class-positional-attributes" ,
127
180
"multiple-class-sub-patterns" ,
128
181
"too-many-positional-sub-patterns" ,
129
182
)
130
183
def visit_matchclass (self , node : nodes .MatchClass ) -> None :
131
184
attrs : set [str ] = set ()
132
185
dups : set [str ] = set ()
133
186
187
+ if node .patterns :
188
+ if isinstance (node , nodes .MatchClass ) and isinstance (node .cls , nodes .Name ):
189
+ inferred = safe_infer (node .cls )
190
+ if not (
191
+ isinstance (inferred , nodes .ClassDef )
192
+ and inferred .qname () in MATCH_CLASS_SELF_NAMES
193
+ ):
194
+ self .add_message (
195
+ "match-class-positional-attributes" ,
196
+ node = node ,
197
+ confidence = HIGH ,
198
+ )
199
+
134
200
if (
135
201
node .patterns
136
202
and (match_args := self .get_match_args_for_class (node .cls )) is not None
0 commit comments