@@ -76,6 +76,7 @@ class _Scope:
7676 __slots__ = (
7777 "parent" , "funcs" , "bindings" , "imports" , "mod_imports" , "blocked" ,
7878 "literal_types" , "instance_types" , "call_assigns" , "return_ctors" ,
79+ "return_calls" , "loopvar_sources" ,
7980 )
8081
8182 def __init__ (self , parent : Optional ["_Scope" ]) -> None :
@@ -100,6 +101,12 @@ def __init__(self, parent: Optional["_Scope"]) -> None:
100101 self .call_assigns : Dict [str , ast .expr ] = {}
101102 # bare class names this scope's `return C(...)` statements construct
102103 self .return_ctors : set = set ()
104+ # func expressions of every `return <call>(...)` in this scope, for
105+ # chained return summaries (`return self.build_response(...)`)
106+ self .return_calls : List [ast .expr ] = []
107+ # loop variables drawn from a self-attribute container:
108+ # `for k, v in self.adapters.items():` -> v: ("elem", "adapters")
109+ self .loopvar_sources : Dict [str , Tuple [str , str ]] = {}
103110
104111
105112def _module_qual (file_key : str ) -> str :
@@ -276,12 +283,23 @@ def _record_stmt(stmt: ast.AST, scope: _Scope) -> None:
276283 if isinstance (stmt .value .func , ast .Name ):
277284 scope .instance_types [tgt .id ] = stmt .value .func .id
278285 scope .call_assigns [tgt .id ] = stmt .value .func
286+ elif isinstance (stmt , (ast .For , ast .AsyncFor )):
287+ attr = _self_container_of (stmt .iter )
288+ if attr is not None :
289+ targets = (
290+ stmt .target .elts if isinstance (stmt .target , ast .Tuple ) else [stmt .target ]
291+ )
292+ # the value position: last element of a tuple target (items()),
293+ # or the single target (values()/direct iteration)
294+ val = targets [- 1 ]
295+ if isinstance (val , ast .Name ):
296+ scope .loopvar_sources [val .id ] = ("elem" , attr )
279297 elif isinstance (stmt , ast .Return ):
280298 if stmt .value is not None :
281- if isinstance (stmt .value , ast .Call ) and isinstance (
282- stmt .value .func , ast . Name
283- ):
284- scope .return_ctors .add (stmt .value .func .id )
299+ if isinstance (stmt .value , ast .Call ):
300+ scope . return_calls . append ( stmt .value .func )
301+ if isinstance ( stmt . value . func , ast . Name ):
302+ scope .return_ctors .add (stmt .value .func .id )
285303 elif isinstance (stmt .value , ast .Name ):
286304 # `cj = RequestsCookieJar(); ...; return cj` — resolved when
287305 # the summary is read, against this scope's ctor-typed locals.
@@ -293,6 +311,22 @@ def _record_stmt(stmt: ast.AST, scope: _Scope) -> None:
293311 scope .blocked .add (stmt .target .id )
294312
295313
314+ def _self_container_of (expr : ast .expr ) -> Optional [str ]:
315+ """``self.X`` / ``self.X.items()`` / ``self.X.values()`` -> ``"X"``."""
316+ node = expr
317+ if isinstance (node , ast .Call ) and isinstance (node .func , ast .Attribute ):
318+ if node .func .attr not in ("items" , "values" ):
319+ return None
320+ node = node .func .value
321+ if (
322+ isinstance (node , ast .Attribute )
323+ and isinstance (node .value , ast .Name )
324+ and node .value .id == "self"
325+ ):
326+ return node .attr
327+ return None
328+
329+
296330def _record_import_from (node : ast .ImportFrom , scope : _Scope ) -> None :
297331 """``from m import f [as g]``; relative spellings resolve at lookup time.
298332
@@ -811,6 +845,10 @@ def __init__(self) -> None:
811845 self .self_attr : Dict [Tuple [str , str ], Tuple [str , str ]] = {}
812846 self .return_class : Dict [str , Optional [str ]] = {}
813847 self .module_classes : Dict [str , Dict [str , PyClass ]] = {}
848+ self .owner_by_sig : Dict [str , Optional [PyClass ]] = {}
849+ # (class sig, attr) -> [(writer method name, value var name)] for
850+ # `self.attr[key] = value` container writes
851+ self .elem_writes : Dict [Tuple [str , str ], List [Tuple [str , str ]]] = {}
814852
815853 # -- construction ------------------------------------------------------
816854 def add_module (self , qual : str , mod : PyModule , tree : ast .Module ,
@@ -820,6 +858,7 @@ def add_module(self, qual: str, mod: PyModule, tree: ast.Module,
820858 self .classes_global .setdefault (name , []).append (cls )
821859 for caller , _owner in _iter_callables (mod ):
822860 self .func_by_sig [caller .signature ] = caller
861+ self .owner_by_sig [caller .signature ] = _owner
823862 self .by_name .setdefault (caller .name , []).append (caller .signature )
824863 names = [p .name for p in caller .parameters or []]
825864 self .param_names [caller .signature ] = names
@@ -838,6 +877,26 @@ def _collect_self_attrs(self, qual: str, tree: ast.Module,
838877 cls = classes .get (node .name )
839878 if cls is None :
840879 continue
880+ method_stack : Dict [int , str ] = {}
881+ for m in ast .walk (node ):
882+ if isinstance (m , (ast .FunctionDef , ast .AsyncFunctionDef )):
883+ for sub in ast .walk (m ):
884+ if not isinstance (sub , ast .Assign ):
885+ continue
886+ for tgt in sub .targets :
887+ if (
888+ isinstance (tgt , ast .Subscript )
889+ and isinstance (tgt .value , ast .Attribute )
890+ and isinstance (tgt .value .value , ast .Name )
891+ and tgt .value .value .id == "self"
892+ and isinstance (sub .value , ast .Name )
893+ ):
894+ # self.X[key] = value_name — the writer method
895+ # and value name; the value's type resolves
896+ # lazily (parameter votes land later)
897+ self .elem_writes .setdefault (
898+ (cls .signature , tgt .value .attr ), []
899+ ).append ((m .name , sub .value .id ))
841900 for sub in ast .walk (node ):
842901 if not isinstance (sub , ast .Assign ):
843902 continue
@@ -1029,10 +1088,9 @@ def bump(src: str, dst: str) -> None:
10291088 global_classes = oracle .classes_global ,
10301089 )
10311090 if sig is None :
1032- if site .receiver_expr :
1033- pending .append (
1034- (caller , owner , site , scope , mod , qual , classes )
1035- )
1091+ pending .append (
1092+ (caller , owner , site , scope , mod , qual , classes )
1093+ )
10361094 continue
10371095 oracle .vote (sig , site )
10381096 bump (caller .signature , sig )
@@ -1141,27 +1199,99 @@ def bump(src: str, dst: str) -> None:
11411199 # ---- interprocedural round (#148 extension): type the receivers the
11421200 # local pass could not, in a strict deterministic order, then resolve
11431201 # the method on the typed class. One round, no fixpoint.
1144- def _returned_ctor_class (callee , qual ):
1145- """Unique `return C(...)` inside *callee*, resolved to a class."""
1202+ def _container_elem_type (owner , attr , qual ):
1203+ """`self.attr[k] = value` writers vote the container's element type."""
1204+ if owner is None :
1205+ return None
1206+ cands = set ()
1207+ for method_name , value_name in oracle .elem_writes .get (
1208+ (owner .signature , attr ), []
1209+ ):
1210+ writer = (owner .callables or {}).get (method_name )
1211+ if writer is None :
1212+ continue
1213+ t = oracle .param_type (writer , value_name , qual )
1214+ if t is not None and t [0 ] == "class" :
1215+ cands .add (t [1 ].name )
1216+ return next (iter (cands )) if len (cands ) == 1 else None
1217+
1218+ _ret_memo : Dict [str , Optional [Tuple ]] = {}
1219+
1220+ def _returned_summary (callee , depth = 0 ):
1221+ """What does *callee* return? -> ("class", PyClass) |
1222+ ("callable", sig) | None. Memoized, cycle-safe, depth-capped —
1223+ chains through `return self.m(...)` / `return f(...)`.
1224+ """
1225+ if callee is None or depth > 4 :
1226+ return None
1227+ key = callee .signature
1228+ if key in _ret_memo :
1229+ return _ret_memo [key ]
1230+ _ret_memo [key ] = None # cycle guard
1231+ result = None
11461232 for home_qual , (hmod , hfacts , hclasses ) in sorted (module_ctx .items ()):
1147- if not callee . signature .startswith (home_qual + "." ):
1233+ if not key .startswith (home_qual + "." ):
11481234 continue
11491235 cscope = _scope_for_callable (callee , hfacts .by_def , hfacts .module_scope )
11501236 if cscope is hfacts .module_scope :
1151- continue
1237+ break
11521238 names = set ()
1239+ fn_paths = set ()
11531240 for c in sorted (cscope .return_ctors ):
11541241 if c .startswith ("~" ):
1155- it = cscope .instance_types .get (c [1 :])
1242+ nm = c [1 :]
1243+ if nm in cscope .funcs :
1244+ fn_paths .add (cscope .funcs [nm ])
1245+ continue
1246+ it = cscope .instance_types .get (nm )
1247+ if it is None and nm in cscope .loopvar_sources :
1248+ # loop var drawn from a self container:
1249+ # `for k, v in self.adapters.items(): ... return v`
1250+ _ , attr = cscope .loopvar_sources [nm ]
1251+ own = oracle .owner_by_sig .get (key )
1252+ it = _container_elem_type (own , attr , home_qual )
11561253 if it is not None :
11571254 names .add (it )
11581255 else :
11591256 names .add (c )
11601257 hits = sorted ({n for n in names if n in hclasses })
1161- if len (hits ) == 1 and len (names ) == 1 :
1162- return hclasses [hits [0 ]]
1163- return None
1164- return None
1258+ if len (hits ) == 1 and len (names ) == 1 and not fn_paths :
1259+ result = ("class" , hclasses [hits [0 ]])
1260+ break
1261+ if len (fn_paths ) == 1 and not names :
1262+ sig = _signature_for_path (hmod , next (iter (fn_paths )))
1263+ if sig :
1264+ result = ("callable" , sig )
1265+ break
1266+ if len (cscope .return_calls ) == 1 and not names and not fn_paths :
1267+ fexpr = cscope .return_calls [0 ]
1268+ nxt_sig = None
1269+ if (
1270+ isinstance (fexpr , ast .Attribute )
1271+ and isinstance (fexpr .value , ast .Name )
1272+ and fexpr .value .id in ("self" , "cls" )
1273+ ):
1274+ own = oracle .owner_by_sig .get (key )
1275+ if own is not None :
1276+ nxt_sig = _resolve_self_call (
1277+ fexpr .attr , own , hclasses ,
1278+ hfacts .module_scope , hmod , home_qual , by_qual ,
1279+ global_classes = oracle .classes_global ,
1280+ )
1281+ else :
1282+ nxt_sig = _resolve_expr (
1283+ fexpr , cscope , hmod , home_qual , by_qual
1284+ )
1285+ nxt = oracle .func_by_sig .get (nxt_sig ) if nxt_sig else None
1286+ if nxt is not None :
1287+ result = _returned_summary (nxt , depth + 1 )
1288+ break
1289+ _ret_memo [key ] = result
1290+ return result
1291+
1292+ def _returned_ctor_class (callee , qual ):
1293+ r = _returned_summary (callee )
1294+ return r [1 ] if r is not None and r [0 ] == "class" else None
11651295
11661296 def _typed_receiver (caller , owner , name , scope , mod , qual , classes ):
11671297 t = oracle .param_type (caller , name , qual )
@@ -1224,9 +1354,56 @@ def _method_on(t, method, qual):
12241354 )
12251355
12261356 remaining = pending
1227- for _round in (1 , 2 ):
1357+ _MAX_ROUNDS = 8 # monotone: resolutions only grow; cap is a safety net
1358+ for _round in range (_MAX_ROUNDS ):
1359+ made_progress = False
12281360 still : List [Tuple ] = []
12291361 for caller , owner , site , scope , mod , qual , classes in remaining :
1362+ if not (site .receiver_expr or "" ):
1363+ # bare call of a variable holding a returned closure:
1364+ # `compute = make_compute(...); compute(...)`
1365+ sig = None
1366+ s_ = scope
1367+ while s_ is not None :
1368+ if site .method_name in s_ .call_assigns :
1369+ fexpr = s_ .call_assigns [site .method_name ]
1370+ fsig = None
1371+ if (
1372+ isinstance (fexpr , ast .Attribute )
1373+ and isinstance (fexpr .value , ast .Name )
1374+ and fexpr .value .id in ("self" , "cls" )
1375+ and owner is not None
1376+ ):
1377+ fsig = _resolve_self_call (
1378+ fexpr .attr , owner , classes ,
1379+ module_ctx [qual ][1 ].module_scope , mod , qual ,
1380+ by_qual , global_classes = oracle .classes_global ,
1381+ )
1382+ else :
1383+ fsig = _resolve_expr (fexpr , s_ , mod , qual , by_qual )
1384+ summ = _returned_summary (oracle .func_by_sig .get (fsig )) if fsig else None
1385+ if summ is not None and summ [0 ] == "callable" :
1386+ sig = summ [1 ]
1387+ break
1388+ if (
1389+ site .method_name in s_ .blocked
1390+ or site .method_name in s_ .bindings
1391+ or site .method_name in s_ .funcs
1392+ or site .method_name in s_ .imports
1393+ or site .method_name in s_ .mod_imports
1394+ ):
1395+ break
1396+ s_ = s_ .parent
1397+ if sig is not None :
1398+ oracle .vote (sig , site )
1399+ bump (caller .signature , sig )
1400+ resolutions [
1401+ (caller .signature , f"{ site .start_line } :{ site .start_column } " )
1402+ ] = sig
1403+ made_progress = True
1404+ else :
1405+ still .append ((caller , owner , site , scope , mod , qual , classes ))
1406+ continue
12301407 if (site .receiver_expr or "" ) in ("self" , "cls" ) and owner is not None :
12311408 sig = _resolve_self_call (
12321409 site .method_name , owner , classes ,
@@ -1277,7 +1454,10 @@ def _method_on(t, method, qual):
12771454 resolutions [
12781455 (caller .signature , f"{ site .start_line } :{ site .start_column } " )
12791456 ] = sig
1457+ made_progress = True
12801458 remaining = still
1459+ if not made_progress :
1460+ break
12811461
12821462 iter_still : List [Tuple ] = []
12831463 for caller , owner , name , scope , mod , qual , classes in pending_iter :
@@ -1298,7 +1478,12 @@ def _method_on(t, method, qual):
12981478 # bounded per site so a common name cannot explode the graph.
12991479 _FAN_CAP = 1024 # pathology guard only; Joern's widest observed fan is 222
13001480 for caller , owner , site , scope , mod , qual , classes in remaining :
1301- for sig in (oracle .by_name .get (site .method_name ) or [])[:_FAN_CAP ]:
1481+ cands = oracle .by_name .get (site .method_name ) or []
1482+ if not (site .receiver_expr or "" ):
1483+ # a bare name can never invoke a method (no receiver at runtime)
1484+ # — only module-level functions are legal targets
1485+ cands = [c for c in cands if oracle .owner_by_sig .get (c ) is None ]
1486+ for sig in cands [:_FAN_CAP ]:
13021487 if sig != caller .signature :
13031488 bump (caller .signature , sig )
13041489 for caller , mname in iter_still :
0 commit comments