Skip to content

Commit 9dd73ec

Browse files
committed
Handle subtrees as input to checker
This patch essentially adds the the functionality for the Checker to handle subtrees. Along with this args have also been parsed through handlers. Closes #338
1 parent d6f54c3 commit 9dd73ec

File tree

4 files changed

+212
-14
lines changed

4 files changed

+212
-14
lines changed

pyflakes/checker.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,15 @@ def iter_child_nodes(node, omit=None, _fields_order=_FieldsOrder()):
9595
"""
9696
Yield all direct child nodes of *node*, that is, all fields that
9797
are nodes and all items of fields that are lists of nodes.
98+
99+
:param node: AST node to be iterated upon
100+
:param omit: String or tuple of strings denoting the
101+
attributes of the node to be omitted from
102+
further parsing
103+
:param _fields_order: Order of AST node fields
98104
"""
99105
for name in _fields_order[node.__class__]:
100-
if name == omit:
106+
if omit and name in omit:
101107
continue
102108
field = getattr(node, name, None)
103109
if isinstance(field, ast.AST):
@@ -472,6 +478,17 @@ class Checker(object):
472478
callables which are deferred assignment checks.
473479
"""
474480

481+
_ast_node_scope = {
482+
ast.Module: ModuleScope,
483+
ast.ClassDef: ClassScope,
484+
ast.FunctionDef: FunctionScope,
485+
ast.Lambda: FunctionScope,
486+
ast.ListComp: GeneratorScope,
487+
ast.SetComp: GeneratorScope,
488+
ast.GeneratorExp: GeneratorScope,
489+
ast.DictComp: GeneratorScope,
490+
}
491+
475492
nodeDepth = 0
476493
offset = None
477494
traceTree = False
@@ -493,7 +510,10 @@ def __init__(self, tree, filename='(none)', builtins=None,
493510
if builtins:
494511
self.builtIns = self.builtIns.union(builtins)
495512
self.withDoctest = withDoctest
496-
self.scopeStack = [ModuleScope()]
513+
try:
514+
self.scopeStack = [Checker._ast_node_scope[type(tree)]()]
515+
except KeyError:
516+
raise RuntimeError('No scope implemented for the node %r' % tree)
497517
self.exceptHandlers = [()]
498518
self.root = tree
499519
self.handleChildren(tree)
@@ -643,6 +663,18 @@ def descendantOf(self, node, ancestors, stop):
643663
return True
644664
return False
645665

666+
def _getAncestor(self, node, ancestor_type):
667+
parent = node
668+
while True:
669+
if parent is self.root:
670+
return None
671+
parent = self.getParent(parent)
672+
if isinstance(parent, ancestor_type):
673+
return parent
674+
675+
def getScopeNode(self, node):
676+
return self._getAncestor(node, tuple(Checker._ast_node_scope.keys()))
677+
646678
def differentForks(self, lnode, rnode):
647679
"""True, if lnode and rnode are located on different forks of IF/TRY"""
648680
ancestor = self.getCommonAncestor(lnode, rnode, self.root)
@@ -790,6 +822,8 @@ def handleNodeStore(self, node):
790822
binding = Binding(name, node)
791823
elif name == '__all__' and isinstance(self.scope, ModuleScope):
792824
binding = ExportBinding(name, node.parent, self.scope)
825+
elif isinstance(getattr(node, 'ctx', None), ast.Param):
826+
binding = Argument(name, self.getScopeNode(node))
793827
else:
794828
binding = Assignment(name, node)
795829
self.addBinding(node, binding)
@@ -1103,13 +1137,12 @@ def NAME(self, node):
11031137
and isinstance(node.parent, ast.Call)):
11041138
# we are doing locals() call in current scope
11051139
self.scope.usesLocals = True
1106-
elif isinstance(node.ctx, (ast.Store, ast.AugStore)):
1140+
elif isinstance(node.ctx, (ast.Store, ast.AugStore, ast.Param)):
11071141
self.handleNodeStore(node)
11081142
elif isinstance(node.ctx, ast.Del):
11091143
self.handleNodeDelete(node)
11101144
else:
1111-
# must be a Param context -- this only happens for names in function
1112-
# arguments, but these aren't dispatched through here
1145+
# Unknown context
11131146
raise RuntimeError("Got impossible expression context: %r" % (node.ctx,))
11141147

11151148
def CONTINUE(self, node):
@@ -1225,15 +1258,8 @@ def addArgs(arglist):
12251258
def runFunction():
12261259

12271260
self.pushScope()
1228-
for name in args:
1229-
self.addBinding(node, Argument(name, node))
1230-
if isinstance(node.body, list):
1231-
# case for FunctionDefs
1232-
for stmt in node.body:
1233-
self.handleNode(stmt, node)
1234-
else:
1235-
# case for Lambdas
1236-
self.handleNode(node.body, node)
1261+
1262+
self.handleChildren(node, omit='decorator_list')
12371263

12381264
def checkUnusedAssignments():
12391265
"""
@@ -1257,6 +1283,18 @@ def checkReturnWithArgumentInsideGenerator():
12571283

12581284
self.deferFunction(runFunction)
12591285

1286+
def ARGUMENTS(self, node):
1287+
self.handleChildren(node, omit=('defaults', 'kw_defaults'))
1288+
if PY2:
1289+
scope_node = self.getScopeNode(node)
1290+
if node.vararg:
1291+
self.addBinding(node, Argument(node.vararg, scope_node))
1292+
if node.kwarg:
1293+
self.addBinding(node, Argument(node.kwarg, scope_node))
1294+
1295+
def ARG(self, node):
1296+
self.addBinding(node, Argument(node.arg, self.getScopeNode(node)))
1297+
12601298
def CLASSDEF(self, node):
12611299
"""
12621300
Check names used in a class definition, including its decorators, base

pyflakes/test/harness.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class TestCase(unittest.TestCase):
1717

1818
def flakes(self, input, *expectedOutputs, **kw):
1919
tree = compile(textwrap.dedent(input), "<test>", "exec", PyCF_ONLY_AST)
20+
if kw.get('is_segment'):
21+
tree = tree.body[0]
22+
kw.pop('is_segment')
2023
w = checker.Checker(tree, withDoctest=self.withDoctest, **kw)
2124
outputs = [type(o) for o in w.messages]
2225
expectedOutputs = list(expectedOutputs)

pyflakes/test/test_code_segment.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from pyflakes import messages as m
2+
from pyflakes.checker import (FunctionScope, ClassScope, ModuleScope,
3+
Argument, FunctionDefinition, Assignment)
4+
from pyflakes.test.harness import TestCase
5+
6+
7+
class TestCodeSegments(TestCase):
8+
"""
9+
Tests for segments of a module
10+
"""
11+
12+
def test_function_segment(self):
13+
self.flakes('''
14+
def foo():
15+
def bar():
16+
pass
17+
''', is_segment=True)
18+
19+
self.flakes('''
20+
def foo():
21+
def bar():
22+
x = 0
23+
''', m.UnusedVariable, is_segment=True)
24+
25+
def test_class_segment(self):
26+
self.flakes('''
27+
class Foo:
28+
class Bar:
29+
pass
30+
''', is_segment=True)
31+
32+
self.flakes('''
33+
class Foo:
34+
def bar():
35+
x = 0
36+
''', m.UnusedVariable, is_segment=True)
37+
38+
def test_scope_class(self):
39+
checker = self.flakes('''
40+
class Foo:
41+
x = 0
42+
def bar(a, b=1, *d, **e):
43+
pass
44+
''', is_segment=True)
45+
46+
scopes = checker.deadScopes
47+
module_scopes = [
48+
scope for scope in scopes if scope.__class__ is ModuleScope]
49+
class_scopes = [
50+
scope for scope in scopes if scope.__class__ is ClassScope]
51+
function_scopes = [
52+
scope for scope in scopes if scope.__class__ is FunctionScope]
53+
54+
# Ensure module scope is not present because we are analysing
55+
# the inner contents of Foo
56+
self.assertEqual(len(module_scopes), 0)
57+
self.assertEqual(len(class_scopes), 1)
58+
self.assertEqual(len(function_scopes), 1)
59+
60+
class_scope = class_scopes[0]
61+
function_scope = function_scopes[0]
62+
63+
self.assertIsInstance(class_scope, ClassScope)
64+
self.assertIsInstance(function_scope, FunctionScope)
65+
66+
self.assertIn('x', class_scope)
67+
self.assertIn('bar', class_scope)
68+
69+
self.assertIn('a', function_scope)
70+
self.assertIn('b', function_scope)
71+
self.assertIn('d', function_scope)
72+
self.assertIn('e', function_scope)
73+
74+
self.assertIsInstance(class_scope['bar'], FunctionDefinition)
75+
self.assertIsInstance(class_scope['x'], Assignment)
76+
77+
self.assertIsInstance(function_scope['a'], Argument)
78+
self.assertIsInstance(function_scope['b'], Argument)
79+
self.assertIsInstance(function_scope['d'], Argument)
80+
self.assertIsInstance(function_scope['e'], Argument)
81+
82+
def test_scope_function(self):
83+
checker = self.flakes('''
84+
def foo(a, b=1, *d, **e):
85+
def bar(f, g=1, *h, **i):
86+
pass
87+
''', is_segment=True)
88+
89+
scopes = checker.deadScopes
90+
module_scopes = [
91+
scope for scope in scopes if scope.__class__ is ModuleScope]
92+
function_scopes = [
93+
scope for scope in scopes if scope.__class__ is FunctionScope]
94+
95+
# Ensure module scope is not present because we are analysing
96+
# the inner contents of foo
97+
self.assertEqual(len(module_scopes), 0)
98+
self.assertEqual(len(function_scopes), 2)
99+
100+
function_scope_foo = function_scopes[1]
101+
function_scope_bar = function_scopes[0]
102+
103+
self.assertIsInstance(function_scope_foo, FunctionScope)
104+
self.assertIsInstance(function_scope_bar, FunctionScope)
105+
106+
self.assertIn('a', function_scope_foo)
107+
self.assertIn('b', function_scope_foo)
108+
self.assertIn('d', function_scope_foo)
109+
self.assertIn('e', function_scope_foo)
110+
self.assertIn('bar', function_scope_foo)
111+
112+
self.assertIn('f', function_scope_bar)
113+
self.assertIn('g', function_scope_bar)
114+
self.assertIn('h', function_scope_bar)
115+
self.assertIn('i', function_scope_bar)
116+
117+
self.assertIsInstance(function_scope_foo['bar'], FunctionDefinition)
118+
self.assertIsInstance(function_scope_foo['a'], Argument)
119+
self.assertIsInstance(function_scope_foo['b'], Argument)
120+
self.assertIsInstance(function_scope_foo['d'], Argument)
121+
self.assertIsInstance(function_scope_foo['e'], Argument)
122+
123+
self.assertIsInstance(function_scope_bar['f'], Argument)
124+
self.assertIsInstance(function_scope_bar['g'], Argument)
125+
self.assertIsInstance(function_scope_bar['h'], Argument)
126+
self.assertIsInstance(function_scope_bar['i'], Argument)

pyflakes/test/test_other.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,37 @@ def f(): global foo
11591159
def g(): foo = 'anything'; foo.is_used()
11601160
''')
11611161

1162+
def test_function_arguments(self):
1163+
"""
1164+
Test to traverse ARG and ARGUMENT handler
1165+
"""
1166+
self.flakes('''
1167+
def foo(a, b):
1168+
pass
1169+
''')
1170+
1171+
self.flakes('''
1172+
def foo(a, b, c=0):
1173+
pass
1174+
''')
1175+
1176+
self.flakes('''
1177+
def foo(a, b, c=0, *args):
1178+
pass
1179+
''')
1180+
1181+
self.flakes('''
1182+
def foo(a, b, c=0, *args, **kwargs):
1183+
pass
1184+
''')
1185+
1186+
@skipIf(version_info < (3, 3), "Python >= 3.3 only")
1187+
def test_function_arguments_python3(self):
1188+
self.flakes('''
1189+
def foo(a, b, c=0, *args, d=0, **kwargs):
1190+
pass
1191+
''')
1192+
11621193

11631194
class TestUnusedAssignment(TestCase):
11641195
"""

0 commit comments

Comments
 (0)