1919 from pylint .lint import PyLinter
2020
2121
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+
2239class MatchStatementChecker (BaseChecker ):
2340 name = "match_statements"
2441 msgs = {
@@ -46,6 +63,19 @@ class MatchStatementChecker(BaseChecker):
4663 "Emitted when there is more than one sub-pattern for a specific "
4764 "attribute in a class pattern." ,
4865 ),
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+ ),
4979 }
5080
5181 @only_required_for_messages ("invalid-match-args-definition" )
@@ -86,6 +116,26 @@ def visit_match(self, node: nodes.Match) -> None:
86116 confidence = HIGH ,
87117 )
88118
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+
89139 @staticmethod
90140 def get_match_args_for_class (node : nodes .NodeNG ) -> list [str ] | None :
91141 """Infer __match_args__ from class name."""
@@ -95,6 +145,8 @@ def get_match_args_for_class(node: nodes.NodeNG) -> list[str] | None:
95145 try :
96146 match_args = inferred .getattr ("__match_args__" )
97147 except astroid .exceptions .NotFoundError :
148+ if inferred .qname () in MATCH_CLASS_SELF_NAMES :
149+ return ["<self>" ]
98150 return None
99151
100152 match match_args :
@@ -124,6 +176,7 @@ def check_duplicate_sub_patterns(
124176 attrs .add (name )
125177
126178 @only_required_for_messages (
179+ "match-class-positional-attributes" ,
127180 "multiple-class-sub-patterns" ,
128181 "too-many-positional-sub-patterns" ,
129182 )
@@ -144,6 +197,22 @@ def visit_matchclass(self, node: nodes.MatchClass) -> None:
144197 )
145198 return
146199
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+
147216 for i in range (len (node .patterns )):
148217 name = match_args [i ]
149218 self .check_duplicate_sub_patterns (name , node , attrs = attrs , dups = dups )
0 commit comments