Skip to content

Commit ae9d62c

Browse files
committed
Improved assignment handling with sorting
1 parent e608777 commit ae9d62c

File tree

2 files changed

+97
-30
lines changed

2 files changed

+97
-30
lines changed

dowsing/setuptools/setup_py_parsing.py

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from typing import Any, Dict, Optional
99

1010
import 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

1318
from ..types import Distribution
1419
from .setup_and_metadata import SETUP_ARGS
@@ -124,7 +129,12 @@ def leave_Call(
124129

125130

126131
class 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 "??"

dowsing/tests/setuptools.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,32 @@ def test_add_items(self) -> None:
344344
self.assertEqual(d.name, "aaaa1111")
345345
self.assertEqual(d.packages, ["a", "b", "c"])
346346
self.assertEqual(d.classifiers, "??")
347+
348+
def test_self_reference_assignments(self) -> None:
349+
d = self._read(
350+
"""\
351+
from setuptools import setup
352+
353+
version = "base"
354+
name = "foo"
355+
name += "bar"
356+
version = version + ".suffix"
357+
358+
classifiers = [
359+
"123",
360+
"abc",
361+
]
362+
363+
if True:
364+
classifiers = classifiers + ["xyz"]
365+
366+
setup(
367+
name=name,
368+
version=version,
369+
classifiers=classifiers,
370+
)
371+
"""
372+
)
373+
self.assertEqual(d.name, "foobar")
374+
self.assertEqual(d.version, "base.suffix")
375+
self.assertListEqual(d.classifiers, ["123", "abc", "xyz"])

0 commit comments

Comments
 (0)