@@ -61,7 +61,8 @@ static const char *lookup_url_builder(const CBMExtractCtx *ctx, const char *name
6161static int is_string_like (const char * kind ) {
6262 return (strcmp (kind , "string" ) == 0 || strcmp (kind , "string_literal" ) == 0 ||
6363 strcmp (kind , "interpreted_string_literal" ) == 0 ||
64- strcmp (kind , "raw_string_literal" ) == 0 || strcmp (kind , "string_content" ) == 0 );
64+ strcmp (kind , "raw_string_literal" ) == 0 || strcmp (kind , "string_content" ) == 0 ||
65+ strcmp (kind , "line_string_literal" ) == 0 );
6566}
6667
6768/* Strip surrounding quotes from a string, return arena-allocated copy */
@@ -2286,6 +2287,18 @@ static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
22862287 if (strcmp (ts_node_type (arg ), "argument" ) == 0 && ts_node_named_child_count (arg ) > 0 ) {
22872288 arg = ts_node_named_child (arg , 0 );
22882289 }
2290+ /* Swift wraps each argument in a value_argument that may lead with its
2291+ * label, so `data(from: url)` would otherwise yield the label `from`
2292+ * rather than the value. Step past a leading value_argument_label. */
2293+ if (strcmp (ts_node_type (arg ), "value_argument" ) == 0 &&
2294+ ts_node_named_child_count (arg ) > 0 ) {
2295+ TSNode val = ts_node_named_child (arg , 0 );
2296+ if (strcmp (ts_node_type (val ), "value_argument_label" ) == 0 &&
2297+ ts_node_named_child_count (arg ) > 1 ) {
2298+ val = ts_node_named_child (arg , 1 );
2299+ }
2300+ arg = val ;
2301+ }
22892302 const char * ak = ts_node_type (arg );
22902303
22912304 if (strcmp (ak , "keyword_argument" ) == 0 || strcmp (ak , "pair" ) == 0 ) {
@@ -2973,6 +2986,41 @@ static bool python_receiver_is_exempt(CBMExtractCtx *ctx, TSNode receiver) {
29732986 return false;
29742987}
29752988
2989+ /* Name bound by one Python parameter node, or NULL when the shape binds none.
2990+ * Covers every binding form a `parameters` / `lambda_parameters` list produces:
2991+ * a bare `identifier`, the `name` field of default/typed parameters, and the
2992+ * identifier under a `*args` / `**kwargs` splat. A shape with no identifier (the
2993+ * bare `*` keyword separator) yields NULL and simply matches nothing. */
2994+ /* True when the callee of a BARE Python call `foo()` is bound as a parameter of
2995+ * an enclosing function or lambda -- the bare-call counterpart of
2996+ * python_receiver_is_exempt above.
2997+ *
2998+ * A parameter binding shadows any module-level `foo` for the whole body, so
2999+ * resolving such a call to a project Function/Method by short name alone
3000+ * fabricates the edge BY CONSTRUCTION: `def _run_with_heavy_slot(run): run()`
3001+ * must not bind an unrelated `SatoriLive.run`. Unlike a receiver type this is
3002+ * decidable from the AST outright, with no flow analysis and no list of
3003+ * "generic-looking" callee names -- Python forbids `global` on a parameter, and
3004+ * a parameter is in scope for the entire body regardless of position.
3005+ *
3006+ * The answer is CARRIED BY THE WALK rather than recomputed here. The unified
3007+ * walk binds a def's or lambda's parameters when it opens that scope and
3008+ * unwinds them when it closes it, so this is an O(1) map lookup. Deciding it
3009+ * by ascending the tree instead -- with ts_node_parent() or with a copied walk
3010+ * cursor -- costs O(depth) per call, and since every level of f(f(f(...))) is
3011+ * itself a bare call, that is quadratic across the file: the 30,000-deep
3012+ * fixture in tests/test_stack_overflow.c turned it into a hang rather than a
3013+ * slowdown. An O(1) lookup needs no hop cap, so unlike a capped walk this can
3014+ * no longer fail open on deep-but-ordinary code.
3015+ *
3016+ * It still answers false if the walk's own tracking hit an allocation failure,
3017+ * which costs a suppression and never a true edge. */
3018+ static bool python_callee_is_bound_parameter (CBMExtractCtx * ctx , WalkState * state ,
3019+ TSNode callee_ident ) {
3020+ const char * callee_name = cbm_node_text (ctx -> arena , callee_ident , ctx -> source );
3021+ return cbm_walk_python_param_is_bound (state , callee_name );
3022+ }
3023+
29763024static bool is_objectscript_language (CBMLanguage language ) {
29773025 return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE ;
29783026}
@@ -3045,6 +3093,19 @@ static TSNode objectscript_call_args(TSNode node) {
30453093 : cbm_find_child_by_kind (macro_function , "method_args" );
30463094}
30473095
3096+ /* Swift models a call as a target expression plus a call_suffix, and its grammar
3097+ * declares no "arguments" field at all, so the generic field lookup finds
3098+ * nothing for every Swift call. Reach the argument list through the suffix
3099+ * instead. A trailing closure has a call_suffix with no value_arguments, which
3100+ * returns a null node and leaves the call without a string argument, as before. */
3101+ static TSNode swift_call_args (TSNode node ) {
3102+ TSNode suffix = cbm_find_child_by_kind (node , "call_suffix" );
3103+ if (ts_node_is_null (suffix )) {
3104+ return (TSNode ){0 };
3105+ }
3106+ return cbm_find_child_by_kind (suffix , "value_arguments" );
3107+ }
3108+
30483109static bool node_has_token (TSNode node , const char * token ) {
30493110 uint32_t count = ts_node_child_count (node );
30503111 for (uint32_t i = 0 ; i < count ; i ++ ) {
@@ -3597,11 +3658,18 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
35973658 // (`accelerator.print()` must not bind MockAccelerator.print).
35983659 // Imported receivers stay unflagged: module.function() is Python's
35993660 // canonical cross-file call and the import map resolves it.
3661+ // A BARE Python call foo() whose callee is bound as a parameter of an
3662+ // enclosing scope cannot be the module-level foo, so short-name
3663+ // resolution would fabricate the edge (`def f(run): run()` must not
3664+ // bind SatoriLive.run). Distinct from is_method: there is no receiver
3665+ // here, so the weak-member guard cannot see this class at all.
36003666 if (ctx -> language == CBM_LANG_PYTHON && strcmp (ts_node_type (node ), "call" ) == 0 ) {
36013667 TSNode fn = ts_node_child_by_field_name (node , TS_FIELD ("function" ));
36023668 if (!ts_node_is_null (fn ) && strcmp (ts_node_type (fn ), "attribute" ) == 0 ) {
36033669 TSNode obj = ts_node_child_by_field_name (fn , TS_FIELD ("object" ));
36043670 call .is_method = !python_receiver_is_exempt (ctx , obj );
3671+ } else if (!ts_node_is_null (fn ) && strcmp (ts_node_type (fn ), "identifier" ) == 0 ) {
3672+ call .callee_is_locally_bound = python_callee_is_bound_parameter (ctx , state , fn );
36053673 }
36063674 }
36073675 // TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
@@ -3634,6 +3702,10 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
36343702 if (ts_node_is_null (args ) && is_objectscript_language (ctx -> language )) {
36353703 args = objectscript_call_args (node );
36363704 }
3705+ // Swift has no "arguments" field either; its args hang off call_suffix.
3706+ if (ts_node_is_null (args ) && ctx -> language == CBM_LANG_SWIFT ) {
3707+ args = swift_call_args (node );
3708+ }
36373709 if (!ts_node_is_null (args )) {
36383710 call .first_string_arg = extract_url_or_topic_arg (ctx , args );
36393711 /* #952: routes registered inside Laravel `prefix()->group()`
0 commit comments