88from typing import Any , Dict , Optional
99
1010import libcst as cst
11- from libcst .metadata import ParentNodeProvider , QualifiedNameProvider , ScopeProvider
11+ from libcst .metadata import (
12+ ParentNodeProvider ,
13+ PositionProvider ,
14+ QualifiedNameProvider ,
15+ ScopeProvider ,
16+ )
1217
1318from ..types import Distribution
1419from .setup_and_metadata import SETUP_ARGS
@@ -124,7 +129,12 @@ def leave_Call(
124129
125130
126131class SetupCallAnalyzer (cst .CSTVisitor ):
127- METADATA_DEPENDENCIES = (ScopeProvider , ParentNodeProvider , QualifiedNameProvider )
132+ METADATA_DEPENDENCIES = (
133+ ScopeProvider ,
134+ ParentNodeProvider ,
135+ QualifiedNameProvider ,
136+ PositionProvider ,
137+ )
128138
129139 # TODO names resulting from other than 'from setuptools import setup'
130140 # TODO wrapper funcs that modify args
@@ -178,7 +188,9 @@ def visit_Call(self, node: cst.Call) -> Optional[bool]:
178188 BOOL_NAMES = {"True" : True , "False" : False , "None" : None }
179189 PRETEND_ARGV = ["setup.py" , "bdist_wheel" ]
180190
181- def evaluate_in_scope (self , item : cst .CSTNode , scope : Any ) -> Any :
191+ def evaluate_in_scope (
192+ self , item : cst .CSTNode , scope : Any , target_line : int = 0
193+ ) -> Any :
182194 qnames = self .get_metadata (QualifiedNameProvider , item )
183195
184196 if isinstance (item , cst .SimpleString ):
@@ -190,19 +202,36 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
190202 elif isinstance (item , cst .Name ):
191203 name = item .value
192204 assignments = scope [name ]
193- for a in assignments :
194- # TODO: Only assignments "before" this node matter if in the
195- # same scope; really if we had a call graph and walked the other
196- # way, we could have a better idea of what has already happened.
205+ assignment_nodes = sorted (
206+ (
207+ (self .get_metadata (PositionProvider , a .node ).start .line , a .node )
208+ for a in assignments
209+ if a .node
210+ ),
211+ reverse = True ,
212+ )
213+ # Walk assignments from bottom to top, evaluating them recursively.
214+ for lineno , node in assignment_nodes :
215+
216+ # When recursing, only look at assignments above the "target line".
217+ if target_line and lineno >= target_line :
218+ continue
197219
198220 # Assign(
199221 # targets=[AssignTarget(target=Name(value="v"))],
200222 # value=SimpleString(value="'x'"),
201223 # )
224+ #
225+ # AugAssign(
226+ # target=Name(value="v"),
227+ # operator=AddAssign(...),
228+ # value=SimpleString(value="'x'"),
229+ # )
230+ #
202231 # TODO or an import...
203232 # TODO builtins have BuiltinAssignment
233+
204234 try :
205- node = a .node
206235 if node :
207236 parent = self .get_metadata (ParentNodeProvider , node )
208237 if parent :
@@ -212,25 +241,37 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
212241 else :
213242 raise KeyError
214243 except (KeyError , AttributeError ):
215- return "??"
216-
217- # This presumes a single assignment
218- if not isinstance (gp , cst .Assign ) or len (gp .targets ) != 1 :
219- return "??" # TooComplicated(repr(gp))
244+ continue
220245
221246 try :
222247 scope = self .get_metadata (ScopeProvider , gp )
223248 except KeyError :
224249 # module scope isn't in the dict
225- return "??"
250+ continue
251+
252+ # This presumes a single assignment
253+ if isinstance (gp , cst .Assign ) and len (gp .targets ) == 1 :
254+ result = self .evaluate_in_scope (gp .value , scope , lineno )
255+ elif isinstance (parent , cst .AugAssign ):
256+ result = self .evaluate_in_scope (parent , scope , lineno )
257+ else :
258+ # too complicated?
259+ continue
226260
227- return self .evaluate_in_scope (gp .value , scope )
261+ # keep trying assignments until we get something other than ??
262+ if result != "??" :
263+ return result
264+
265+ # give up
266+ return "??"
228267 elif isinstance (item , (cst .Tuple , cst .List )):
229268 lst = []
230269 for el in item .elements :
231270 lst .append (
232271 self .evaluate_in_scope (
233- el .value , self .get_metadata (ScopeProvider , el )
272+ el .value ,
273+ self .get_metadata (ScopeProvider , el ),
274+ target_line ,
234275 )
235276 )
236277 if isinstance (item , cst .Tuple ):
@@ -248,10 +289,10 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
248289 for arg in item .args :
249290 if isinstance (arg .keyword , cst .Name ):
250291 args [names .index (arg .keyword .value )] = self .evaluate_in_scope (
251- arg .value , scope
292+ arg .value , scope , target_line
252293 )
253294 else :
254- args [i ] = self .evaluate_in_scope (arg .value , scope )
295+ args [i ] = self .evaluate_in_scope (arg .value , scope , target_line )
255296 i += 1
256297
257298 # TODO clear ones that are still default
@@ -264,26 +305,30 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
264305 d = {}
265306 for arg in item .args :
266307 if isinstance (arg .keyword , cst .Name ):
267- d [arg .keyword .value ] = self .evaluate_in_scope (arg .value , scope )
308+ d [arg .keyword .value ] = self .evaluate_in_scope (
309+ arg .value , scope , target_line
310+ )
268311 # TODO something with **kwargs
269312 return d
270313 elif isinstance (item , cst .Dict ):
271314 d = {}
272315 for el2 in item .elements :
273316 if isinstance (el2 , cst .DictElement ):
274317 d [self .evaluate_in_scope (el2 .key , scope )] = self .evaluate_in_scope (
275- el2 .value , scope
318+ el2 .value , scope , target_line
276319 )
277320 return d
278321 elif isinstance (item , cst .Subscript ):
279- lhs = self .evaluate_in_scope (item .value , scope )
322+ lhs = self .evaluate_in_scope (item .value , scope , target_line )
280323 if isinstance (lhs , str ):
281324 # A "??" entry, propagate
282325 return "??"
283326
284327 # TODO: Figure out why this is Sequence
285328 if isinstance (item .slice [0 ].slice , cst .Index ):
286- rhs = self .evaluate_in_scope (item .slice [0 ].slice .value , scope )
329+ rhs = self .evaluate_in_scope (
330+ item .slice [0 ].slice .value , scope , target_line
331+ )
287332 try :
288333 if isinstance (lhs , dict ):
289334 return lhs .get (rhs , "??" )
@@ -296,15 +341,29 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
296341 # LOG.warning(f"Omit2 {type(item.slice[0].slice)!r}")
297342 return "??"
298343 elif isinstance (item , cst .BinaryOperation ):
299- lhs = self .evaluate_in_scope (item .left , scope )
300- rhs = self .evaluate_in_scope (item .right , scope )
344+ lhs = self .evaluate_in_scope (item .left , scope , target_line )
345+ rhs = self .evaluate_in_scope (item .right , scope , target_line )
346+ if lhs == "??" or rhs == "??" :
347+ return "??"
301348 if isinstance (item .operator , cst .Add ):
302349 try :
303350 return lhs + rhs
304351 except Exception :
305352 return "??"
306353 else :
307354 return "??"
355+ elif isinstance (item , cst .AugAssign ):
356+ lhs = self .evaluate_in_scope (item .target , scope , target_line )
357+ rhs = self .evaluate_in_scope (item .value , scope , target_line )
358+ if lhs == "??" or rhs == "??" :
359+ return "??"
360+ if isinstance (item .operator , cst .AddAssign ):
361+ try :
362+ return lhs + rhs
363+ except Exception :
364+ return "??"
365+ else :
366+ return "??"
308367 else :
309368 # LOG.warning(f"Omit1 {type(item)!r}")
310369 return "??"
0 commit comments