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
@@ -179,7 +189,7 @@ def visit_Call(self, node: cst.Call) -> Optional[bool]:
179189 PRETEND_ARGV = ["setup.py" , "bdist_wheel" ]
180190
181191 def evaluate_in_scope (
182- self , item : cst .CSTNode , scope : Any , target_name : str = ""
192+ self , item : cst .CSTNode , scope : Any , target_name : str = "" , target_line : int = 0
183193 ) -> Any :
184194 qnames = self .get_metadata (QualifiedNameProvider , item )
185195
@@ -192,19 +202,30 @@ def evaluate_in_scope(
192202 elif isinstance (item , cst .Name ):
193203 name = item .value
194204 assignments = scope [name ]
195- for a in assignments :
196- # TODO: Only assignments "before" this node matter if in the
197- # same scope; really if we had a call graph and walked the other
198- # way, we could have a better idea of what has already happened.
199-
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+ # When recursing, only look at assignments above the "target line".
215+ for lineno , node in assignment_nodes :
200216 # Assign(
201217 # targets=[AssignTarget(target=Name(value="v"))],
202218 # value=SimpleString(value="'x'"),
203219 # )
204220 # TODO or an import...
205221 # TODO builtins have BuiltinAssignment
222+
223+ # we have recursed, likey due to `x = x + y` assignment or similar
224+ # self-referential evaluation, and can't
225+ if target_name and target_name == name and lineno >= target_line :
226+ continue
227+
206228 try :
207- node = a .node
208229 if node :
209230 parent = self .get_metadata (ParentNodeProvider , node )
210231 if parent :
@@ -214,35 +235,38 @@ def evaluate_in_scope(
214235 else :
215236 raise KeyError
216237 except (KeyError , AttributeError ):
217- return "??"
218-
219- # This presumes a single assignment
220- if not isinstance (gp , cst .Assign ) or len (gp .targets ) != 1 :
221- return "??" # TooComplicated(repr(gp))
238+ continue
222239
223240 try :
224241 scope = self .get_metadata (ScopeProvider , gp )
225242 except KeyError :
226243 # module scope isn't in the dict
227- return "??"
244+ continue
228245
229- # we have recursed, likey due to `x = x + y` assignment or similar
230- # self-referential evaluation
231- if target_name and target_name == name :
232- return "??"
246+ # This presumes a single assignment
247+ if isinstance (gp , cst .Assign ) and len (gp .targets ) == 1 :
248+ result = self .evaluate_in_scope (gp .value , scope , name , lineno )
249+ elif isinstance (parent , cst .AugAssign ):
250+ result = self .evaluate_in_scope (parent , scope , name , lineno )
251+ else :
252+ # too complicated?
253+ continue
233254
234255 # keep trying assignments until we get something other than ??
235- result = self .evaluate_in_scope (gp .value , scope , name )
236- if result and result != "??" :
256+ if result != "??" :
237257 return result
258+
238259 # give up
239260 return "??"
240261 elif isinstance (item , (cst .Tuple , cst .List )):
241262 lst = []
242263 for el in item .elements :
243264 lst .append (
244265 self .evaluate_in_scope (
245- el .value , self .get_metadata (ScopeProvider , el ), target_name
266+ el .value ,
267+ self .get_metadata (ScopeProvider , el ),
268+ target_name ,
269+ target_line ,
246270 )
247271 )
248272 if isinstance (item , cst .Tuple ):
@@ -260,10 +284,12 @@ def evaluate_in_scope(
260284 for arg in item .args :
261285 if isinstance (arg .keyword , cst .Name ):
262286 args [names .index (arg .keyword .value )] = self .evaluate_in_scope (
263- arg .value , scope , target_name
287+ arg .value , scope , target_name , target_line
264288 )
265289 else :
266- args [i ] = self .evaluate_in_scope (arg .value , scope , target_name )
290+ args [i ] = self .evaluate_in_scope (
291+ arg .value , scope , target_name , target_line
292+ )
267293 i += 1
268294
269295 # TODO clear ones that are still default
@@ -277,7 +303,7 @@ def evaluate_in_scope(
277303 for arg in item .args :
278304 if isinstance (arg .keyword , cst .Name ):
279305 d [arg .keyword .value ] = self .evaluate_in_scope (
280- arg .value , scope , target_name
306+ arg .value , scope , target_name , target_line
281307 )
282308 # TODO something with **kwargs
283309 return d
@@ -286,19 +312,19 @@ def evaluate_in_scope(
286312 for el2 in item .elements :
287313 if isinstance (el2 , cst .DictElement ):
288314 d [self .evaluate_in_scope (el2 .key , scope )] = self .evaluate_in_scope (
289- el2 .value , scope , target_name
315+ el2 .value , scope , target_name , target_line
290316 )
291317 return d
292318 elif isinstance (item , cst .Subscript ):
293- lhs = self .evaluate_in_scope (item .value , scope , target_name )
319+ lhs = self .evaluate_in_scope (item .value , scope , target_name , target_line )
294320 if isinstance (lhs , str ):
295321 # A "??" entry, propagate
296322 return "??"
297323
298324 # TODO: Figure out why this is Sequence
299325 if isinstance (item .slice [0 ].slice , cst .Index ):
300326 rhs = self .evaluate_in_scope (
301- item .slice [0 ].slice .value , scope , target_name
327+ item .slice [0 ].slice .value , scope , target_name , target_line
302328 )
303329 try :
304330 if isinstance (lhs , dict ):
@@ -312,8 +338,8 @@ def evaluate_in_scope(
312338 # LOG.warning(f"Omit2 {type(item.slice[0].slice)!r}")
313339 return "??"
314340 elif isinstance (item , cst .BinaryOperation ):
315- lhs = self .evaluate_in_scope (item .left , scope , target_name )
316- rhs = self .evaluate_in_scope (item .right , scope , target_name )
341+ lhs = self .evaluate_in_scope (item .left , scope , target_name , target_line )
342+ rhs = self .evaluate_in_scope (item .right , scope , target_name , target_line )
317343 if lhs == "??" or rhs == "??" :
318344 return "??"
319345 if isinstance (item .operator , cst .Add ):
@@ -323,6 +349,18 @@ def evaluate_in_scope(
323349 return "??"
324350 else :
325351 return "??"
352+ elif isinstance (item , cst .AugAssign ):
353+ lhs = self .evaluate_in_scope (item .target , scope , target_name , target_line )
354+ rhs = self .evaluate_in_scope (item .value , scope , target_name , target_line )
355+ if lhs == "??" or rhs == "??" :
356+ return "??"
357+ if isinstance (item .operator , cst .AddAssign ):
358+ try :
359+ return lhs + rhs
360+ except Exception :
361+ return "??"
362+ else :
363+ return "??"
326364 else :
327365 # LOG.warning(f"Omit1 {type(item)!r}")
328366 return "??"
0 commit comments