summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcpopa <devnull@localhost>2014-04-25 11:20:38 +0300
committercpopa <devnull@localhost>2014-04-25 11:20:38 +0300
commit5500f23bf628980cf260923cc600cd3ac47ef63e (patch)
tree36166ec48629e0f47119273159c44e19b3bb4726
parent5d0874d07d58716158efae4bead9cbe18bd15c9c (diff)
parent5a130703e1a1f23bc5f80294527299675de1293e (diff)
downloadpylint-5500f23bf628980cf260923cc600cd3ac47ef63e.tar.gz
Merge with default.
-rw-r--r--ChangeLog11
-rw-r--r--__pkginfo__.py4
-rw-r--r--checkers/format.py416
-rw-r--r--checkers/typecheck.py3
-rw-r--r--checkers/variables.py28
-rw-r--r--lint.py2
-rw-r--r--test/input/func_bad_continuation.py222
-rw-r--r--test/input/func_format_py27.py3
-rw-r--r--test/input/func_format_py_27.py3
-rw-r--r--test/input/func_indent.py1
-rw-r--r--test/input/func_loopvar_in_closure.py114
-rw-r--r--test/input/func_loopvar_in_dict_comp_py27.py8
-rw-r--r--test/input/func_noerror_new_style_class_py_30.py2
-rw-r--r--test/input/func_typecheck_callfunc_assigment.py7
-rw-r--r--test/messages/func_bad_continuation.txt70
-rw-r--r--test/messages/func_break_or_return_in_try_finally.txt1
-rw-r--r--test/messages/func_format_py27.txt6
-rw-r--r--test/messages/func_indent.txt2
-rw-r--r--test/messages/func_loopvar_in_closure.txt8
-rw-r--r--test/messages/func_loopvar_in_dict_comp_py27.txt1
-rw-r--r--test/messages/func_typecheck_callfunc_assigment.txt1
-rw-r--r--test/unittest_checker_format.py2
-rw-r--r--test/unittest_lint.py4
23 files changed, 858 insertions, 61 deletions
diff --git a/ChangeLog b/ChangeLog
index e40459d..d57133d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,10 +2,21 @@ ChangeLog for Pylint
====================
--
+ * Add a new warning [bad-continuation] for badly indentend continued
+ lines.
+
+ * Emit [assignment-from-none] when the function contains bare returns.
+ Fixes BitBucket issue #191.
+
+ * Added a new warning for closing over variables that are
+ defined in loops. Fixes Bitbucket issue #176.
* Extend the checking for unbalanced-tuple-unpacking and
unpacking-non-sequence to instance attribute unpacking as well.
+ * Restore --init-hook, renamed accidentally into --init-hooks in 1.2.0
+ (#211)
+
* Add 'indexing-exception' warning, which detects that indexing
an exception occurs in Python 2 (behaviour removed in Python 3).
diff --git a/__pkginfo__.py b/__pkginfo__.py
index ea67fe6..d0a9aac 100644
--- a/__pkginfo__.py
+++ b/__pkginfo__.py
@@ -45,8 +45,8 @@ classifiers = ['Development Status :: 4 - Beta',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Debuggers',
'Topic :: Software Development :: Quality Assurance',
- 'Topic :: Software Development :: Testing',
- ]
+ 'Topic :: Software Development :: Testing'
+ ]
long_desc = """\
diff --git a/checkers/format.py b/checkers/format.py
index 7ab6c7e..c02986e 100644
--- a/checkers/format.py
+++ b/checkers/format.py
@@ -35,6 +35,7 @@ from pylint.checkers import BaseTokenChecker
from pylint.checkers.utils import check_messages
from pylint.utils import WarningScope, OPTION_RGX
+_CONTINUATION_BLOCK_OPENERS = ['elif', 'except', 'for', 'if', 'while', 'def', 'class']
_KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not',
'raise', 'return', 'while', 'yield']
if sys.version_info < (3, 0):
@@ -45,8 +46,10 @@ _SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=',
'%=', '>>=', '<<=']
_OPENING_BRACKETS = ['(', '[', '{']
_CLOSING_BRACKETS = [')', ']', '}']
+_TAB_LENGTH = 8
_EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT])
+_JUNK_TOKENS = (tokenize.COMMENT, tokenize.NL)
# Whitespace checking policy constants
_MUST = 0
@@ -77,6 +80,9 @@ MSGS = {
'bad-indentation',
'Used when an unexpected number of indentation\'s tabulations or '
'spaces has been found.'),
+ 'C0330': ('Wrong %s indentation%s.\n%s%s',
+ 'bad-continuation',
+ 'TODO'),
'W0312': ('Found indentation with %ss instead of %ss',
'mixed-indentation',
'Used when there are some mixed tabs and spaces in a module.'),
@@ -138,6 +144,261 @@ def _column_distance(token1, token2):
return token2[2][1] - token1[3][1]
+def _last_token_on_line_is(tokens, line_end, token):
+ return (
+ line_end > 0 and tokens.token(line_end-1) == token or
+ line_end > 1 and tokens.token(line_end-2) == token
+ and tokens.type(line_end-1) == tokenize.COMMENT)
+
+
+def _token_followed_by_eol(tokens, position):
+ return (tokens.type(position+1) == tokenize.NL or
+ tokens.type(position+1) == tokenize.COMMENT and
+ tokens.type(position+2) == tokenize.NL)
+
+
+def _get_indent_length(line):
+ """Return the length of the indentation on the given token's line."""
+ result = 0
+ for char in line:
+ if char == ' ':
+ result += 1
+ elif char == '\t':
+ result += _TAB_LENGTH
+ else:
+ break
+ return result
+
+
+def _get_indent_hint_line(bar_positions, bad_position):
+ """Return a line with |s for each of the positions in the given lists."""
+ if not bar_positions:
+ return ''
+ markers = [(pos, '|') for pos in bar_positions]
+ markers.append((bad_position, '^'))
+ markers.sort()
+ line = [' '] * (markers[-1][0] + 1)
+ for position, marker in markers:
+ line[position] = marker
+ return ''.join(line)
+
+
+class _ContinuedIndent(object):
+ __slots__ = ('valid_outdent_offsets',
+ 'valid_continuation_offsets',
+ 'context_type',
+ 'token',
+ 'position')
+
+ def __init__(self,
+ context_type,
+ token,
+ position,
+ valid_outdent_offsets,
+ valid_continuation_offsets):
+ self.valid_outdent_offsets = valid_outdent_offsets
+ self.valid_continuation_offsets = valid_continuation_offsets
+ self.context_type = context_type
+ self.position = position
+ self.token = token
+
+
+# The contexts for hanging indents.
+# A hanging indented dictionary value after :
+HANGING_DICT_VALUE = 'dict-value'
+# Hanging indentation in an expression.
+HANGING = 'hanging'
+# Hanging indentation in a block header.
+HANGING_BLOCK = 'hanging-block'
+# Continued indentation inside an expression.
+CONTINUED = 'continued'
+# Continued indentation in a block header.
+CONTINUED_BLOCK = 'continued-block'
+
+SINGLE_LINE = 'single'
+WITH_BODY = 'multi'
+
+_CONTINUATION_MSG_PARTS = {
+ HANGING_DICT_VALUE: ('hanging', ' in dict value'),
+ HANGING: ('hanging', ''),
+ HANGING_BLOCK: ('hanging', ' before block'),
+ CONTINUED: ('continued', ''),
+ CONTINUED_BLOCK: ('continued', ' before block'),
+}
+
+
+def _Offsets(*args):
+ """Valid indentation offsets for a continued line."""
+ return {a: None for a in args}
+
+
+def _BeforeBlockOffsets(single, with_body):
+ """Valid alternative indent offsets for continued lines before blocks.
+
+ :param single: Valid offset for statements on a single logical line.
+ :param with_body: Valid offset for statements on several lines.
+ """
+ return {single: SINGLE_LINE, with_body: WITH_BODY}
+
+
+class TokenWrapper(object):
+ """A wrapper for readable access to token information."""
+
+ def __init__(self, tokens):
+ self._tokens = tokens
+
+ def token(self, idx):
+ return self._tokens[idx][1]
+
+ def type(self, idx):
+ return self._tokens[idx][0]
+
+ def start_line(self, idx):
+ return self._tokens[idx][2][0]
+
+ def start_col(self, idx):
+ return self._tokens[idx][2][1]
+
+ def line(self, idx):
+ return self._tokens[idx][4]
+
+
+class ContinuedLineState(object):
+ """Tracker for continued indentation inside a logical line."""
+
+ def __init__(self, tokens, config):
+ self._line_start = -1
+ self._cont_stack = []
+ self._is_block_opener = False
+ self.retained_warnings = []
+ self._config = config
+ self._tokens = TokenWrapper(tokens)
+
+ @property
+ def has_content(self):
+ return bool(self._cont_stack)
+
+ @property
+ def _block_indent_size(self):
+ return len(self._config.indent_string.replace('\t', ' ' * _TAB_LENGTH))
+
+ @property
+ def _continuation_size(self):
+ return self._config.indent_after_paren
+
+ def handle_line_start(self, pos):
+ """Record the first non-junk token at the start of a line."""
+ if self._line_start > -1:
+ return
+ self._is_block_opener = self._tokens.token(pos) in _CONTINUATION_BLOCK_OPENERS
+ self._line_start = pos
+
+ def next_physical_line(self):
+ """Prepares the tracker for a new physical line (NL)."""
+ self._line_start = -1
+ self._is_block_opener = False
+
+ def next_logical_line(self):
+ """Prepares the tracker for a new logical line (NEWLINE).
+
+ A new logical line only starts with block indentation.
+ """
+ self.next_physical_line()
+ self.retained_warnings = []
+ self._cont_stack = []
+
+ def add_block_warning(self, token_position, state, valid_offsets):
+ self.retained_warnings.append((token_position, state, valid_offsets))
+
+ def get_valid_offsets(self, idx):
+ """"Returns the valid offsets for the token at the given position."""
+ # The closing brace on a dict or the 'for' in a dict comprehension may
+ # reset two indent levels because the dict value is ended implicitly
+ stack_top = -1
+ if self._tokens.token(idx) in ('}', 'for') and self._cont_stack[-1].token == ':':
+ stack_top = -2
+ indent = self._cont_stack[stack_top]
+ if self._tokens.token(idx) in _CLOSING_BRACKETS:
+ valid_offsets = indent.valid_outdent_offsets
+ else:
+ valid_offsets = indent.valid_continuation_offsets
+ return indent, valid_offsets.copy()
+
+ def _hanging_indent_after_bracket(self, bracket, position):
+ """Extracts indentation information for a hanging indent."""
+ indentation = _get_indent_length(self._tokens.line(position))
+ if self._is_block_opener and self._continuation_size == self._block_indent_size:
+ return _ContinuedIndent(
+ HANGING_BLOCK,
+ bracket,
+ position,
+ _Offsets(indentation + self._continuation_size, indentation),
+ _BeforeBlockOffsets(indentation + self._continuation_size,
+ indentation + self._continuation_size * 2))
+ elif bracket == ':':
+ if self._cont_stack[-1].context_type == CONTINUED:
+ # If the dict key was on the same line as the open brace, the new
+ # correct indent should be relative to the key instead of the
+ # current indent level
+ paren_align = self._cont_stack[-1].valid_outdent_offsets
+ next_align = self._cont_stack[-1].valid_continuation_offsets.copy()
+ next_align[next_align.keys()[0] + self._continuation_size] = True
+ else:
+ next_align = _Offsets(indentation + self._continuation_size, indentation)
+ paren_align = _Offsets(indentation + self._continuation_size, indentation)
+ return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align)
+ else:
+ return _ContinuedIndent(
+ HANGING,
+ bracket,
+ position,
+ _Offsets(indentation, indentation + self._continuation_size),
+ _Offsets(indentation + self._continuation_size))
+
+ def _continuation_inside_bracket(self, bracket, pos):
+ """Extracts indentation information for a continued indent."""
+ indentation = _get_indent_length(self._tokens.line(pos))
+ if self._is_block_opener and self._tokens.start_col(pos+1) - indentation == self._block_indent_size:
+ return _ContinuedIndent(
+ CONTINUED_BLOCK,
+ bracket,
+ pos,
+ _Offsets(self._tokens.start_col(pos)),
+ _BeforeBlockOffsets(self._tokens.start_col(pos+1),
+ self._tokens.start_col(pos+1) + self._continuation_size))
+ else:
+ return _ContinuedIndent(
+ CONTINUED,
+ bracket,
+ pos,
+ _Offsets(self._tokens.start_col(pos)),
+ _Offsets(self._tokens.start_col(pos+1)))
+
+ def pop_token(self):
+ self._cont_stack.pop()
+
+ def push_token(self, token, position):
+ """Pushes a new token for continued indentation on the stack.
+
+ Tokens that can modify continued indentation offsets are:
+ * opening brackets
+ * 'lambda'
+ * : inside dictionaries
+
+ push_token relies on the caller to filter out those
+ interesting tokens.
+
+ :param token: The concrete token
+ :param position: The position of the token in the stream.
+ """
+ if _token_followed_by_eol(self._tokens, position):
+ self._cont_stack.append(
+ self._hanging_indent_after_bracket(token, position))
+ else:
+ self._cont_stack.append(
+ self._continuation_inside_bracket(token, position))
+
+
class FormatChecker(BaseTokenChecker):
"""checks for :
* unauthorized constructions
@@ -180,15 +441,34 @@ class FormatChecker(BaseTokenChecker):
{'default' : ' ', 'type' : "string", 'metavar' : '<string>',
'help' : 'String used as indentation unit. This is usually \
" " (4 spaces) or "\\t" (1 tab).'}),
+ ('indent-after-paren',
+ {'type': 'int', 'metavar': '<int>', 'default': 4,
+ 'help': 'Number of spaces of indent required inside a hanging '
+ ' or continued line.'}),
)
+
def __init__(self, linter=None):
BaseTokenChecker.__init__(self, linter)
self._lines = None
self._visited_lines = None
+ self._bracket_stack = [None]
+
+ def _pop_token(self):
+ self._bracket_stack.pop()
+ self._current_line.pop_token()
+
+ def _push_token(self, token, idx):
+ self._bracket_stack.append(token)
+ self._current_line.push_token(token, idx)
- def new_line(self, tok_type, line, line_num, junk):
+ def new_line(self, tokens, line_end, line_start):
"""a new line has been encountered, process it if necessary"""
- if not tok_type in junk:
+ if _last_token_on_line_is(tokens, line_end, ';'):
+ self.add_message('unnecessary-semicolon', line=tokens.start_line(line_end))
+
+ line_num = tokens.start_line(line_start)
+ line = tokens.line(line_start)
+ if tokens.type(line_start) not in _JUNK_TOKENS:
self._lines[line_num] = line.split('\n')[0]
self.check_lines(line, line_num)
@@ -211,6 +491,8 @@ class FormatChecker(BaseTokenChecker):
start: int; the position of the keyword in the token list.
"""
# If the next token is not a paren, we're fine.
+ if self._inside_brackets(':') and tokens[start][1] == 'for':
+ self._pop_token()
if tokens[start+1][1] != '(':
return
@@ -270,7 +552,7 @@ class FormatChecker(BaseTokenChecker):
return
def _opening_bracket(self, tokens, i):
- self._bracket_stack.append(tokens[i][1])
+ self._push_token(tokens[i][1], i)
# Special case: ignore slices
if tokens[i][1] == '[' and tokens[i+1][1] == ':':
return
@@ -283,7 +565,9 @@ class FormatChecker(BaseTokenChecker):
self._check_space(tokens, i, (_IGNORE, _MUST_NOT))
def _closing_bracket(self, tokens, i):
- self._bracket_stack.pop()
+ if self._inside_brackets(':'):
+ self._pop_token()
+ self._pop_token()
# Special case: ignore slices
if tokens[i-1][1] == ':' and tokens[i][1] == ']':
return
@@ -302,7 +586,7 @@ class FormatChecker(BaseTokenChecker):
self._check_space(tokens, i, (_MUST, _MUST))
def _open_lambda(self, tokens, i): # pylint:disable=unused-argument
- self._bracket_stack.append('lambda')
+ self._push_token('lambda', i)
def _handle_colon(self, tokens, i):
# Special case: ignore slices
@@ -316,7 +600,9 @@ class FormatChecker(BaseTokenChecker):
self._check_space(tokens, i, policy)
if self._inside_brackets('lambda'):
- self._bracket_stack.pop()
+ self._pop_token()
+ elif self._inside_brackets('{'):
+ self._push_token(':', i)
def _handle_comma(self, tokens, i):
# Only require a following whitespace if this is
@@ -325,6 +611,8 @@ class FormatChecker(BaseTokenChecker):
self._check_space(tokens, i, (_MUST_NOT, _IGNORE))
else:
self._check_space(tokens, i, (_MUST_NOT, _MUST))
+ if self._inside_brackets(':'):
+ self._pop_token()
def _check_surrounded_by_space(self, tokens, i):
"""Check that a binary operator is surrounded by exactly one space."""
@@ -383,6 +671,10 @@ class FormatChecker(BaseTokenChecker):
def _inside_brackets(self, left):
return self._bracket_stack[-1] == left
+ def _handle_old_ne_operator(self, tokens, i):
+ if tokens[i][1] == '<>':
+ self.add_message('old-ne-operator', line=tokens[i][2][0])
+
def _prepare_token_dispatcher(self):
raw = [
(_KEYWORD_TOKENS,
@@ -401,6 +693,8 @@ class FormatChecker(BaseTokenChecker):
([':'], self._handle_colon),
(['lambda'], self._open_lambda),
+
+ (['<>'], self._handle_old_ne_operator),
]
dispatch = {}
@@ -419,76 +713,63 @@ class FormatChecker(BaseTokenChecker):
regular expression).
"""
self._bracket_stack = [None]
- indent = tokenize.INDENT
- dedent = tokenize.DEDENT
- newline = tokenize.NEWLINE
- junk = (tokenize.COMMENT, tokenize.NL)
indents = [0]
- check_equal = 0
+ check_equal = False
line_num = 0
- previous = None
self._lines = {}
self._visited_lines = {}
- new_line_delay = False
token_handlers = self._prepare_token_dispatcher()
+
+ self._current_line = ContinuedLineState(tokens, self.config)
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)
if start[0] != line_num:
- if previous is not None and previous[0] == tokenize.OP and previous[1] == ';':
- self.add_message('unnecessary-semicolon', line=previous[2])
- previous = None
line_num = start[0]
# A tokenizer oddity: if an indented line contains a multi-line
# docstring, the line member of the INDENT token does not contain
- # the full line; therefore we delay checking the new line until
- # the next token.
+ # the full line; therefore we check the next token on the line.
if tok_type == tokenize.INDENT:
- new_line_delay = True
+ self.new_line(TokenWrapper(tokens), idx-1, idx+1)
else:
- self.new_line(tok_type, line, line_num, junk)
- if tok_type not in (indent, dedent, newline) + junk:
- previous = tok_type, token, start[0]
-
- if tok_type == tokenize.OP:
- if token == '<>':
- self.add_message('old-ne-operator', line=line_num)
- elif tok_type == tokenize.NUMBER:
- if token.endswith('l'):
- self.add_message('lowercase-l-suffix', line=line_num)
-
- elif tok_type == newline:
+ self.new_line(TokenWrapper(tokens), idx-1, idx)
+
+ if tok_type == tokenize.NEWLINE:
# a program statement, or ENDMARKER, will eventually follow,
# after some (possibly empty) run of tokens of the form
# (NL | COMMENT)* (INDENT | DEDENT+)?
# If an INDENT appears, setting check_equal is wrong, and will
# be undone when we see the INDENT.
- check_equal = 1
-
- elif tok_type == indent:
- check_equal = 0
+ check_equal = True
+ self._process_retained_warnings(TokenWrapper(tokens), idx)
+ self._current_line.next_logical_line()
+ elif tok_type == tokenize.INDENT:
+ check_equal = False
self.check_indent_level(token, indents[-1]+1, line_num)
indents.append(indents[-1]+1)
-
- elif tok_type == dedent:
+ elif tok_type == tokenize.DEDENT:
# there's nothing we need to check here! what's important is
# that when the run of DEDENTs ends, the indentation of the
# program statement (or ENDMARKER) that triggered the run is
# equal to what's left at the top of the indents stack
- check_equal = 1
+ check_equal = True
if len(indents) > 1:
del indents[-1]
-
- elif check_equal and tok_type not in junk:
- # this is the first "real token" following a NEWLINE, so it
+ elif tok_type == tokenize.NL:
+ self._check_continued_indentation(TokenWrapper(tokens), idx+1)
+ self._current_line.next_physical_line()
+ elif tok_type != tokenize.COMMENT:
+ self._current_line.handle_line_start(idx)
+ # This is the first concrete token following a NEWLINE, so it
# must be the first token of the next program statement, or an
# ENDMARKER; the "line" argument exposes the leading whitespace
# for this statement; in the case of ENDMARKER, line is an empty
# string, so will properly match the empty string with which the
# "indents" stack was seeded
- check_equal = 0
- self.check_indent_level(line, indents[-1], line_num)
+ if check_equal:
+ check_equal = False
+ self.check_indent_level(line, indents[-1], line_num)
+
+ if tok_type == tokenize.NUMBER and token.endswith('l'):
+ self.add_message('lowercase-l-suffix', line=line_num)
try:
handler = token_handlers[token]
@@ -501,6 +782,49 @@ class FormatChecker(BaseTokenChecker):
if line_num > self.config.max_module_lines:
self.add_message('too-many-lines', args=line_num, line=1)
+ def _process_retained_warnings(self, tokens, current_pos):
+ single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':')
+
+ for indent_pos, state, offsets in self._current_line.retained_warnings:
+ block_type = offsets[tokens.start_col(indent_pos)]
+ hints = dict((k, v) for k, v in offsets.iteritems()
+ if v != block_type)
+ if single_line_block_stmt and block_type == WITH_BODY:
+ self._add_continuation_message(state, hints, tokens, indent_pos)
+ elif not single_line_block_stmt and block_type == SINGLE_LINE:
+ self._add_continuation_message(state, hints, tokens, indent_pos)
+
+ def _check_continued_indentation(self, tokens, next_idx):
+ # Do not issue any warnings if the next line is empty.
+ if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL:
+ return
+
+ state, valid_offsets = self._current_line.get_valid_offsets(next_idx)
+ # Special handling for hanging comments. If the last line ended with a
+ # comment and the new line contains only a comment, the line may also be
+ # indented to the start of the previous comment.
+ if (tokens.type(next_idx) == tokenize.COMMENT and
+ tokens.type(next_idx-2) == tokenize.COMMENT):
+ valid_offsets[tokens.start_col(next_idx-2)] = True
+
+ # We can only decide if the indentation of a continued line before opening
+ # a new block is valid once we know of the body of the block is on the
+ # same line as the block opener. Since the token processing is single-pass,
+ # emitting those warnings is delayed until the block opener is processed.
+ if (state.context_type in (HANGING_BLOCK, CONTINUED_BLOCK)
+ and tokens.start_col(next_idx) in valid_offsets):
+ self._current_line.add_block_warning(next_idx, state, valid_offsets)
+ elif tokens.start_col(next_idx) not in valid_offsets:
+ self._add_continuation_message(state, valid_offsets, tokens, next_idx)
+
+ def _add_continuation_message(self, state, offsets, tokens, position):
+ readable_type, readable_position = _CONTINUATION_MSG_PARTS[state.context_type]
+ hint_line = _get_indent_hint_line(offsets, tokens.start_col(position))
+ self.add_message(
+ 'bad-continuation',
+ line=tokens.start_line(position),
+ args=(readable_type, readable_position, tokens.line(position), hint_line))
+
@check_messages('multiple-statements')
def visit_default(self, node):
"""check the node line number and check it if not yet done"""
diff --git a/checkers/typecheck.py b/checkers/typecheck.py
index 5fe293d..4cb406f 100644
--- a/checkers/typecheck.py
+++ b/checkers/typecheck.py
@@ -287,7 +287,8 @@ accessed. Python regular expressions are accepted.'}
else:
for rnode in returns:
if not (isinstance(rnode.value, astroid.Const)
- and rnode.value.value is None):
+ and rnode.value.value is None
+ or rnode.value is None):
break
else:
self.add_message('assignment-from-none', node=node)
diff --git a/checkers/variables.py b/checkers/variables.py
index 2ce7fcf..a6123de 100644
--- a/checkers/variables.py
+++ b/checkers/variables.py
@@ -146,6 +146,12 @@ MSGS = {
'Used when something which is not '
'a sequence is used in an unpack assignment'),
+ 'W0640': ('Cell variable %s defined in loop',
+ 'cell-var-from-loop',
+ 'A variable used in a closure is defined in a loop. '
+ 'This will result in all closures using the same value for '
+ 'the closed-over variable.'),
+
}
class VariablesChecker(BaseChecker):
@@ -423,6 +429,25 @@ builtins. Remember that you should avoid to define new builtins when possible.'
if default_message:
self.add_message('global-statement', node=node)
+ def _check_late_binding_closure(self, node, assignment_node, scope_type):
+ node_scope = node.scope()
+ if not isinstance(node_scope, (astroid.Lambda, astroid.Function)):
+ return
+
+ if isinstance(assignment_node, astroid.Comprehension):
+ if assignment_node.parent.parent_of(node.scope()):
+ self.add_message('cell-var-from-loop', node=node, args=node.name)
+ else:
+ assign_scope = assignment_node.scope()
+ maybe_for = assignment_node
+ while not isinstance(maybe_for, astroid.For):
+ if maybe_for is assign_scope:
+ break
+ maybe_for = maybe_for.parent
+ else:
+ if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return):
+ self.add_message('cell-var-from-loop', node=node, args=node.name)
+
def _loopvar_name(self, node, name):
# filter variables according to node's scope
# XXX used to filter parents but don't remember why, and removing this
@@ -509,6 +534,8 @@ builtins. Remember that you should avoid to define new builtins when possible.'
# the name has already been consumed, only check it's not a loop
# variable used outside the loop
if name in consumed:
+ defnode = assign_parent(consumed[name][0])
+ self._check_late_binding_closure(node, defnode, scope_type)
self._loopvar_name(node, name)
break
# mark the name as consumed if it's defined in this scope
@@ -520,6 +547,7 @@ builtins. Remember that you should avoid to define new builtins when possible.'
# checks for use before assignment
defnode = assign_parent(to_consume[name][0])
if defnode is not None:
+ self._check_late_binding_closure(node, defnode, scope_type)
defstmt = defnode.statement()
defframe = defstmt.frame()
maybee0601 = True
diff --git a/lint.py b/lint.py
index 49eea73..679ee69 100644
--- a/lint.py
+++ b/lint.py
@@ -907,7 +907,7 @@ group are mutually exclusive.'),
try:
preprocess_options(args, {
# option: (callback, takearg)
- 'init-hooks': (cb_init_hook, True),
+ 'init-hook': (cb_init_hook, True),
'rcfile': (self.cb_set_rcfile, True),
'load-plugins': (self.cb_add_plugins, True),
})
diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py
new file mode 100644
index 0000000..f0787de
--- /dev/null
+++ b/test/input/func_bad_continuation.py
@@ -0,0 +1,222 @@
+"""Regression test case for bad-continuation."""
+
+__revision__ = 1
+
+# Various alignment for brackets
+print [
+ 1, 2, 3
+]
+print [
+ 1, 2, 3
+ ]
+print [
+ 1, 2, 3
+ ] # [bad-continuation]
+
+# Alignment inside literals
+W0 = [1, 2, 3,
+ 4, 5, 6,
+ 7, # [bad-continuation]
+ 8, 9, 10,
+ 11, 12, 13,
+ # and a comment
+ 14, 15, 16]
+
+W1 = {
+ 'a': 1,
+ 'b': 2, # [bad-continuation]
+ 'c': 3,
+ }
+
+W2 = {
+ 'a': 1,
+ 'b': 2, # [bad-continuation]
+ 'c': 3,
+ }
+
+W2 = ['some', 'contents' # with a continued comment that may be aligned
+ # under the previous comment (optionally)
+ 'and',
+ 'more', # but this
+ # [bad-continuation] is not accepted
+ 'contents', # nor this. [bad-continuation]
+ ]
+
+# Values in dictionaries should be indented 4 spaces further if they are on a
+# different line than their key
+W4 = {
+ 'key1':
+ 'value1', # Grandfather in the old style
+ 'key2':
+ 'value2', # [bad-continuation]
+ 'key3':
+ 'value3', # Comma here
+ }
+
+# And should follow the same rules as continuations within parens
+W5 = {
+ 'key1': 'long value'
+ 'long continuation',
+ 'key2': 'breaking'
+ 'wrong', # [bad-continuation]
+ 'key3': 2*(
+ 2+2),
+ 'key4': ('parenthesis',
+ 'continuation') # No comma here
+ }
+
+# Allow values to line up with their keys when the key is next to the brace
+W6 = {'key1':
+ 'value1',
+ 'key2':
+ 'value2',
+ }
+
+# Or allow them to be indented
+W7 = {'key1':
+ 'value1',
+ 'key2':
+ 'value2'
+ }
+
+# Bug that caused a warning on the previous two cases permitted these odd
+# incorrect indentations
+W8 = {'key1':
+'value1', # [bad-continuation]
+ }
+
+W9 = {'key1':
+ 'value1', # [bad-continuation]
+ }
+
+# Dictionary comprehensions should not require extra indentation when breaking
+# before the 'for', which is not part of the value
+C1 = {'key{}'.format(x): 'value{}'.format(x)
+ for x in range(3)}
+
+C2 = {'key{}'.format(x): 'value{}'.format(x) for x in
+ range(3)}
+
+# Dictionary comprehensions with multiple loops broken in different places
+C3 = {x*y: (x, y) for x in range(3) for y in range(3)}
+
+C4 = {x*y: (x, y)
+ for x in range(3) for y in range(3)}
+
+C5 = {x*y: (x, y) for x
+ in range(3) for y in range(3)}
+
+C6 = {x*y: (x, y) for x in range(3)
+ for y in range(3)}
+
+C7 = {key:
+ key ** 2
+ for key in range(10)}
+
+C8 = {
+ key: key ** 2
+ for key in range(10)}
+
+# Misaligned cases for dict comprehensions
+C9 = {'key{}'.format(x): 'value{}'.format(x)
+ for x in range(3)} # [bad-continuation]
+
+C9 = {'key{}'.format(x): 'value{}'.format(x)
+ for x in range(3)} # [bad-continuation]
+
+# Alignment of arguments in function definitions
+def continue1(some_arg,
+ some_other_arg):
+ """A function with well-aligned arguments."""
+ print some_arg, some_other_arg
+
+
+def continue2(
+ some_arg,
+ some_other_arg):
+ """A function with well-aligned arguments."""
+ print some_arg, some_other_arg
+
+def continue3(
+ some_arg, # [bad-continuation]
+ some_other_arg): # [bad-continuation]
+ """A function with misaligned arguments"""
+ print some_arg, some_other_arg
+
+def continue4(
+ arg1,
+ arg2): print arg1, arg2
+
+
+def callee(*args):
+ """noop"""
+ print args
+
+
+callee(
+ "a",
+ "b"
+ )
+
+callee("a",
+ "b") # [bad-continuation]
+
+callee(5, {'a': 'b',
+ 'c': 'd'})
+
+if (
+ 1
+ ): pass
+
+if (
+ 1
+): pass
+if (
+ 1
+ ): pass # [bad-continuation]
+
+if (1 and
+ 2): # [bad-continuation]
+ pass
+
+while (1 and
+ 2):
+ pass
+
+while (1 and
+ 2 and # [bad-continuation]
+ 3):
+ pass
+
+if (
+ 2): pass # [bad-continuation]
+
+if (1 or
+ 2 or
+ 3): pass
+
+if (1 or
+ 2 or # [bad-continuation]
+ 3): print 1, 2
+
+if (1 and
+ 2): pass # [bad-continuation]
+
+if (
+ 2): pass
+
+if (
+ 2): # [bad-continuation]
+ pass
+
+L1 = (lambda a,
+ b: a + b)
+
+if not (1 and
+ 2):
+ print 3
+
+if not (1 and
+ 2): # [bad-continuation]
+ print 3
+
diff --git a/test/input/func_format_py27.py b/test/input/func_format_py27.py
index 828a180..46d5cfe 100644
--- a/test/input/func_format_py27.py
+++ b/test/input/func_format_py27.py
@@ -79,8 +79,7 @@ def hop(context):
return ['''<a id="sendbutton" href="javascript: $('%(domid)s').submit()">
<img src="%(sendimgpath)s" alt="%(send)s"/>%(send)s</a>''' % context,
'''<a id="cancelbutton" href="javascript: history.back()">
-<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context,
- ]
+<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context]
titreprojet = '<tr><td colspan="10">\
<img src="images/drapeau_vert.png" alt="Drapeau vert" />\
<strong>%s</strong></td></tr>' % aaaa
diff --git a/test/input/func_format_py_27.py b/test/input/func_format_py_27.py
index f062f27..95f7fde 100644
--- a/test/input/func_format_py_27.py
+++ b/test/input/func_format_py_27.py
@@ -79,8 +79,7 @@ def hop(context):
return ['''<a id="sendbutton" href="javascript: $('%(domid)s').submit()">
<img src="%(sendimgpath)s" alt="%(send)s"/>%(send)s</a>''' % context,
'''<a id="cancelbutton" href="javascript: history.back()">
-<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context,
- ]
+<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context]
titreprojet = '<tr><td colspan="10">\
<img src="images/drapeau_vert.png" alt="Drapeau vert" />\
<strong>%s</strong></td></tr>' % aaaa
diff --git a/test/input/func_indent.py b/test/input/func_indent.py
index 18159e9..3c6ee41 100644
--- a/test/input/func_indent.py
+++ b/test/input/func_indent.py
@@ -11,6 +11,7 @@ def tutuu():
def titii():
"""also malindented"""
+ 1 # and this.
def tataa(kdict):
"""blank line unindented"""
diff --git a/test/input/func_loopvar_in_closure.py b/test/input/func_loopvar_in_closure.py
new file mode 100644
index 0000000..32b7a6c
--- /dev/null
+++ b/test/input/func_loopvar_in_closure.py
@@ -0,0 +1,114 @@
+"""Tests for loopvar-in-closure."""
+
+__revision__ = 0
+
+
+def good_case():
+ """No problems here."""
+ lst = []
+ for i in range(10):
+ lst.append(i)
+
+
+def good_case2():
+ """No problems here."""
+ return [i for i in range(10)]
+
+
+def good_case3():
+ """No problems here."""
+ lst = []
+ for i in range(10):
+ lst.append(lambda i=i: i)
+
+
+def good_case4():
+ """No problems here."""
+ lst = []
+ for i in range(10):
+ print i
+ lst.append(lambda i: i)
+
+
+def good_case5():
+ """No problems here."""
+ return (i for i in range(10))
+
+
+def good_case6():
+ """Accept use of the variable after the loop.
+
+ There's already a warning about possibly undefined loop variables, and
+ the value will not change any more."""
+ for i in range(10):
+ print i
+ return lambda: i
+
+
+def good_case7():
+ """Accept use of the variable inside return."""
+ for i in range(10):
+ if i == 8:
+ return lambda: i
+ return lambda: -1
+
+
+def bad_case():
+ """Closing over a loop variable."""
+ lst = []
+ for i in range(10):
+ print i
+ lst.append(lambda: i)
+
+
+def bad_case2():
+ """Closing over a loop variable."""
+ return [lambda: i for i in range(10)]
+
+
+def bad_case3():
+ """Closing over variable defined in loop."""
+ lst = []
+ for i in range(10):
+ j = i * i
+ lst.append(lambda: j)
+ return lst
+
+
+def bad_case4():
+ """Closing over variable defined in loop."""
+ lst = []
+ for i in range(10):
+ def nested():
+ """Nested function."""
+ return i**2
+ lst.append(nested)
+ return lst
+
+
+def bad_case5():
+ """Problematic case.
+
+ If this function is used as
+
+ >>> [x() for x in bad_case5()]
+
+ it behaves 'as expected', i.e. the result is range(10).
+
+ If it's used with
+
+ >>> lst = list(bad_case5())
+ >>> [x() for x in lst]
+
+ the result is [9] * 10 again.
+ """
+ return (lambda: i for i in range(10))
+
+
+def bad_case6():
+ """Closing over variable defined in loop."""
+ lst = []
+ for i, j in zip(range(10), range(10, 20)):
+ print j
+ lst.append(lambda: i)
+ return lst
diff --git a/test/input/func_loopvar_in_dict_comp_py27.py b/test/input/func_loopvar_in_dict_comp_py27.py
new file mode 100644
index 0000000..312eee7
--- /dev/null
+++ b/test/input/func_loopvar_in_dict_comp_py27.py
@@ -0,0 +1,8 @@
+"""Tests for loopvar-in-closure."""
+
+__revision__ = 0
+
+
+def bad_case():
+ """Loop variable from dict comprehension."""
+ return {x: lambda: x for x in range(10)}
diff --git a/test/input/func_noerror_new_style_class_py_30.py b/test/input/func_noerror_new_style_class_py_30.py
index 87bfb9e..9e55277 100644
--- a/test/input/func_noerror_new_style_class_py_30.py
+++ b/test/input/func_noerror_new_style_class_py_30.py
@@ -18,7 +18,7 @@ class File(file):
super(File, self).__init__(name, mode, buffering)
if self.verbose:
print "File %s is opened. The mode is: %s" % (self.name,
-self.mode)
+ self.mode)
#
def write(self, a_string):
diff --git a/test/input/func_typecheck_callfunc_assigment.py b/test/input/func_typecheck_callfunc_assigment.py
index a909f17..838b27c 100644
--- a/test/input/func_typecheck_callfunc_assigment.py
+++ b/test/input/func_typecheck_callfunc_assigment.py
@@ -28,6 +28,13 @@ def func_return_none():
A = func_return_none()
+def func_implicit_return_none():
+ """Function returning None from bare return statement."""
+ return
+
+A = func_implicit_return_none()
+
+
def func_return_none_and_smth():
"""function returning none and something else"""
print 'dougloup'
diff --git a/test/messages/func_bad_continuation.txt b/test/messages/func_bad_continuation.txt
new file mode 100644
index 0000000..ba6516d
--- /dev/null
+++ b/test/messages/func_bad_continuation.txt
@@ -0,0 +1,70 @@
+C: 14: Wrong hanging indentation.
+ ] # [bad-continuation]
+| ^|
+C: 19: Wrong continued indentation.
+ 7, # [bad-continuation]
+ | ^
+C: 27: Wrong hanging indentation.
+ 'b': 2, # [bad-continuation]
+ ^|
+C: 33: Wrong hanging indentation.
+ 'b': 2, # [bad-continuation]
+ ^|
+C: 41: Wrong continued indentation.
+ # [bad-continuation] is not accepted
+ | | ^
+C: 42: Wrong continued indentation.
+ 'contents', # nor this. [bad-continuation]
+ | ^
+C: 51: Wrong hanging indentation in dict value.
+ 'value2', # [bad-continuation]
+ | ^ |
+C: 61: Wrong continued indentation.
+ 'wrong', # [bad-continuation]
+ ^ |
+C: 85: Wrong hanging indentation in dict value.
+'value1', # [bad-continuation]
+^ | |
+C: 89: Wrong hanging indentation in dict value.
+ 'value1', # [bad-continuation]
+ ^ | |
+C:122: Wrong continued indentation.
+ for x in range(3)} # [bad-continuation]
+ ^ |
+C:125: Wrong continued indentation.
+ for x in range(3)} # [bad-continuation]
+ | ^
+C:141: Wrong hanging indentation before block.
+ some_arg, # [bad-continuation]
+ ^ |
+C:142: Wrong hanging indentation before block.
+ some_other_arg): # [bad-continuation]
+ ^ |
+C:146:continue4: Missing function docstring
+C:162: Wrong continued indentation.
+ "b") # [bad-continuation]
+ ^ |
+C:176: Wrong hanging indentation before block.
+ ): pass # [bad-continuation]
+| ^|
+C:179: Wrong continued indentation before block.
+ 2): # [bad-continuation]
+ ^ |
+C:187: Wrong continued indentation.
+ 2 and # [bad-continuation]
+ | ^
+C:192: Wrong hanging indentation before block.
+ 2): pass # [bad-continuation]
+ ^ | |
+C:199: Wrong continued indentation before block.
+ 2 or # [bad-continuation]
+ |^ |
+C:203: Wrong continued indentation before block.
+ 2): pass # [bad-continuation]
+ ^ | |
+C:209: Wrong hanging indentation before block.
+ 2): # [bad-continuation]
+ ^ | |
+C:220: Wrong continued indentation.
+ 2): # [bad-continuation]
+ ^ |
diff --git a/test/messages/func_break_or_return_in_try_finally.txt b/test/messages/func_break_or_return_in_try_finally.txt
index 4b674ae..04f27fe 100644
--- a/test/messages/func_break_or_return_in_try_finally.txt
+++ b/test/messages/func_break_or_return_in_try_finally.txt
@@ -1,2 +1,3 @@
W: 18:insidious_break_and_return: break statement in finally block may swallow exception
W: 20:insidious_break_and_return: return statement in finally block may swallow exception
+W: 39:break_and_return.strange: Cell variable my_var defined in loop
diff --git a/test/messages/func_format_py27.txt b/test/messages/func_format_py27.txt
index ce5df81..f71cee8 100644
--- a/test/messages/func_format_py27.txt
+++ b/test/messages/func_format_py27.txt
@@ -41,6 +41,6 @@ C: 71: Exactly one space required before assignment
C: 73: Exactly one space required around assignment
ocount[obj.__class__]=1
^
-C: 91: More than one statement on a single line
-C:105: More than one statement on a single line
-C:108: More than one statement on a single line
+C: 90: More than one statement on a single line
+C:104: More than one statement on a single line
+C:107: More than one statement on a single line
diff --git a/test/messages/func_indent.txt b/test/messages/func_indent.txt
index aa3645f..e24af18 100644
--- a/test/messages/func_indent.txt
+++ b/test/messages/func_indent.txt
@@ -1,3 +1,5 @@
W: 5: Bad indentation. Found 1 spaces, expected 4
W: 6: Bad indentation. Found 1 spaces, expected 4
W: 13: Bad indentation. Found 5 spaces, expected 4
+W: 14: Bad indentation. Found 5 spaces, expected 4
+W: 14:titii: Statement seems to have no effect
diff --git a/test/messages/func_loopvar_in_closure.txt b/test/messages/func_loopvar_in_closure.txt
new file mode 100644
index 0000000..6ca613a
--- /dev/null
+++ b/test/messages/func_loopvar_in_closure.txt
@@ -0,0 +1,8 @@
+W: 21:good_case3: Unused variable 'i'
+W: 45:good_case6.<lambda>: Using possibly undefined loop variable 'i'
+W: 61:bad_case.<lambda>: Cell variable i defined in loop
+W: 66:bad_case2.<lambda>: Cell variable i defined in loop
+W: 74:bad_case3.<lambda>: Cell variable j defined in loop
+W: 84:bad_case4.nested: Cell variable i defined in loop
+W:105:bad_case5.<lambda>: Cell variable i defined in loop
+W:113:bad_case6.<lambda>: Cell variable i defined in loop
diff --git a/test/messages/func_loopvar_in_dict_comp_py27.txt b/test/messages/func_loopvar_in_dict_comp_py27.txt
new file mode 100644
index 0000000..bc11121
--- /dev/null
+++ b/test/messages/func_loopvar_in_dict_comp_py27.txt
@@ -0,0 +1 @@
+W: 8:bad_case.<lambda>: Cell variable x defined in loop
diff --git a/test/messages/func_typecheck_callfunc_assigment.txt b/test/messages/func_typecheck_callfunc_assigment.txt
index 96ad43e..1d84510 100644
--- a/test/messages/func_typecheck_callfunc_assigment.txt
+++ b/test/messages/func_typecheck_callfunc_assigment.txt
@@ -1,2 +1,3 @@
E: 20: Assigning to function call which doesn't return
W: 28: Assigning to function call which only returns None
+W: 35: Assigning to function call which only returns None
diff --git a/test/unittest_checker_format.py b/test/unittest_checker_format.py
index 8f1be78..b9fba5c 100644
--- a/test/unittest_checker_format.py
+++ b/test/unittest_checker_format.py
@@ -135,7 +135,7 @@ class CheckSpaceTest(CheckerTestCase):
good_cases = [
'(a)\n',
'(a * (b + c))\n',
- '( #\na)\n',
+ '(#\n a)\n',
]
with self.assertNoMessages():
for code in good_cases:
diff --git a/test/unittest_lint.py b/test/unittest_lint.py
index a7213bd..2b13039 100644
--- a/test/unittest_lint.py
+++ b/test/unittest_lint.py
@@ -381,9 +381,9 @@ class PyLinterTC(TestCase):
def test_init_hooks_called_before_load_plugins(self):
self.assertRaises(RuntimeError,
- Run, ['--load-plugins', 'unexistant', '--init-hooks', 'raise RuntimeError'])
+ Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError'])
self.assertRaises(RuntimeError,
- Run, ['--init-hooks', 'raise RuntimeError', '--load-plugins', 'unexistant'])
+ Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant'])
class ConfigTC(TestCase):