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() as %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 (%s)" ,
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 , patterns = [_]),
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 = (cls_name .name , 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,6 +176,7 @@ 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
)
@@ -144,6 +197,22 @@ def visit_matchclass(self, node: nodes.MatchClass) -> None:
144
197
)
145
198
return
146
199
200
+ inferred = safe_infer (node .cls )
201
+ if not (
202
+ isinstance (inferred , nodes .ClassDef )
203
+ and (
204
+ inferred .qname () in MATCH_CLASS_SELF_NAMES
205
+ or "tuple" in inferred .basenames
206
+ )
207
+ ):
208
+ attributes = [f"'{ attr } '" for attr in match_args [: len (node .patterns )]]
209
+ self .add_message (
210
+ "match-class-positional-attributes" ,
211
+ node = node ,
212
+ args = (", " .join (attributes ),),
213
+ confidence = INFERENCE ,
214
+ )
215
+
147
216
for i in range (len (node .patterns )):
148
217
name = match_args [i ]
149
218
self .check_duplicate_sub_patterns (name , node , attrs = attrs , dups = dups )
0 commit comments