From 0c5b28190c339d05ef70883f12a71c9613c9762e Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 5 Dec 2015 22:40:00 +1100 Subject: [PATCH] Report unassigned expressions --- pyflakes/checker.py | 24 +++++++ pyflakes/messages.py | 11 +++ pyflakes/test/test_imports.py | 79 ++++++++++++---------- pyflakes/test/test_other.py | 97 ++++++++++++++------------- pyflakes/test/test_undefined_names.py | 24 +++---- 5 files changed, 140 insertions(+), 95 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index fe201462..cb5d872e 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -12,6 +12,7 @@ PY2 = sys.version_info < (3, 0) PY32 = sys.version_info < (3, 3) # Python 2.5 to 3.2 PY33 = sys.version_info < (3, 4) # Python 2.5 to 3.3 +PY34 = sys.version_info < (3, 5) # Python 2.5 to 3.4 try: sys.pypy_version_info PYPY = True @@ -33,6 +34,20 @@ from pyflakes import messages +ALLOWED_EXPR_NODE_TYPE = ( + ast.Str, ast.Yield, ast.Call, + ast.Name, ast.Attribute, + ast.Ellipsis, +) + +if not PY32: + ALLOWED_EXPR_NODE_TYPE = ALLOWED_EXPR_NODE_TYPE + ( + ast.YieldFrom, ) + +if not PY34: + ALLOWED_EXPR_NODE_TYPE = ALLOWED_EXPR_NODE_TYPE + ( + ast.Await, ) + if PY2: def getNodeType(node_class): # workaround str.upper() which is locale-dependent @@ -772,6 +787,15 @@ def ignore(self, node): # additional node types COMPREHENSION = KEYWORD = FORMATTEDVALUE = handleChildren + def EXPR(self, node): + value = node.value + + if (not self._in_doctest() and + not isinstance(value, ALLOWED_EXPR_NODE_TYPE)): + self.report(messages.UnusedExpression, node) + + self.handleNode(value, node) + def ASSERT(self, node): if isinstance(node.test, ast.Tuple) and node.test.elts != []: self.report(messages.AssertTuple, node) diff --git a/pyflakes/messages.py b/pyflakes/messages.py index 05db5bfa..d1111833 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -145,6 +145,17 @@ def __init__(self, filename, loc, names): self.message_args = (names,) +class UnusedExpression(Message): + """ + Indicates that an expression was not assigned. + """ + message = 'expression not used' + + def __init__(self, filename, loc): + Message.__init__(self, filename, loc) + self.message_args = () + + class ReturnWithArgsInsideGenerator(Message): """ Indicates a return statement with arguments inside a generator. diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py index 21015795..9adc8395 100644 --- a/pyflakes/test/test_imports.py +++ b/pyflakes/test/test_imports.py @@ -396,38 +396,38 @@ def fun(): ''') def test_usedInOperators(self): - self.flakes('import fu; 3 + fu.bar') - self.flakes('import fu; 3 % fu.bar') - self.flakes('import fu; 3 - fu.bar') - self.flakes('import fu; 3 * fu.bar') - self.flakes('import fu; 3 ** fu.bar') - self.flakes('import fu; 3 / fu.bar') - self.flakes('import fu; 3 // fu.bar') - self.flakes('import fu; -fu.bar') - self.flakes('import fu; ~fu.bar') - self.flakes('import fu; 1 == fu.bar') - self.flakes('import fu; 1 | fu.bar') - self.flakes('import fu; 1 & fu.bar') - self.flakes('import fu; 1 ^ fu.bar') - self.flakes('import fu; 1 >> fu.bar') - self.flakes('import fu; 1 << fu.bar') + self.flakes('import fu; x = 3 + fu.bar') + self.flakes('import fu; x = 3 % fu.bar') + self.flakes('import fu; x = 3 - fu.bar') + self.flakes('import fu; x = 3 * fu.bar') + self.flakes('import fu; x = 3 ** fu.bar') + self.flakes('import fu; x = 3 / fu.bar') + self.flakes('import fu; x = 3 // fu.bar') + self.flakes('import fu; x = -fu.bar') + self.flakes('import fu; x = ~fu.bar') + self.flakes('import fu; x = 1 == fu.bar') + self.flakes('import fu; x = 1 | fu.bar') + self.flakes('import fu; x = 1 & fu.bar') + self.flakes('import fu; x = 1 ^ fu.bar') + self.flakes('import fu; x = 1 >> fu.bar') + self.flakes('import fu; x = 1 << fu.bar') def test_usedInAssert(self): self.flakes('import fu; assert fu.bar') def test_usedInSubscript(self): - self.flakes('import fu; fu.bar[1]') + self.flakes('import fu; x = fu.bar[1]') def test_usedInLogic(self): - self.flakes('import fu; fu and False') - self.flakes('import fu; fu or False') - self.flakes('import fu; not fu.bar') + self.flakes('import fu; x = fu and False') + self.flakes('import fu; x = fu or False') + self.flakes('import fu; x = not fu.bar') def test_usedInList(self): - self.flakes('import fu; [fu]') + self.flakes('import fu; x = [fu]') def test_usedInTuple(self): - self.flakes('import fu; (fu,)') + self.flakes('import fu; x = (fu,)') def test_usedInTry(self): self.flakes(''' @@ -465,8 +465,8 @@ def gen(): ''') def test_usedInDict(self): - self.flakes('import fu; {fu:None}') - self.flakes('import fu; {1:fu}') + self.flakes('import fu; x = {fu:None}') + self.flakes('import fu; x = {1:fu}') def test_usedInParameterDefault(self): self.flakes(''' @@ -486,13 +486,13 @@ def test_usedInAssignment(self): self.flakes('import fu; n=0; n+=fu') def test_usedInListComp(self): - self.flakes('import fu; [fu for _ in range(1)]') - self.flakes('import fu; [1 for _ in range(1) if fu]') + self.flakes('import fu; x = [fu for _ in range(1)]') + self.flakes('import fu; x = [1 for _ in range(1) if fu]') @skipIf(version_info >= (3,), 'in Python 3 list comprehensions execute in a separate scope') def test_redefinedByListComp(self): - self.flakes('import fu; [1 for fu in range(1)]', + self.flakes('import fu; x = [1 for fu in range(1)]', m.RedefinedInListComp) def test_usedInTryFinally(self): @@ -553,7 +553,7 @@ def g(): foo.is_used() @skipIf(version_info >= (3,), 'deprecated syntax') def test_usedInBackquote(self): - self.flakes('import fu; `fu`') + self.flakes('import fu; x = `fu`') def test_usedInExec(self): if version_info < (3,): @@ -563,15 +563,20 @@ def test_usedInExec(self): self.flakes('import fu; %s' % exec_stmt) def test_usedInLambda(self): - self.flakes('import fu; lambda: fu') + self.flakes('import fu; x = lambda: fu') def test_shadowedByLambda(self): - self.flakes('import fu; lambda fu: fu', + self.flakes('import fu; x = lambda fu: fu', m.UnusedImport, m.RedefinedWhileUnused) - self.flakes('import fu; lambda fu: fu\nfu()') + + if self.withDoctest: + expect = [] + else: + expect = [m.UnusedExpression] + self.flakes('import fu; lambda fu: fu\nfu()', *expect) def test_usedInSliceObj(self): - self.flakes('import fu; "meow"[::fu]') + self.flakes('import fu; x = "meow"[::fu]') def test_unusedInNestedScope(self): self.flakes(''' @@ -677,12 +682,14 @@ def test_differentSubmoduleImport(self): """ self.flakes(''' import fu.bar, fu.baz - fu.bar, fu.baz + fu.bar + fu.baz ''') self.flakes(''' import fu.bar import fu.baz - fu.bar, fu.baz + fu.bar + fu.baz ''') def test_assignRHSFirst(self): @@ -903,15 +910,15 @@ def test_usedInGenExp(self): """ Using a global in a generator expression results in no warnings. """ - self.flakes('import fu; (fu for _ in range(1))') - self.flakes('import fu; (1 for _ in range(1) if fu)') + self.flakes('import fu; x = (fu for _ in range(1))') + self.flakes('import fu; x = (1 for _ in range(1) if fu)') def test_redefinedByGenExp(self): """ Re-using a global name as the loop variable for a generator expression results in a redefinition warning. """ - self.flakes('import fu; (1 for fu in range(1))', + self.flakes('import fu; x = (1 for fu in range(1))', m.RedefinedWhileUnused, m.UnusedImport) def test_usedAsDecorator(self): diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index 0ac96a32..442e7120 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -30,26 +30,26 @@ def test_redefinedInListComp(self): """ self.flakes(''' a = 1 - [1 for a, b in [(1, 2)]] + x = [1 for a, b in [(1, 2)]] ''', m.RedefinedInListComp) self.flakes(''' class A: a = 1 - [1 for a, b in [(1, 2)]] + x = [1 for a, b in [(1, 2)]] ''', m.RedefinedInListComp) self.flakes(''' def f(): a = 1 - [1 for a, b in [(1, 2)]] + return [1 for a, b in [(1, 2)]] ''', m.RedefinedInListComp) self.flakes(''' - [1 for a, b in [(1, 2)]] - [1 for a, b in [(1, 2)]] + x = [1 for a, b in [(1, 2)]] + y = [1 for a, b in [(1, 2)]] ''') self.flakes(''' for a, b in [(1, 2)]: pass - [1 for a, b in [(1, 2)]] + x = [1 for a, b in [(1, 2)]] ''') def test_redefinedInGenerator(self): @@ -59,26 +59,26 @@ def test_redefinedInGenerator(self): """ self.flakes(''' a = 1 - (1 for a, b in [(1, 2)]) + x = (1 for a, b in [(1, 2)]) ''') self.flakes(''' class A: a = 1 - list(1 for a, b in [(1, 2)]) + x = list(1 for a, b in [(1, 2)]) ''') self.flakes(''' def f(): a = 1 - (1 for a, b in [(1, 2)]) + return (1 for a, b in [(1, 2)]) ''', m.UnusedVariable) self.flakes(''' - (1 for a, b in [(1, 2)]) - (1 for a, b in [(1, 2)]) + x = (1 for a, b in [(1, 2)]) + y = (1 for a, b in [(1, 2)]) ''') self.flakes(''' for a, b in [(1, 2)]: pass - (1 for a, b in [(1, 2)]) + x = (1 for a, b in [(1, 2)]) ''') @skipIf(version_info < (2, 7), "Python >= 2.7 only") @@ -89,26 +89,26 @@ def test_redefinedInSetComprehension(self): """ self.flakes(''' a = 1 - {1 for a, b in [(1, 2)]} + x = {1 for a, b in [(1, 2)]} ''') self.flakes(''' class A: a = 1 - {1 for a, b in [(1, 2)]} + x = {1 for a, b in [(1, 2)]} ''') self.flakes(''' def f(): a = 1 - {1 for a, b in [(1, 2)]} + return {1 for a, b in [(1, 2)]} ''', m.UnusedVariable) self.flakes(''' - {1 for a, b in [(1, 2)]} - {1 for a, b in [(1, 2)]} + x = {1 for a, b in [(1, 2)]} + y = {1 for a, b in [(1, 2)]} ''') self.flakes(''' for a, b in [(1, 2)]: pass - {1 for a, b in [(1, 2)]} + x = {1 for a, b in [(1, 2)]} ''') @skipIf(version_info < (2, 7), "Python >= 2.7 only") @@ -119,26 +119,26 @@ def test_redefinedInDictComprehension(self): """ self.flakes(''' a = 1 - {1: 42 for a, b in [(1, 2)]} + x = {1: 42 for a, b in [(1, 2)]} ''') self.flakes(''' class A: a = 1 - {1: 42 for a, b in [(1, 2)]} + x = {1: 42 for a, b in [(1, 2)]} ''') self.flakes(''' def f(): a = 1 - {1: 42 for a, b in [(1, 2)]} + return {1: 42 for a, b in [(1, 2)]} ''', m.UnusedVariable) self.flakes(''' - {1: 42 for a, b in [(1, 2)]} - {1: 42 for a, b in [(1, 2)]} + x = {1: 42 for a, b in [(1, 2)]} + y = {1: 42 for a, b in [(1, 2)]} ''') self.flakes(''' for a, b in [(1, 2)]: pass - {1: 42 for a, b in [(1, 2)]} + x = {1: 42 for a, b in [(1, 2)]} ''') def test_redefinedFunction(self): @@ -219,7 +219,7 @@ def test_redefinedIfElseInListComp(self): if False: a = 1 else: - [a for a in '12'] + x = [a for a in '12'] ''') @skipIf(version_info >= (3,), @@ -234,7 +234,7 @@ def test_redefinedElseInListComp(self): pass else: a = 1 - [a for a in '12'] + x = [a for a in '12'] ''', m.RedefinedInListComp) def test_functionDecorator(self): @@ -277,7 +277,10 @@ def t(self): def test_unaryPlus(self): """Don't die on unary +.""" - self.flakes('+1') + if self.withDoctest: + self.flakes('+1') + else: + self.flakes('+1', m.UnusedExpression) def test_undefinedBaseClass(self): """ @@ -1052,12 +1055,12 @@ def test_comparison(self): self.flakes(''' x = 10 y = 20 - x < y - x <= y - x == y - x != y - x >= y - x > y + if x < y: pass + if x <= y: pass + if x == y: pass + if x != y: pass + if x >= y: pass + if x > y: pass ''') def test_identity(self): @@ -1068,8 +1071,8 @@ def test_identity(self): self.flakes(''' x = 10 y = 20 - x is y - x is not y + if x is y: pass + if x is not y: pass ''') def test_containment(self): @@ -1080,8 +1083,8 @@ def test_containment(self): self.flakes(''' x = 10 y = 20 - x in y - x not in y + if x in y: pass + if x not in y: pass ''') def test_loopControl(self): @@ -1102,7 +1105,7 @@ def test_ellipsis(self): Ellipsis in a slice is supported. """ self.flakes(''' - [1, 2][...] + x = [1, 2][...] ''') def test_extendedSlice(self): @@ -1111,7 +1114,7 @@ def test_extendedSlice(self): """ self.flakes(''' x = 3 - [1, 2][x,:] + y = [1, 2][x,:] ''') def test_varAugmentedAssignment(self): @@ -1262,7 +1265,7 @@ def test_assignInListComprehension(self): """ self.flakes(''' def f(): - [None for i in range(10)] + return [None for i in range(10)] ''') def test_generatorExpression(self): @@ -1272,7 +1275,7 @@ def test_generatorExpression(self): """ self.flakes(''' def f(): - (None for i in range(10)) + return (None for i in range(10)) ''') def test_assignmentInsideLoop(self): @@ -1443,8 +1446,8 @@ def test_withStatementTupleNames(self): self.flakes(''' from __future__ import with_statement with open('foo') as (bar, baz): - bar, baz - bar, baz + x = bar, baz + x = bar, baz ''') def test_withStatementListNames(self): @@ -1455,8 +1458,8 @@ def test_withStatementListNames(self): self.flakes(''' from __future__ import with_statement with open('foo') as [bar, baz]: - bar, baz - bar, baz + x = bar, baz + x = bar, baz ''') def test_withStatementComplicatedTarget(self): @@ -1471,8 +1474,8 @@ def test_withStatementComplicatedTarget(self): from __future__ import with_statement c = d = e = g = h = i = None with open('foo') as [(a, b), c[d], e.f, g[h:i]]: - a, b, c, d, e, g, h, i - a, b, c, d, e, g, h, i + x = a, b, c, d, e, g, h, i + x = a, b, c, d, e, g, h, i ''') def test_withStatementSingleNameUndefined(self): diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py index 5653b915..c5c64c22 100644 --- a/pyflakes/test/test_undefined_names.py +++ b/pyflakes/test/test_undefined_names.py @@ -11,13 +11,13 @@ def test_undefined(self): self.flakes('bar', m.UndefinedName) def test_definedInListComp(self): - self.flakes('[a for a in range(10) if a]') + self.flakes('x = [a for a in range(10) if a]') @skipIf(version_info < (3,), 'in Python 2 list comprehensions execute in the same scope') def test_undefinedInListComp(self): self.flakes(''' - [a for a in range(10)] + x = [a for a in range(10)] a ''', m.UndefinedName) @@ -482,19 +482,19 @@ def test_definedInGenExp(self): Using the loop variable of a generator expression results in no warnings. """ - self.flakes('(a for a in [1, 2, 3] if a)') + self.flakes('x = (a for a in [1, 2, 3] if a)') - self.flakes('(b for b in (a for a in [1, 2, 3] if a) if b)') + self.flakes('x = (b for b in (a for a in [1, 2, 3] if a) if b)') def test_undefinedInGenExpNested(self): """ The loop variables of generator expressions nested together are not defined in the other generator. """ - self.flakes('(b for b in (a for a in [1, 2, 3] if b) if b)', + self.flakes('x = (b for b in (a for a in [1, 2, 3] if b) if b)', m.UndefinedName) - self.flakes('(b for b in (a for a in [1, 2, 3] if a) if a)', + self.flakes('x = (b for b in (a for a in [1, 2, 3] if a) if a)', m.UndefinedName) def test_undefinedWithErrorHandler(self): @@ -568,10 +568,10 @@ def test_undefinedInLoop(self): print(i) ''', m.UndefinedName) self.flakes(''' - [42 for i in range(i)] + x = [42 for i in range(i)] ''', m.UndefinedName) self.flakes(''' - (42 for i in range(i)) + x = (42 for i in range(i)) ''', m.UndefinedName) @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist') @@ -581,7 +581,7 @@ def test_definedFromLambdaInDictionaryComprehension(self): comprehension. """ self.flakes(''' - {lambda: id(x) for x in range(10)} + x = {lambda: id(x) for x in range(10)} ''') def test_definedFromLambdaInGenerator(self): @@ -590,7 +590,7 @@ def test_definedFromLambdaInGenerator(self): expression. """ self.flakes(''' - any(lambda: id(x) for x in range(10)) + x = any(lambda: id(x) for x in range(10)) ''') @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist') @@ -600,7 +600,7 @@ def test_undefinedFromLambdaInDictionaryComprehension(self): comprehension. """ self.flakes(''' - {lambda: id(y) for x in range(10)} + x = {lambda: id(y) for x in range(10)} ''', m.UndefinedName) def test_undefinedFromLambdaInComprehension(self): @@ -609,7 +609,7 @@ def test_undefinedFromLambdaInComprehension(self): expression. """ self.flakes(''' - any(lambda: id(y) for x in range(10)) + x = any(lambda: id(y) for x in range(10)) ''', m.UndefinedName)