@@ -702,6 +702,12 @@ def _has_same_layout_slots(
702702 "Used when a class tries to extend an inherited Enum class. "
703703 "Doing so will raise a TypeError at runtime." ,
704704 ),
705+ "E0245" : (
706+ "No such name %r in __slots__" ,
707+ "declare-non-slot" ,
708+ "Raised when a type annotation on a class is absent from the list of names in __slots__, "
709+ "and __slots__ does not contain a __dict__ entry." ,
710+ ),
705711 "R0202" : (
706712 "Consider using a decorator instead of calling classmethod" ,
707713 "no-classmethod-decorator" ,
@@ -870,6 +876,7 @@ def _dummy_rgx(self) -> Pattern[str]:
870876 "invalid-enum-extension" ,
871877 "subclassed-final-class" ,
872878 "implicit-flag-alias" ,
879+ "declare-non-slot" ,
873880 )
874881 def visit_classdef (self , node : nodes .ClassDef ) -> None :
875882 """Init visit variable _accessed."""
@@ -878,6 +885,50 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
878885 self ._check_proper_bases (node )
879886 self ._check_typing_final (node )
880887 self ._check_consistent_mro (node )
888+ self ._check_declare_non_slot (node )
889+
890+ def _check_declare_non_slot (self , node : nodes .ClassDef ) -> None :
891+ if not self ._has_valid_slots (node ):
892+ return
893+
894+ slot_names = self ._get_classdef_slots_names (node )
895+
896+ # Stop if empty __slots__ in the class body, this likely indicates that
897+ # this class takes part in multiple inheritance with other slotted classes.
898+ if not slot_names :
899+ return
900+
901+ # Stop if we find __dict__, since this means attributes can be set
902+ # dynamically
903+ if "__dict__" in slot_names :
904+ return
905+
906+ for base in node .bases :
907+ ancestor = safe_infer (base )
908+ if not isinstance (ancestor , nodes .ClassDef ):
909+ continue
910+ # if any base doesn't have __slots__, attributes can be set dynamically, so stop
911+ if not self ._has_valid_slots (ancestor ):
912+ return
913+ for slot_name in self ._get_classdef_slots_names (ancestor ):
914+ if slot_name == "__dict__" :
915+ return
916+ slot_names .append (slot_name )
917+
918+ # Every class in bases has __slots__, our __slots__ is non-empty and there is no __dict__
919+
920+ for child in node .body :
921+ if isinstance (child , nodes .AnnAssign ):
922+ if child .value is not None :
923+ continue
924+ if isinstance (child .target , nodes .AssignName ):
925+ if child .target .name not in slot_names :
926+ self .add_message (
927+ "declare-non-slot" ,
928+ args = child .target .name ,
929+ node = child .target ,
930+ confidence = INFERENCE ,
931+ )
881932
882933 def _check_consistent_mro (self , node : nodes .ClassDef ) -> None :
883934 """Detect that a class has a consistent mro or duplicate bases."""
@@ -1482,6 +1533,24 @@ def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool:
14821533
14831534 return "functools" in dict (import_node .names )
14841535
1536+ def _has_valid_slots (self , node : nodes .ClassDef ) -> bool :
1537+ if "__slots__" not in node .locals :
1538+ return False
1539+
1540+ for slots in node .ilookup ("__slots__" ):
1541+ # check if __slots__ is a valid type
1542+ if isinstance (slots , util .UninferableBase ):
1543+ return False
1544+ if not is_iterable (slots ) and not is_comprehension (slots ):
1545+ return False
1546+ if isinstance (slots , nodes .Const ):
1547+ return False
1548+ if not hasattr (slots , "itered" ):
1549+ # we can't obtain the values, maybe a .deque?
1550+ return False
1551+
1552+ return True
1553+
14851554 def _check_slots (self , node : nodes .ClassDef ) -> None :
14861555 if "__slots__" not in node .locals :
14871556 return
@@ -1515,13 +1584,19 @@ def _check_slots(self, node: nodes.ClassDef) -> None:
15151584 continue
15161585 self ._check_redefined_slots (node , slots , values )
15171586
1518- def _check_redefined_slots (
1519- self ,
1520- node : nodes .ClassDef ,
1521- slots_node : nodes .NodeNG ,
1522- slots_list : list [nodes .NodeNG ],
1523- ) -> None :
1524- """Check if `node` redefines a slot which is defined in an ancestor class."""
1587+ def _get_classdef_slots_names (self , node : nodes .ClassDef ) -> list [str ]:
1588+
1589+ slots_names = []
1590+ for slots in node .ilookup ("__slots__" ):
1591+ if isinstance (slots , nodes .Dict ):
1592+ values = [item [0 ] for item in slots .items ]
1593+ else :
1594+ values = slots .itered ()
1595+ slots_names .extend (self ._get_slots_names (values ))
1596+
1597+ return slots_names
1598+
1599+ def _get_slots_names (self , slots_list : list [nodes .NodeNG ]) -> list [str ]:
15251600 slots_names : list [str ] = []
15261601 for slot in slots_list :
15271602 if isinstance (slot , nodes .Const ):
@@ -1531,6 +1606,16 @@ def _check_redefined_slots(
15311606 inferred_slot_value = getattr (inferred_slot , "value" , None )
15321607 if isinstance (inferred_slot_value , str ):
15331608 slots_names .append (inferred_slot_value )
1609+ return slots_names
1610+
1611+ def _check_redefined_slots (
1612+ self ,
1613+ node : nodes .ClassDef ,
1614+ slots_node : nodes .NodeNG ,
1615+ slots_list : list [nodes .NodeNG ],
1616+ ) -> None :
1617+ """Check if `node` redefines a slot which is defined in an ancestor class."""
1618+ slots_names : list [str ] = self ._get_slots_names (slots_list )
15341619
15351620 # Slots of all parent classes
15361621 ancestors_slots_names = {
0 commit comments