diff options
| author | cpopa <devnull@localhost> | 2014-01-09 14:50:10 +0200 |
|---|---|---|
| committer | cpopa <devnull@localhost> | 2014-01-09 14:50:10 +0200 |
| commit | 79ded0d5f158f19569b53f02e53f9eb8b5be1e3d (patch) | |
| tree | 5f3a3e058493641d6e1edf9df334acef64209086 /checkers | |
| parent | 95ec900f33bae5dcf71a00cdf4ca4ab7392d604f (diff) | |
| parent | 9e0dfe50de779a4fdf4d2abe80587f541c7cb71c (diff) | |
| download | pylint-79ded0d5f158f19569b53f02e53f9eb8b5be1e3d.tar.gz | |
Merge with default.
Diffstat (limited to 'checkers')
| -rw-r--r-- | checkers/__init__.py | 4 | ||||
| -rw-r--r-- | checkers/base.py | 22 | ||||
| -rw-r--r-- | checkers/classes.py | 8 | ||||
| -rw-r--r-- | checkers/design_analysis.py | 49 | ||||
| -rw-r--r-- | checkers/exceptions.py | 42 | ||||
| -rw-r--r-- | checkers/format.py | 418 | ||||
| -rw-r--r-- | checkers/imports.py | 11 | ||||
| -rw-r--r-- | checkers/logging.py | 34 | ||||
| -rw-r--r-- | checkers/newstyle.py | 5 | ||||
| -rw-r--r-- | checkers/raw_metrics.py | 4 | ||||
| -rw-r--r-- | checkers/similar.py | 18 | ||||
| -rw-r--r-- | checkers/stdlib.py | 2 | ||||
| -rw-r--r-- | checkers/strings.py | 6 | ||||
| -rw-r--r-- | checkers/typecheck.py | 4 | ||||
| -rw-r--r-- | checkers/utils.py | 68 | ||||
| -rw-r--r-- | checkers/variables.py | 64 |
16 files changed, 486 insertions, 273 deletions
diff --git a/checkers/__init__.py b/checkers/__init__.py index 27dc364..1d0aa42 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -99,10 +99,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): """add a message of a given type""" self.linter.add_message(msg_id, line, node, args) - def package_dir(self): - """return the base directory for the analysed package""" - return dirname(self.linter.base_file) - # dummy methods implementing the IChecker interface def open(self): diff --git a/checkers/base.py b/checkers/base.py index 6aae709..5df0477 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -52,6 +52,10 @@ NO_REQUIRED_DOC_RGX = re.compile('__.*__') REVERSED_METHODS = (('__getitem__', '__len__'), ('__reversed__', )) +BAD_FUNCTIONS = ['map', 'filter', 'apply'] +if sys.version_info < (3, 0): + BAD_FUNCTIONS.append('input') + del re def in_loop(node): @@ -83,12 +87,19 @@ def _loop_exits_early(loop): # in orelse. for child in loop.body: if isinstance(child, loop_nodes): + # break statement may be in orelse of child loop. + for orelse in (child.orelse or ()): + for _ in orelse.nodes_of_class(astroid.Break, skip_klass=loop_nodes): + return True continue for _ in child.nodes_of_class(astroid.Break, skip_klass=loop_nodes): return True return False - +if sys.version_info < (3, 0): + PROPERTY_CLASSES = set(('__builtin__.property', 'abc.abstractproperty')) +else: + PROPERTY_CLASSES = set(('builtins.property', 'abc.abstractproperty')) def _determine_function_name_type(node): """Determine the name type whose regex the a function's name should match. @@ -109,8 +120,7 @@ def _determine_function_name_type(node): (isinstance(decorator, astroid.Getattr) and decorator.attrname == 'abstractproperty')): infered = safe_infer(decorator) - if (infered and - infered.qname() in ('__builtin__.property', 'abc.abstractproperty')): + if infered and infered.qname() in PROPERTY_CLASSES: return 'attr' # If the function is decorated using the prop_method.{setter,getter} # form, treat it like an attribute as well. @@ -251,7 +261,7 @@ class BasicErrorChecker(_BasicChecker): not (v is None or (isinstance(v, astroid.Const) and v.value is None) or (isinstance(v, astroid.Name) and v.name == 'None') - ) ]: + )]: self.add_message('return-in-init', node=node) elif node.is_generator(): # make sure we don't mix non-None returns and yields @@ -432,13 +442,13 @@ functions, methods 'comma'} ), ('bad-functions', - {'default' : ('map', 'filter', 'apply', 'input'), + {'default' : BAD_FUNCTIONS, 'type' :'csv', 'metavar' : '<builtin function names>', 'help' : 'List of builtins function names that should not be ' 'used, separated by a comma'} ), ) - reports = ( ('RP0101', 'Statistics by type', report_by_type_stats), ) + reports = (('RP0101', 'Statistics by type', report_by_type_stats),) def __init__(self, linter): _BasicChecker.__init__(self, linter) diff --git a/checkers/classes.py b/checkers/classes.py index fd76146..fc09021 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -183,7 +183,7 @@ class ClassChecker(BaseChecker): options = (('ignore-iface-methods', {'default' : (#zope interface 'isImplementedBy', 'deferred', 'extends', 'names', - 'namesAndDescriptions', 'queryDescriptionFor', 'getBases', + 'namesAndDescriptions', 'queryDescriptionFor', 'getBases', 'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue', 'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue', 'isImplementedByInstancesOf', @@ -355,10 +355,10 @@ a metaclass class method.'} positional = sum(1 for arg in node.args.args if arg.name != 'self') if positional < 3 and not node.args.vararg: self.add_message('bad-context-manager', - node=node) + node=node) elif positional > 3: self.add_message('bad-context-manager', - node=node) + node=node) def leave_function(self, node): """on method node, check if this method couldn't be a function diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py index f3b5882..11defbf 100644 --- a/checkers/design_analysis.py +++ b/checkers/design_analysis.py @@ -26,42 +26,6 @@ import re # regexp for ignored argument name IGNORED_ARGUMENT_NAMES = re.compile('_.*') -SPECIAL_METHODS = [('Context manager', set(('__enter__', - '__exit__',))), - ('Container', set(('__len__', - '__getitem__',))), - ('Mutable container', set(('__setitem__', - '__delitem__',))), - ] - -class SpecialMethodChecker(object): - """A functor that checks for consistency of a set of special methods""" - def __init__(self, methods_found, on_error): - """Stores the set of __x__ method names that were found in the - class and a callable that will be called with args to R0024 if - the check fails - """ - self.methods_found = methods_found - self.on_error = on_error - - def __call__(self, methods_required, protocol): - """Checks the set of method names given to __init__ against the set - required. - - If they are all present, returns true. - If they are all absent, returns false. - If some are present, reports the error and returns false. - """ - required_methods_found = methods_required & self.methods_found - if required_methods_found == methods_required: - return True - if required_methods_found: - required_methods_missing = methods_required - self.methods_found - self.on_error((protocol, - ', '.join(sorted(required_methods_found)), - ', '.join(sorted(required_methods_missing)))) - return False - def class_is_abstract(klass): """return true if the given class node should be considered as an abstract @@ -121,10 +85,6 @@ MSGS = { 'R0923': ('Interface not implemented', 'interface-not-implemented', 'Used when an interface class is not implemented anywhere.'), - 'R0924': ('Badly implemented %s, implements %s but not %s', - 'incomplete-protocol', - 'A class implements some of the special methods for a particular \ - protocol, but not all of them') } @@ -289,13 +249,6 @@ class MisdesignChecker(BaseChecker): # stop here for exception, metaclass and interface classes if node.type != 'class': return - # Does the class implement special methods consitently? - # If so, don't enforce minimum public methods. - check_special = SpecialMethodChecker( - special_methods, lambda args: self.add_message('R0924', node=node, args=args)) - protocols = [check_special(pmethods, pname) for pname, pmethods in SPECIAL_METHODS] - if True in protocols: - return # Does the class contain more than 5 public methods ? if nb_public_methods < self.config.min_public_methods: self.add_message('R0903', node=node, @@ -379,7 +332,7 @@ class MisdesignChecker(BaseChecker): """increments the branches counter""" branches = 1 # don't double count If nodes coming from some 'elif' - if node.orelse and (len(node.orelse)>1 or + if node.orelse and (len(node.orelse) > 1 or not isinstance(node.orelse[0], If)): branches += 1 self._inc_branch(branches) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 8ac00a5..5bb07ac 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -25,6 +25,23 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import is_empty, is_raising, check_messages from pylint.interfaces import IAstroidChecker +def infer_bases(klass): + """ Fully infer the bases of the klass node. + + This doesn't use .ancestors(), because we need + the non-inferable nodes (YES nodes), + which can't be retrieved from .ancestors() + """ + for base in klass.bases: + try: + inferit = base.infer().next() + except astroid.InferenceError: + continue + if inferit is YES: + yield inferit + else: + for base in infer_bases(inferit): + yield base OVERGENERAL_EXCEPTIONS = ('Exception',) @@ -50,7 +67,7 @@ MSGS = { 'catching-non-exception', 'Used when a class which doesn\'t inherit from \ BaseException is used as an exception in an except clause.'), - + 'W0701': ('Raising a string exception', 'raising-string', 'Used when a string exception is raised.'), @@ -141,10 +158,10 @@ class ExceptionsChecker(BaseChecker): isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple, astroid.Module, astroid.Function)): self.add_message('E0702', node=node, args=expr.name) - elif ( (isinstance(expr, astroid.Name) and expr.name == 'NotImplemented') - or (isinstance(expr, astroid.CallFunc) and - isinstance(expr.func, astroid.Name) and - expr.func.name == 'NotImplemented') ): + elif ((isinstance(expr, astroid.Name) and expr.name == 'NotImplemented') + or (isinstance(expr, astroid.CallFunc) and + isinstance(expr.func, astroid.Name) and + expr.func.name == 'NotImplemented')): self.add_message('E0711', node=node) elif isinstance(expr, astroid.BinOp) and expr.op == '%': self.add_message('W0701', node=node) @@ -211,12 +228,19 @@ class ExceptionsChecker(BaseChecker): and exc.root().name == EXCEPTIONS_MODULE and nb_handlers == 1 and not is_raising(handler.body)): self.add_message('W0703', args=exc.name, node=handler.type) - + if (not inherit_from_std_ex(exc) and exc.root().name != BUILTINS_NAME): - self.add_message('catching-non-exception', - node=handler.type, - args=(exc.name, )) + # try to see if the exception is based on a C based + # exception, by infering all the base classes and + # looking for inference errors + bases = infer_bases(exc) + fully_infered = all(inferit is not YES + for inferit in bases) + if fully_infered: + self.add_message('catching-non-exception', + node=handler.type, + args=(exc.name, )) exceptions_classes += excs diff --git a/checkers/format.py b/checkers/format.py index 1cf0edc..aab2320 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -21,19 +21,43 @@ http://www.python.org/doc/essays/styleguide.html Some parts of the process_token method is based from The Tab Nanny std module. """ -import re, sys +import keyword +import sys import tokenize + if not hasattr(tokenize, 'NL'): raise ValueError("tokenize.NL doesn't exist -- tokenize module too old") -from logilab.common.textutils import pretty_match from astroid import nodes -from pylint.interfaces import ITokenChecker, IAstroidChecker +from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker from pylint.checkers import BaseTokenChecker from pylint.checkers.utils import check_messages from pylint.utils import WarningScope, OPTION_RGX +_KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not', + 'raise', 'return', 'while', 'yield'] +if sys.version_info < (3, 0): + _KEYWORD_TOKENS.append('print') + +_SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=', + '+=', '-=', '*=', '**=', '/=', '//=', '&=', '|=', '^=', + '%=', '>>=', '<<='] +_OPENING_BRACKETS = ['(', '[', '{'] +_CLOSING_BRACKETS = [')', ']', '}'] + +_EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT]) + +# Whitespace checking policy constants +_MUST = 0 +_MUST_NOT = 1 +_IGNORE = 2 + +# Whitespace checking config constants +_DICT_SEPARATOR = 'dict-separator' +_TRAILING_COMMA = 'trailing-comma' +_NO_SPACE_CHECK_CHOICES = [_TRAILING_COMMA, _DICT_SEPARATOR] + MSGS = { 'C0301': ('Line too long (%s/%s)', 'line-too-long', @@ -64,22 +88,20 @@ MSGS = { 'multiple-statements', 'Used when more than on statement are found on the same line.', {'scope': WarningScope.NODE}), - 'C0322': ('Operator not preceded by a space\n%s', - 'no-space-before-operator', - 'Used when one of the following operator (!= | <= | == | >= | < ' - '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.', - {'scope': WarningScope.NODE}), - 'C0323': ('Operator not followed by a space\n%s', - 'no-space-after-operator', - 'Used when one of the following operator (!= | <= | == | >= | < ' - '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.', - {'scope': WarningScope.NODE}), - 'C0324': ('Comma not followed by a space\n%s', - 'no-space-after-comma', - 'Used when a comma (",") is not followed by a space.', - {'scope': WarningScope.NODE}), + 'C0325' : ('Unnecessary parens after %r keyword', + 'superfluous-parens', + 'Used when a single item in parentheses follows an if, for, or ' + 'other keyword.'), + 'C0326': ('%s space %s %s %s\n%s', + 'bad-whitespace', + ('Used when a wrong number of spaces is used around an operator, ' + 'bracket or block opener.'), + {'old_names': [('C0323', 'no-space-after-operator'), + ('C0324', 'no-space-after-comma'), + ('C0322', 'no-space-before-operator')]}) } + if sys.version_info < (3, 0): MSGS.update({ @@ -99,74 +121,21 @@ if sys.version_info < (3, 0): {'scope': WarningScope.NODE}), }) -# simple quoted string rgx -SQSTRING_RGX = r'"([^"\\]|\\.)*?"' -# simple apostrophed rgx -SASTRING_RGX = r"'([^'\\]|\\.)*?'" -# triple quoted string rgx -TQSTRING_RGX = r'"""([^"]|("(?!"")))*?(""")' -# triple apostrophe'd string rgx -TASTRING_RGX = r"'''([^']|('(?!'')))*?(''')" - -# finally, the string regular expression -STRING_RGX = re.compile('(%s)|(%s)|(%s)|(%s)' % (TQSTRING_RGX, TASTRING_RGX, - SQSTRING_RGX, SASTRING_RGX), - re.MULTILINE|re.DOTALL) - -COMMENT_RGX = re.compile("#.*$", re.M) - -OPERATORS = r'!=|<=|==|>=|<|>|=|\+=|-=|\*=|/=|%' - -OP_RGX_MATCH_1 = r'[^(]*(?<!\s|\^|<|>|=|\+|-|\*|/|!|%%|&|\|)(%s).*' % OPERATORS -OP_RGX_SEARCH_1 = r'(?<!\s|\^|<|>|=|\+|-|\*|/|!|%%|&|\|)(%s)' % OPERATORS -OP_RGX_MATCH_2 = r'[^(]*(%s)(?!\s|=|>|<).*' % OPERATORS -OP_RGX_SEARCH_2 = r'(%s)(?!\s|=|>)' % OPERATORS +def _underline_token(token): + length = token[3][1] - token[2][1] + offset = token[2][1] + return token[4] + (' ' * offset) + ('^' * length) -BAD_CONSTRUCT_RGXS = ( - (re.compile(OP_RGX_MATCH_1, re.M), - re.compile(OP_RGX_SEARCH_1, re.M), - 'C0322'), - - (re.compile(OP_RGX_MATCH_2, re.M), - re.compile(OP_RGX_SEARCH_2, re.M), - 'C0323'), - - (re.compile(r'.*,[^(\s|\]|}|\))].*', re.M), - re.compile(r',[^\s)]', re.M), - 'C0324'), - ) - - -def get_string_coords(line): - """return a list of string positions (tuple (start, end)) in the line - """ - result = [] - for match in re.finditer(STRING_RGX, line): - result.append( (match.start(), match.end()) ) - return result - -def in_coords(match, string_coords): - """return true if the match is in the string coord""" - mstart = match.start() - for start, end in string_coords: - if mstart >= start and mstart < end: - return True - return False - -def check_line(line): - """check a line for a bad construction - if it founds one, return a message describing the problem - else return None - """ - cleanstr = COMMENT_RGX.sub('', STRING_RGX.sub('', line)) - for rgx_match, rgx_search, msg_id in BAD_CONSTRUCT_RGXS: - if rgx_match.match(cleanstr): - string_positions = get_string_coords(line) - for match in re.finditer(rgx_search, line): - if not in_coords(match, string_positions): - return msg_id, pretty_match(match, line.rstrip()) +def _column_distance(token1, token2): + if token1 == token2: + return 0 + if token2[3] < token1[3]: + token1, token2 = token2, token1 + if token1[3][0] != token2[2][0]: + return None + return token2[2][1] - token1[3][1] class FormatChecker(BaseTokenChecker): @@ -177,7 +146,7 @@ class FormatChecker(BaseTokenChecker): * use of <> instead of != """ - __implements__ = (ITokenChecker, IAstroidChecker) + __implements__ = (ITokenChecker, IAstroidChecker, IRawChecker) # configuration section name name = 'format' @@ -193,6 +162,16 @@ class FormatChecker(BaseTokenChecker): 'default': r'^\s*(# )?<?https?://\S+>?$', 'help': ('Regexp for a line that is allowed to be longer than ' 'the limit.')}), + ('single-line-if-stmt', + {'default': False, 'type' : 'yn', 'metavar' : '<y_or_n>', + 'help' : ('Allow the body of an if to be on the same ' + 'line as the test if there is no else.')}), + ('no-space-check', + {'default': ','.join(_NO_SPACE_CHECK_CHOICES), + 'type': 'multiple_choice', + 'choices': _NO_SPACE_CHECK_CHOICES, + 'help': ('List of optional constructs for which whitespace ' + 'checking is disabled')}), ('max-module-lines', {'default' : 1000, 'type' : 'int', 'metavar' : '<int>', 'help': 'Maximum number of lines in a module'} @@ -213,6 +192,223 @@ class FormatChecker(BaseTokenChecker): self._lines[line_num] = line.split('\n')[0] self.check_lines(line, line_num) + def process_module(self, module): + self._keywords_with_parens = set() + for node in module.body: + if (isinstance(node, nodes.From) and node.modname == '__future__' + and any(name == 'print_function' for name, _ in node.names)): + self._keywords_with_parens.add('print') + + def _check_keyword_parentheses(self, tokens, start): + """Check that there are not unnecessary parens after a keyword. + + Parens are unnecessary if there is exactly one balanced outer pair on a + line, and it is followed by a colon, and contains no commas (i.e. is not a + tuple). + + Args: + tokens: list of Tokens; the entire list of Tokens. + start: int; the position of the keyword in the token list. + """ + # If the next token is not a paren, we're fine. + if tokens[start+1][1] != '(': + return + + found_and_or = False + depth = 0 + keyword_token = tokens[start][1] + line_num = tokens[start][2][0] + + for i in xrange(start, len(tokens) - 1): + token = tokens[i] + + # If we hit a newline, then assume any parens were for continuation. + if token[0] == tokenize.NL: + return + + if token[1] == '(': + depth += 1 + elif token[1] == ')': + depth -= 1 + if not depth: + # ')' can't happen after if (foo), since it would be a syntax error. + if (tokens[i+1][1] in (':', ')', ']', '}', 'in') or + tokens[i+1][0] in (tokenize.NEWLINE, tokenize.ENDMARKER, + tokenize.COMMENT)): + # The empty tuple () is always accepted. + if i == start + 2: + return + if keyword_token == 'not': + if not found_and_or: + self.add_message('C0325', line=line_num, + args=keyword_token) + elif keyword_token in ('return', 'yield'): + self.add_message('C0325', line=line_num, + args=keyword_token) + elif keyword_token not in self._keywords_with_parens: + if not (tokens[i+1][1] == 'in' and found_and_or): + self.add_message('C0325', line=line_num, + args=keyword_token) + return + elif depth == 1: + # This is a tuple, which is always acceptable. + if token[1] == ',': + return + # 'and' and 'or' are the only boolean operators with lower precedence + # than 'not', so parens are only required when they are found. + elif token[1] in ('and', 'or'): + found_and_or = True + # A yield inside an expression must always be in parentheses, + # quit early without error. + elif token[1] == 'yield': + return + # A generator expression always has a 'for' token in it, and + # the 'for' token is only legal inside parens when it is in a + # generator expression. The parens are necessary here, so bail + # without an error. + elif token[1] == 'for': + return + + def _opening_bracket(self, tokens, i): + self._bracket_stack.append(tokens[i][1]) + # Special case: ignore slices + if tokens[i][1] == '[' and tokens[i+1][1] == ':': + return + + if (i > 0 and (tokens[i-1][0] == tokenize.NAME and + not (keyword.iskeyword(tokens[i-1][1])) + or tokens[i-1][1] in _CLOSING_BRACKETS)): + self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT)) + else: + self._check_space(tokens, i, (_IGNORE, _MUST_NOT)) + + def _closing_bracket(self, tokens, i): + self._bracket_stack.pop() + # Special case: ignore slices + if tokens[i-1][1] == ':' and tokens[i][1] == ']': + return + policy_before = _MUST_NOT + if tokens[i][1] in _CLOSING_BRACKETS and tokens[i-1][1] == ',': + if _TRAILING_COMMA in self.config.no_space_check: + policy_before = _IGNORE + + self._check_space(tokens, i, (policy_before, _IGNORE)) + + def _check_equals_spacing(self, tokens, i): + """Check the spacing of a single equals sign.""" + if self._inside_brackets('(') or self._inside_brackets('lambda'): + self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT)) + else: + self._check_space(tokens, i, (_MUST, _MUST)) + + def _open_lambda(self, tokens, i): # pylint:disable=unused-argument + self._bracket_stack.append('lambda') + + def _handle_colon(self, tokens, i): + # Special case: ignore slices + if self._inside_brackets('['): + return + if (self._inside_brackets('{') and + _DICT_SEPARATOR in self.config.no_space_check): + policy = (_IGNORE, _IGNORE) + else: + policy = (_MUST_NOT, _MUST) + self._check_space(tokens, i, policy) + + if self._inside_brackets('lambda'): + self._bracket_stack.pop() + + def _handle_comma(self, tokens, i): + # Only require a following whitespace if this is + # not a hanging comma before a closing bracket. + if tokens[i+1][1] in _CLOSING_BRACKETS: + self._check_space(tokens, i, (_MUST_NOT, _IGNORE)) + else: + self._check_space(tokens, i, (_MUST_NOT, _MUST)) + + def _check_surrounded_by_space(self, tokens, i): + """Check that a binary operator is surrounded by exactly one space.""" + self._check_space(tokens, i, (_MUST, _MUST)) + + def _check_space(self, tokens, i, policies): + def _policy_string(policy): + if policy == _MUST: + return 'Exactly one', 'required' + else: + return 'No', 'allowed' + + def _name_construct(token): + if tokens[i][1] == ',': + return 'comma' + elif tokens[i][1] == ':': + return ':' + elif tokens[i][1] in '()[]{}': + return 'bracket' + elif tokens[i][1] in ('<', '>', '<=', '>=', '!='): + return 'comparison' + else: + if self._inside_brackets('('): + return 'keyword argument assignment' + else: + return 'assignment' + + good_space = [True, True] + pairs = [(tokens[i-1], tokens[i]), (tokens[i], tokens[i+1])] + + for other_idx, (policy, token_pair) in enumerate(zip(policies, pairs)): + if token_pair[other_idx][0] in _EOL or policy == _IGNORE: + continue + + distance = _column_distance(*token_pair) + if distance is None: + continue + good_space[other_idx] = ( + (policy == _MUST and distance == 1) or + (policy == _MUST_NOT and distance == 0)) + + warnings = [] + if not any(good_space) and policies[0] == policies[1]: + warnings.append((policies[0], 'around')) + else: + for ok, policy, position in zip(good_space, policies, ('before', 'after')): + if not ok: + warnings.append((policy, position)) + for policy, position in warnings: + construct = _name_construct(tokens[i]) + count, state = _policy_string(policy) + self.add_message('C0326', line=tokens[i][2][0], + args=(count, state, position, construct, + _underline_token(tokens[i]))) + + def _inside_brackets(self, left): + return self._bracket_stack[-1] == left + + def _prepare_token_dispatcher(self): + raw = [ + (_KEYWORD_TOKENS, + self._check_keyword_parentheses), + + (_OPENING_BRACKETS, self._opening_bracket), + + (_CLOSING_BRACKETS, self._closing_bracket), + + (['='], self._check_equals_spacing), + + (_SPACED_OPERATORS, self._check_surrounded_by_space), + + ([','], self._handle_comma), + + ([':'], self._handle_colon), + + (['lambda'], self._open_lambda), + ] + + dispatch = {} + for tokens, handler in raw: + for token in tokens: + dispatch[token] = handler + return dispatch + def process_tokens(self, tokens): """process tokens and search for : @@ -222,6 +418,7 @@ class FormatChecker(BaseTokenChecker): _ optionally bad construct (if given, bad_construct must be a compiled regular expression). """ + self._bracket_stack = [None] indent = tokenize.INDENT dedent = tokenize.DEDENT newline = tokenize.NEWLINE @@ -233,7 +430,8 @@ class FormatChecker(BaseTokenChecker): self._lines = {} self._visited_lines = {} new_line_delay = False - for (tok_type, token, start, _, line) in tokens: + token_handlers = self._prepare_token_dispatcher() + for idx, (tok_type, token, start, _, line) in enumerate(tokens): if new_line_delay: new_line_delay = False self.new_line(tok_type, line, line_num, junk) @@ -292,11 +490,18 @@ class FormatChecker(BaseTokenChecker): check_equal = 0 self.check_indent_level(line, indents[-1], line_num) + try: + handler = token_handlers[token] + except KeyError: + pass + else: + handler(tokens, idx) + line_num -= 1 # to be ok with "wc -l" if line_num > self.config.max_module_lines: self.add_message('C0302', args=line_num, line=1) - @check_messages('C0321' ,'C03232', 'C0323', 'C0324') + @check_messages('C0321', 'C03232', 'C0323', 'C0324') def visit_default(self, node): """check the node line number and check it if not yet done""" if not node.is_statement: @@ -307,16 +512,19 @@ class FormatChecker(BaseTokenChecker): if prev_sibl is not None: prev_line = prev_sibl.fromlineno else: - prev_line = node.parent.statement().fromlineno + # The line on which a finally: occurs in a try/finally + # is not directly represented in the AST. We infer it + # by taking the last line of the body and adding 1, which + # should be the line of finally: + if (isinstance(node.parent, nodes.TryFinally) + and node in node.parent.finalbody): + prev_line = node.parent.body[0].tolineno + 1 + else: + prev_line = node.parent.statement().fromlineno line = node.fromlineno assert line, node if prev_line == line and self._visited_lines.get(line) != 2: - # py2.5 try: except: finally: - if not (isinstance(node, nodes.TryExcept) - and isinstance(node.parent, nodes.TryFinally) - and node.fromlineno == node.parent.fromlineno): - self.add_message('C0321', node=node) - self._visited_lines[line] = 2 + self._check_multi_statement_line(node, line) return if line in self._visited_lines: return @@ -332,13 +540,23 @@ class FormatChecker(BaseTokenChecker): lines.append(self._lines[line].rstrip()) except KeyError: lines.append('') - try: - msg_def = check_line('\n'.join(lines)) - if msg_def: - self.add_message(msg_def[0], node=node, args=msg_def[1]) - except KeyError: - # FIXME: internal error ! - pass + + def _check_multi_statement_line(self, node, line): + """Check for lines containing multiple statements.""" + # Do not warn about multiple nested context managers + # in with statements. + if isinstance(node, nodes.With): + return + # For try... except... finally..., the two nodes + # appear to be on the same line due to how the AST is built. + if (isinstance(node, nodes.TryExcept) and + isinstance(node.parent, nodes.TryFinally)): + return + if (isinstance(node.parent, nodes.If) and not node.parent.orelse + and self.config.single_line_if_stmt): + return + self.add_message('C0321', node=node) + self._visited_lines[line] = 2 @check_messages('W0333') def visit_backquote(self, node): @@ -388,7 +606,7 @@ class FormatChecker(BaseTokenChecker): self.add_message('W0312', args=args, line=line_num) return level suppl += string[0] - string = string [1:] + string = string[1:] if level != expected or suppl: i_type = 'spaces' if indent[0] == '\t': diff --git a/checkers/imports.py b/checkers/imports.py index 1dd7787..b0a9872 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -93,7 +93,7 @@ def dependencies_graph(filename, dep_info): """write dependencies as a dot (graphviz) file """ done = {} - printer = DotBackend(filename[:-4], rankdir = "LR") + printer = DotBackend(filename[:-4], rankdir='LR') printer.emit('URL="." node[shape="box"]') for modname, dependencies in sorted(dep_info.iteritems()): done[modname] = 1 @@ -301,11 +301,10 @@ given file (report RP0402 must not be disabled)'} importedmodname, set()) if not context_name in importedmodnames: importedmodnames.add(context_name) - if is_standard_module(importedmodname, (self.package_dir(),)): - # update import graph - mgraph = self.import_graph.setdefault(context_name, set()) - if not importedmodname in mgraph: - mgraph.add(importedmodname) + # update import graph + mgraph = self.import_graph.setdefault(context_name, set()) + if not importedmodname in mgraph: + mgraph.add(importedmodname) def _check_deprecated_module(self, node, mod_path): """check if the module is deprecated""" diff --git a/checkers/logging.py b/checkers/logging.py index 6986ca4..d1f9d36 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -91,7 +91,7 @@ class LoggingChecker(checkers.BaseChecker): and ancestor.parent.name == 'logging')))] except astroid.exceptions.InferenceError: return - if (node.func.expr.name != self._logging_name and not logger_class): + if node.func.expr.name != self._logging_name and not logger_class: return self._check_convenience_methods(node) self._check_log_methods(node) @@ -129,7 +129,7 @@ class LoggingChecker(checkers.BaseChecker): node: AST node to be checked. format_arg: Index of the format string in the node arguments. """ - num_args = self._count_supplied_tokens(node.args[format_arg + 1:]) + num_args = _count_supplied_tokens(node.args[format_arg + 1:]) if not num_args: # If no args were supplied, then all format strings are valid - # don't check any further. @@ -147,9 +147,10 @@ class LoggingChecker(checkers.BaseChecker): # Keyword checking on logging strings is complicated by # special keywords - out of scope. return - except utils.UnsupportedFormatCharacter, e: - c = format_string[e.index] - self.add_message('E1200', node=node, args=(c, ord(c), e.index)) + except utils.UnsupportedFormatCharacter, ex: + char = format_string[ex.index] + self.add_message('E1200', node=node, + args=(char, ord(char), ex.index)) return except utils.IncompleteFormatString: self.add_message('E1201', node=node) @@ -159,20 +160,21 @@ class LoggingChecker(checkers.BaseChecker): elif num_args < required_num_args: self.add_message('E1206', node=node) - def _count_supplied_tokens(self, args): - """Counts the number of tokens in an args list. - The Python log functions allow for special keyword arguments: func, - exc_info and extra. To handle these cases correctly, we only count - arguments that aren't keywords. +def _count_supplied_tokens(args): + """Counts the number of tokens in an args list. - Args: - args: List of AST nodes that are arguments for a log format string. + The Python log functions allow for special keyword arguments: func, + exc_info and extra. To handle these cases correctly, we only count + arguments that aren't keywords. - Returns: - Number of AST nodes that aren't keywords. - """ - return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) + Args: + args: List of AST nodes that are arguments for a log format string. + + Returns: + Number of AST nodes that aren't keywords. + """ + return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) def register(linter): diff --git a/checkers/newstyle.py b/checkers/newstyle.py index 9832195..ff9bbc2 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -1,4 +1,4 @@ -# Copyright (c) 2005-2006 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2005-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -37,7 +37,8 @@ MSGS = { 'E1004': ('Missing argument to super()', 'missing-super-argument', 'Used when the super builtin didn\'t receive an \ - argument on Python 2'), + argument on Python 2', + {'maxversion': (3, 0)}), 'W1001': ('Use of "property" on an old style class', 'property-on-old-class', 'Used when PyLint detect the use of the builtin "property" \ diff --git a/checkers/raw_metrics.py b/checkers/raw_metrics.py index a8e4367..23e45b0 100644 --- a/checkers/raw_metrics.py +++ b/checkers/raw_metrics.py @@ -68,11 +68,11 @@ class RawMetricsChecker(BaseTokenChecker): # configuration section name name = 'metrics' # configuration options - options = ( ) + options = () # messages msgs = {} # reports - reports = ( ('RP0701', 'Raw metrics', report_raw_stats), ) + reports = (('RP0701', 'Raw metrics', report_raw_stats),) def __init__(self, linter): BaseTokenChecker.__init__(self, linter) diff --git a/checkers/similar.py b/checkers/similar.py index 26b3725..8d755fa 100644 --- a/checkers/similar.py +++ b/checkers/similar.py @@ -63,15 +63,15 @@ class Similar(object): duplicate = no_duplicates.setdefault(num, []) for couples in duplicate: if (lineset1, idx1) in couples or (lineset2, idx2) in couples: - couples.add( (lineset1, idx1) ) - couples.add( (lineset2, idx2) ) + couples.add((lineset1, idx1)) + couples.add((lineset2, idx2)) break else: - duplicate.append( set([(lineset1, idx1), (lineset2, idx2)]) ) + duplicate.append(set([(lineset1, idx1), (lineset2, idx2)])) sims = [] for num, ensembles in no_duplicates.iteritems(): for couples in ensembles: - sims.append( (num, couples) ) + sims.append((num, couples)) sims.sort() sims.reverse() return sims @@ -104,7 +104,7 @@ class Similar(object): while index1 < len(lineset1): skip = 1 num = 0 - for index2 in find( lineset1[index1] ): + for index2 in find(lineset1[index1]): non_blank = 0 for num, ((_, line1), (_, line2)) in enumerate( izip(lines1(index1), lines2(index2))): @@ -210,7 +210,7 @@ class LineSet(object): index = {} for line_no, line in enumerate(self._stripped_lines): if line: - index.setdefault(line, []).append( line_no ) + index.setdefault(line, []).append(line_no) return index @@ -260,7 +260,7 @@ class SimilarChecker(BaseChecker, Similar): ), ) # reports - reports = ( ('RP0801', 'Duplication', report_similarities), ) + reports = (('RP0801', 'Duplication', report_similarities),) def __init__(self, linter=None): BaseChecker.__init__(self, linter) @@ -349,9 +349,9 @@ def Run(argv=None): usage() elif opt in ('-i', '--ignore-comments'): ignore_comments = True - elif opt in ('--ignore-docstrings'): + elif opt in ('--ignore-docstrings',): ignore_docstrings = True - elif opt in ('--ignore-imports'): + elif opt in ('--ignore-imports',): ignore_imports = True if not args: usage(1) diff --git a/checkers/stdlib.py b/checkers/stdlib.py index 07e1fbe..b63760c 100644 --- a/checkers/stdlib.py +++ b/checkers/stdlib.py @@ -21,7 +21,7 @@ import sys import astroid from pylint.interfaces import IAstroidChecker -from pylint.checkers import BaseChecker, BaseTokenChecker +from pylint.checkers import BaseChecker from pylint.checkers import utils _VALID_OPEN_MODE_REGEX = r'^(r?U|[rwa]\+?b?)$' diff --git a/checkers/strings.py b/checkers/strings.py index 42563da..c6bf960 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -66,11 +66,11 @@ MSGS = { 'E1305': ("Too many arguments for format string", "too-many-format-args", "Used when a format string that uses unnamed conversion \ - specifiers is given too few arguments."), + specifiers is given too many arguments."), 'E1306': ("Not enough arguments for format string", "too-few-format-args", "Used when a format string that uses unnamed conversion \ - specifiers is given too many arguments"), + specifiers is given too few arguments"), } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, @@ -233,7 +233,7 @@ class StringConstantChecker(BaseTokenChecker): if c in '\'\"': quote_char = c break - prefix = token[:i].lower() # markers like u, b, r. + prefix = token[:i].lower() # markers like u, b, r. after_prefix = token[i:] if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char: string_body = after_prefix[3:-3] diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 6988359..2e3785e 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -1,4 +1,4 @@ -# Copyright (c) 2006-2010 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2006-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -292,7 +292,7 @@ accessed. Python regular expressions are accepted.'} # Built-in functions have no argument information. return - if len( called.argnames() ) != len( set( called.argnames() ) ): + if len(called.argnames()) != len(set(called.argnames())): # Duplicate parameter name (see E9801). We can't really make sense # of the function call in this case, so just return. return diff --git a/checkers/utils.py b/checkers/utils.py index 5e028f2..7387711 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -60,7 +60,7 @@ def clobber_in_except(node): (False, None) otherwise. """ if isinstance(node, astroid.AssAttr): - return (True, (node.attrname, 'object %r' % (node.expr.name,))) + return (True, (node.attrname, 'object %r' % (node.expr.as_string(),))) elif isinstance(node, astroid.AssName): name = node.name if is_builtin(name): @@ -69,8 +69,9 @@ def clobber_in_except(node): scope, stmts = node.lookup(name) if (stmts and not isinstance(stmts[0].ass_type(), - (astroid.Assign, astroid.AugAssign, astroid.ExceptHandler))): - return (True, (name, 'outer scope (line %s)' % (stmts[0].fromlineno,))) + (astroid.Assign, astroid.AugAssign, + astroid.ExceptHandler))): + return (True, (name, 'outer scope (line %s)' % stmts[0].fromlineno)) return (False, None) @@ -153,8 +154,10 @@ def is_defined_before(var_node): elif isinstance(_node, astroid.With): for expr, vars in _node.items: if expr.parent_of(var_node): - break - if vars and vars.name == varname: + break + if (vars and + isinstance(vars, astroid.AssName) and + vars.name == varname): return True elif isinstance(_node, (astroid.Lambda, astroid.Function)): if _node.args.is_argument(varname): @@ -162,6 +165,11 @@ def is_defined_before(var_node): if getattr(_node, 'name', None) == varname: return True break + elif isinstance(_node, astroid.ExceptHandler): + if isinstance(_node.name, astroid.AssName): + ass_node = _node.name + if ass_node.name == varname: + return True _node = _node.parent # possibly multiple statements on the same line using semi colon separator stmt = var_node.statement() @@ -171,7 +179,7 @@ def is_defined_before(var_node): for ass_node in _node.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - for imp_node in _node.nodes_of_class( (astroid.From, astroid.Import)): + for imp_node in _node.nodes_of_class((astroid.From, astroid.Import)): if varname in [name[1] or name[0] for name in imp_node.names]: return True _node = _node.previous_sibling() @@ -296,52 +304,52 @@ def parse_format_string(format_string): return (i, format_string[i]) i = 0 while i < len(format_string): - c = format_string[i] - if c == '%': - i, c = next_char(i) + char = format_string[i] + if char == '%': + i, char = next_char(i) # Parse the mapping key (optional). key = None - if c == '(': + if char == '(': depth = 1 - i, c = next_char(i) + i, char = next_char(i) key_start = i while depth != 0: - if c == '(': + if char == '(': depth += 1 - elif c == ')': + elif char == ')': depth -= 1 - i, c = next_char(i) + i, char = next_char(i) key_end = i - 1 key = format_string[key_start:key_end] # Parse the conversion flags (optional). - while c in '#0- +': - i, c = next_char(i) + while char in '#0- +': + i, char = next_char(i) # Parse the minimum field width (optional). - if c == '*': + if char == '*': num_args += 1 - i, c = next_char(i) + i, char = next_char(i) else: - while c in string.digits: - i, c = next_char(i) + while char in string.digits: + i, char = next_char(i) # Parse the precision (optional). - if c == '.': - i, c = next_char(i) - if c == '*': + if char == '.': + i, char = next_char(i) + if char == '*': num_args += 1 - i, c = next_char(i) + i, char = next_char(i) else: - while c in string.digits: - i, c = next_char(i) + while char in string.digits: + i, char = next_char(i) # Parse the length modifier (optional). - if c in 'hlL': - i, c = next_char(i) + if char in 'hlL': + i, char = next_char(i) # Parse the conversion type (mandatory). - if c not in 'diouxXeEfFgGcrs%': + if char not in 'diouxXeEfFgGcrs%': raise UnsupportedFormatCharacter(i) if key: keys.add(key) - elif c != '%': + elif char != '%': num_args += 1 i += 1 return keys, num_args diff --git a/checkers/variables.py b/checkers/variables.py index 0d35884..90b7fe7 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -51,6 +51,20 @@ def overridden_method(klass, name): return meth_node return None +def _get_unpacking_extra_info(node, infered): + """return extra information to add to the message for unpacking-non-sequence + and unbalanced-tuple-unpacking errors + """ + more = '' + infered_module = infered.root().name + if node.root().name == infered_module: + if node.lineno == infered.lineno: + more = ' %s' % infered.as_string() + elif infered.lineno: + more = ' defined at line %s' % infered.lineno + elif infered.lineno: + more = ' defined at line %s of %s' % (infered.lineno, infered_module) + return more MSGS = { 'E0601': ('Using variable %r before assignment', @@ -120,13 +134,12 @@ MSGS = { the loop.'), 'W0632': ('Possible unbalanced tuple unpacking with ' - 'sequence at line %s: ' + 'sequence%s: ' 'left side has %d label(s), right side has %d value(s)', 'unbalanced-tuple-unpacking', 'Used when there is an unbalanced tuple unpacking in assignment'), - 'W0633': ('Attempting to unpack a non-sequence with ' - 'non-sequence at line %s', + 'W0633': ('Attempting to unpack a non-sequence%s', 'unpacking-non-sequence', 'Used when something which is not ' 'a sequence is used in an unpack assignment'), @@ -556,7 +569,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' """ if not isinstance(node.targets[0], (astroid.Tuple, astroid.List)): return - + targets = node.targets[0].itered() if any(not isinstance(target_node, astroid.AssName) for target_node in targets): @@ -572,41 +585,30 @@ builtins. Remember that you should avoid to define new builtins when possible.' """ Check for unbalanced tuple unpacking and unpacking non sequences. """ - if isinstance(infered, (astroid.Tuple, astroid.List)): + if infered is astroid.YES: + return + if isinstance(infered, (astroid.Tuple, astroid.List)): + # attempt to check unpacking is properly balanced values = infered.itered() if len(targets) != len(values): - if node.root().name == infered.root().name: - location = infered.lineno or 'unknown' - else: - location = '%s (%s)' % (infered.lineno or 'unknown', - infered.root().name) - - self.add_message('unbalanced-tuple-unpacking', - node=node, - args=(location, - len(targets), + self.add_message('unbalanced-tuple-unpacking', node=node, + args=(_get_unpacking_extra_info(node, infered), + len(targets), len(values))) - else: - if infered is astroid.YES: - return - + # attempt to check unpacking may be possible (ie RHS is iterable) + elif isinstance(infered, astroid.Instance): for meth in ('__iter__', '__getitem__'): try: infered.getattr(meth) + break except astroid.NotFoundError: continue - else: - break - else: - if node.root().name == infered.root().name: - location = infered.lineno or 'unknown' - else: - location = '%s (%s)' % (infered.lineno or 'unknown', - infered.root().name) - - self.add_message('unpacking-non-sequence', - node=node, - args=(location, )) + else: + self.add_message('unpacking-non-sequence', node=node, + args=(_get_unpacking_extra_info(node, infered),)) + else: + self.add_message('unpacking-non-sequence', node=node, + args=(_get_unpacking_extra_info(node, infered),)) def _check_module_attrs(self, node, module, module_names): |
