diff options
Diffstat (limited to 'venv/Lib/site-packages/pylint/checkers/design_analysis.py')
-rw-r--r-- | venv/Lib/site-packages/pylint/checkers/design_analysis.py | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/venv/Lib/site-packages/pylint/checkers/design_analysis.py b/venv/Lib/site-packages/pylint/checkers/design_analysis.py new file mode 100644 index 0000000..50d8eaa --- /dev/null +++ b/venv/Lib/site-packages/pylint/checkers/design_analysis.py @@ -0,0 +1,496 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2006, 2009-2010, 2012-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr> +# Copyright (c) 2012, 2014 Google, Inc. +# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com> +# Copyright (c) 2014 Arun Persaud <arun@nubati.net> +# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro> +# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com> +# Copyright (c) 2017 ahirnish <ahirnish@gmail.com> +# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com> +# Copyright (c) 2018 Mark Miller <725mrm@gmail.com> +# Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk> +# Copyright (c) 2018 Ville Skyttä <ville.skytta@upcloud.com> +# Copyright (c) 2018 Jakub Wilk <jwilk@jwilk.net> + +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/master/COPYING + +"""check for signs of poor design""" + +import re +from collections import defaultdict + +import astroid +from astroid import BoolOp, If, decorators + +from pylint import utils +from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages +from pylint.interfaces import IAstroidChecker + +MSGS = { + "R0901": ( + "Too many ancestors (%s/%s)", + "too-many-ancestors", + "Used when class has too many parent classes, try to reduce " + "this to get a simpler (and so easier to use) class.", + ), + "R0902": ( + "Too many instance attributes (%s/%s)", + "too-many-instance-attributes", + "Used when class has too many instance attributes, try to reduce " + "this to get a simpler (and so easier to use) class.", + ), + "R0903": ( + "Too few public methods (%s/%s)", + "too-few-public-methods", + "Used when class has too few public methods, so be sure it's " + "really worth it.", + ), + "R0904": ( + "Too many public methods (%s/%s)", + "too-many-public-methods", + "Used when class has too many public methods, try to reduce " + "this to get a simpler (and so easier to use) class.", + ), + "R0911": ( + "Too many return statements (%s/%s)", + "too-many-return-statements", + "Used when a function or method has too many return statement, " + "making it hard to follow.", + ), + "R0912": ( + "Too many branches (%s/%s)", + "too-many-branches", + "Used when a function or method has too many branches, " + "making it hard to follow.", + ), + "R0913": ( + "Too many arguments (%s/%s)", + "too-many-arguments", + "Used when a function or method takes too many arguments.", + ), + "R0914": ( + "Too many local variables (%s/%s)", + "too-many-locals", + "Used when a function or method has too many local variables.", + ), + "R0915": ( + "Too many statements (%s/%s)", + "too-many-statements", + "Used when a function or method has too many statements. You " + "should then split it in smaller functions / methods.", + ), + "R0916": ( + "Too many boolean expressions in if statement (%s/%s)", + "too-many-boolean-expressions", + "Used when an if statement contains too many boolean expressions.", + ), +} +SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") +DATACLASSES_DECORATORS = frozenset({"dataclass", "attrs"}) +DATACLASS_IMPORT = "dataclasses" +TYPING_NAMEDTUPLE = "typing.NamedTuple" + + +def _is_exempt_from_public_methods(node: astroid.ClassDef) -> bool: + """Check if a class is exempt from too-few-public-methods""" + + # If it's a typing.Namedtuple or an Enum + for ancestor in node.ancestors(): + if ancestor.name == "Enum" and ancestor.root().name == "enum": + return True + if ancestor.qname() == TYPING_NAMEDTUPLE: + return True + + # Or if it's a dataclass + if not node.decorators: + return False + + root_locals = set(node.root().locals) + for decorator in node.decorators.nodes: + if isinstance(decorator, astroid.Call): + decorator = decorator.func + if not isinstance(decorator, (astroid.Name, astroid.Attribute)): + continue + if isinstance(decorator, astroid.Name): + name = decorator.name + else: + name = decorator.attrname + if name in DATACLASSES_DECORATORS and ( + root_locals.intersection(DATACLASSES_DECORATORS) + or DATACLASS_IMPORT in root_locals + ): + return True + return False + + +def _count_boolean_expressions(bool_op): + """Counts the number of boolean expressions in BoolOp `bool_op` (recursive) + + example: a and (b or c or (d and e)) ==> 5 boolean expressions + """ + nb_bool_expr = 0 + for bool_expr in bool_op.get_children(): + if isinstance(bool_expr, BoolOp): + nb_bool_expr += _count_boolean_expressions(bool_expr) + else: + nb_bool_expr += 1 + return nb_bool_expr + + +def _count_methods_in_class(node): + 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 + + +class MisdesignChecker(BaseChecker): + """checks for sign of poor/misdesign: + * number of methods, attributes, local variables... + * size, complexity of functions, methods + """ + + __implements__ = (IAstroidChecker,) + + # configuration section name + name = "design" + # messages + msgs = MSGS + priority = -2 + # configuration options + options = ( + ( + "max-args", + { + "default": 5, + "type": "int", + "metavar": "<int>", + "help": "Maximum number of arguments for function / method.", + }, + ), + ( + "max-locals", + { + "default": 15, + "type": "int", + "metavar": "<int>", + "help": "Maximum number of locals for function / method body.", + }, + ), + ( + "max-returns", + { + "default": 6, + "type": "int", + "metavar": "<int>", + "help": "Maximum number of return / yield for function / " + "method body.", + }, + ), + ( + "max-branches", + { + "default": 12, + "type": "int", + "metavar": "<int>", + "help": "Maximum number of branch for function / method body.", + }, + ), + ( + "max-statements", + { + "default": 50, + "type": "int", + "metavar": "<int>", + "help": "Maximum number of statements in function / method " "body.", + }, + ), + ( + "max-parents", + { + "default": 7, + "type": "int", + "metavar": "<num>", + "help": "Maximum number of parents for a class (see R0901).", + }, + ), + ( + "max-attributes", + { + "default": 7, + "type": "int", + "metavar": "<num>", + "help": "Maximum number of attributes for a class \ +(see R0902).", + }, + ), + ( + "min-public-methods", + { + "default": 2, + "type": "int", + "metavar": "<num>", + "help": "Minimum number of public methods for a class \ +(see R0903).", + }, + ), + ( + "max-public-methods", + { + "default": 20, + "type": "int", + "metavar": "<num>", + "help": "Maximum number of public methods for a class \ +(see R0904).", + }, + ), + ( + "max-bool-expr", + { + "default": 5, + "type": "int", + "metavar": "<num>", + "help": "Maximum number of boolean expressions in an if " + "statement (see R0916).", + }, + ), + ) + + def __init__(self, linter=None): + BaseChecker.__init__(self, linter) + self.stats = None + self._returns = None + self._branches = None + self._stmts = None + + def open(self): + """initialize visit variables""" + self.stats = self.linter.add_stats() + self._returns = [] + self._branches = defaultdict(int) + self._stmts = [] + + def _inc_all_stmts(self, amount): + for i in range(len(self._stmts)): + self._stmts[i] += amount + + @decorators.cachedproperty + def _ignored_argument_names(self): + return utils.get_global_option(self, "ignored-argument-names", default=None) + + @check_messages( + "too-many-ancestors", + "too-many-instance-attributes", + "too-few-public-methods", + "too-many-public-methods", + ) + def visit_classdef(self, node): + """check size of inheritance hierarchy and number of instance attributes + """ + nb_parents = len(list(node.ancestors())) + if nb_parents > self.config.max_parents: + self.add_message( + "too-many-ancestors", + node=node, + args=(nb_parents, self.config.max_parents), + ) + + if len(node.instance_attrs) > self.config.max_attributes: + self.add_message( + "too-many-instance-attributes", + node=node, + args=(len(node.instance_attrs), self.config.max_attributes), + ) + + @check_messages("too-few-public-methods", "too-many-public-methods") + def leave_classdef(self, node): + """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 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), + ) + + @check_messages( + "too-many-return-statements", + "too-many-branches", + "too-many-arguments", + "too-many-locals", + "too-many-statements", + "keyword-arg-before-vararg", + ) + def visit_functiondef(self, node): + """check function name, docstring, arguments, redefinition, + variable names, max locals + """ + # init branch and returns counters + self._returns.append(0) + # check number of arguments + args = node.args.args + ignored_argument_names = self._ignored_argument_names + if args is not None: + ignored_args_num = 0 + if ignored_argument_names: + ignored_args_num = sum( + 1 for arg in args if ignored_argument_names.match(arg.name) + ) + + argnum = len(args) - ignored_args_num + if argnum > self.config.max_args: + self.add_message( + "too-many-arguments", + node=node, + args=(len(args), self.config.max_args), + ) + else: + ignored_args_num = 0 + # check number of local variables + locnum = len(node.locals) - ignored_args_num + if locnum > self.config.max_locals: + self.add_message( + "too-many-locals", node=node, args=(locnum, self.config.max_locals) + ) + # init new statements counter + self._stmts.append(1) + + visit_asyncfunctiondef = visit_functiondef + + @check_messages( + "too-many-return-statements", + "too-many-branches", + "too-many-arguments", + "too-many-locals", + "too-many-statements", + ) + def leave_functiondef(self, node): + """most of the work is done here on close: + checks for max returns, branch, return in __init__ + """ + returns = self._returns.pop() + if returns > self.config.max_returns: + self.add_message( + "too-many-return-statements", + node=node, + args=(returns, self.config.max_returns), + ) + branches = self._branches[node] + if branches > self.config.max_branches: + self.add_message( + "too-many-branches", + node=node, + args=(branches, self.config.max_branches), + ) + # check number of statements + stmts = self._stmts.pop() + if stmts > self.config.max_statements: + self.add_message( + "too-many-statements", + node=node, + args=(stmts, self.config.max_statements), + ) + + leave_asyncfunctiondef = leave_functiondef + + def visit_return(self, _): + """count number of returns""" + if not self._returns: + return # return outside function, reported by the base checker + self._returns[-1] += 1 + + def visit_default(self, node): + """default visit method -> increments the statements counter if + necessary + """ + if node.is_statement: + self._inc_all_stmts(1) + + def visit_tryexcept(self, node): + """increments the branches counter""" + branches = len(node.handlers) + if node.orelse: + branches += 1 + self._inc_branch(node, branches) + self._inc_all_stmts(branches) + + def visit_tryfinally(self, node): + """increments the branches counter""" + self._inc_branch(node, 2) + self._inc_all_stmts(2) + + @check_messages("too-many-boolean-expressions") + def visit_if(self, node): + """increments the branches counter and checks boolean expressions""" + self._check_boolean_expressions(node) + branches = 1 + # don't double count If nodes coming from some 'elif' + if node.orelse and (len(node.orelse) > 1 or not isinstance(node.orelse[0], If)): + branches += 1 + self._inc_branch(node, branches) + self._inc_all_stmts(branches) + + def _check_boolean_expressions(self, node): + """Go through "if" node `node` and counts its boolean expressions + + if the "if" node test is a BoolOp node + """ + condition = node.test + if not isinstance(condition, BoolOp): + return + nb_bool_expr = _count_boolean_expressions(condition) + if nb_bool_expr > self.config.max_bool_expr: + self.add_message( + "too-many-boolean-expressions", + node=condition, + args=(nb_bool_expr, self.config.max_bool_expr), + ) + + def visit_while(self, node): + """increments the branches counter""" + branches = 1 + if node.orelse: + branches += 1 + self._inc_branch(node, branches) + + visit_for = visit_while + + def _inc_branch(self, node, branchesnum=1): + """increments the branches counter""" + self._branches[node.scope()] += branchesnum + + +def register(linter): + """required method to auto register this checker """ + linter.register_checker(MisdesignChecker(linter)) |