def visit_classdef(self, node: nodes.ClassDef) -> None: locals_and_methods = set(node.locals).union(x.name for x in node.mymethods()) if "__eq__" in locals_and_methods and "__hash__" not in locals_and_methods: self.add_message("eq-without-hash", node=node, confidence=interfaces.HIGH)
def _visit_class_functions(self, node: nodes.ClassDef, matches: list[TypeHintMatch]) -> None: for match in matches: for function_node in node.mymethods(): function_name: str | None = function_node.name if match.function_name == function_name: self._check_function(function_node, match)
def _count_methods_in_class(node: nodes.ClassDef) -> int: all_methods = sum(1 for method in node.methods() if not method.name.startswith("_")) # Special methods count towards the number of public methods, # but don't count towards there being too many methods. for method in node.mymethods(): if SPECIAL_OBJ.search(method.name) and method.name != "__init__": all_methods += 1 return all_methods
def leave_classdef(self, node: nodes.ClassDef) -> None: """check number of public methods""" my_methods = sum(1 for method in node.mymethods() if not method.name.startswith("_")) # Does the class contain less than n public methods ? # This checks only the methods defined in the current class, # since the user might not have control over the classes # from the ancestors. It avoids some false positives # for classes such as unittest.TestCase, which provides # a lot of assert methods. It doesn't make sense to warn # when the user subclasses TestCase to add his own tests. if my_methods > self.config.max_public_methods: self.add_message( "too-many-public-methods", node=node, args=(my_methods, self.config.max_public_methods), ) # Stop here if the class is excluded via configuration. if node.type == "class" and self._exclude_too_few_public_methods: for ancestor in node.ancestors(): if any( pattern.match(ancestor.qname()) for pattern in self._exclude_too_few_public_methods): return # Stop here for exception, metaclass, interface classes and other # classes for which we don't need to count the methods. if node.type != "class" or _is_exempt_from_public_methods(node): return # Does the class contain more than n public methods ? # This checks all the methods defined by ancestors and # by the current class. all_methods = _count_methods_in_class(node) if all_methods < self.config.min_public_methods: self.add_message( "too-few-public-methods", node=node, args=(all_methods, self.config.min_public_methods), )
def _process_class_member_and_attrs(cldef: ClassDef, pkgfiles: Set[str]) -> Set[str]: """ Determine the class attributes, methods and instance attributes defined by a class. The function will inspect and populate them from the parents while the parent is defined by a module file in the same mmpack package. This function is called recursively to populate a class. Args: cldef: the astroid node defining the class pkgfiles: set of files in the same mmpack package Returns: set of name corresponding to the class attributes and methods and instance attributes. """ syms = set() # add member and attributes from parent classes implemented in files of # the same package for base in cldef.ancestors(recurs=False): mod = base.root() if _is_module_packaged(mod, pkgfiles): syms.update(_process_class_member_and_attrs(base, pkgfiles)) # Add public class attributes for attr in cldef.locals: if (isinstance(cldef.locals[attr][-1], AssignName) and _is_public_sym(attr)): syms.add(attr) # Add public class methods and instance attributes syms.update({m.name for m in cldef.mymethods() if _is_public_sym(m.name)}) syms.update({a for a in cldef.instance_attrs if _is_public_sym(a)}) return syms
def infer_enum_class(node: nodes.ClassDef) -> nodes.ClassDef: """Specific inference for enums.""" for basename in (b for cls in node.mro() for b in cls.basenames): if node.root().name == "enum": # Skip if the class is directly from enum module. break dunder_members = {} target_names = set() for local, values in node.locals.items(): if any(not isinstance(value, nodes.AssignName) for value in values): continue stmt = values[0].statement(future=True) if isinstance(stmt, nodes.Assign): if isinstance(stmt.targets[0], nodes.Tuple): targets = stmt.targets[0].itered() else: targets = stmt.targets elif isinstance(stmt, nodes.AnnAssign): targets = [stmt.target] else: continue inferred_return_value = None if stmt.value is not None: if isinstance(stmt.value, nodes.Const): if isinstance(stmt.value.value, str): inferred_return_value = repr(stmt.value.value) else: inferred_return_value = stmt.value.value else: inferred_return_value = stmt.value.as_string() new_targets = [] for target in targets: if isinstance(target, nodes.Starred): continue target_names.add(target.name) # Replace all the assignments with our mocked class. classdef = dedent(""" class {name}({types}): @property def value(self): return {return_value} @property def name(self): return "{name}" """.format( name=target.name, types=", ".join(node.basenames), return_value=inferred_return_value, )) if "IntFlag" in basename: # Alright, we need to add some additional methods. # Unfortunately we still can't infer the resulting objects as # Enum members, but once we'll be able to do that, the following # should result in some nice symbolic execution classdef += INT_FLAG_ADDITION_METHODS.format( name=target.name) fake = AstroidBuilder( AstroidManager(), apply_transforms=False).string_build(classdef)[target.name] fake.parent = target.parent for method in node.mymethods(): fake.locals[method.name] = [method] new_targets.append(fake.instantiate_class()) dunder_members[local] = fake node.locals[local] = new_targets members = nodes.Dict(parent=node) members.postinit([(nodes.Const(k, parent=members), nodes.Name(v.name, parent=members)) for k, v in dunder_members.items()]) node.locals["__members__"] = [members] # The enum.Enum class itself defines two @DynamicClassAttribute data-descriptors # "name" and "value" (which we override in the mocked class for each enum member # above). When dealing with inference of an arbitrary instance of the enum # class, e.g. in a method defined in the class body like: # class SomeEnum(enum.Enum): # def method(self): # self.name # <- here # In the absence of an enum member called "name" or "value", these attributes # should resolve to the descriptor on that particular instance, i.e. enum member. # For "value", we have no idea what that should be, but for "name", we at least # know that it should be a string, so infer that as a guess. if "name" not in target_names: code = dedent(""" @property def name(self): return '' """) name_dynamicclassattr = AstroidBuilder( AstroidManager()).string_build(code)["name"] node.locals["name"] = [name_dynamicclassattr] break return node