Skip to content

Commit 4d26a25

Browse files
authored
Break-glass merge PR #2520
2 parents bf625b0 + 87a815a commit 4d26a25

15 files changed

Lines changed: 2148 additions & 144 deletions

‎internal/cbm/lsp/php_lsp.c‎

Lines changed: 194 additions & 27 deletions
Large diffs are not rendered by default.

‎internal/cbm/lsp/php_lsp.h‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
#include "../cbm.h"
88
#include "go_lsp.h" /* CBMLSPDef reused across languages */
99

10+
/* Per-file typed import evidence. NULL targets means unknown; otherwise
11+
* targets has one slot per represented import, NULL only for a binding whose
12+
* absence was established. Any unsupported unresolved binding makes the whole
13+
* file unknown. The array is caller-owned; strings and import records borrowed. */
14+
typedef struct {
15+
const CBMImportArray *imports;
16+
const char **targets;
17+
} CBMPHPImportBindings;
18+
1019
/* PHPLSPContext — per-file state for PHP type-aware call resolution.
1120
* Mirrors GoLSPContext / CLSPContext structure. */
1221
typedef struct {
@@ -28,6 +37,13 @@ typedef struct {
2837
int use_count;
2938
int use_cap;
3039

40+
/* Cross-file mode: dotted targets of the file's class `use` declarations
41+
* that the pipeline's import map left unresolved (vendor / unmapped
42+
* namespaces, #1186). A type named through one is outside the project: it
43+
* never matches a project type by short name. */
44+
const char **external_use_qns;
45+
int external_use_count;
46+
3147
/* Current function/method/class context. */
3248
const char *enclosing_func_qn;
3349
const char *enclosing_class_qn; /* NULL outside class body */
@@ -116,6 +132,15 @@ void cbm_run_php_lsp_cross(
116132
TSTree *cached_tree, /* NULL = parse internally */
117133
CBMResolvedCallArray *out);
118134

135+
/* Complete typed bindings enable external receiver evidence. */
136+
void cbm_run_php_lsp_cross_with_bindings(CBMArena *arena, const char *source, int source_len,
137+
const char *module_qn, CBMLSPDef *defs, int def_count,
138+
const char **import_names, const char **import_qns,
139+
int import_count,
140+
TSTree *cached_tree, /* NULL = parse internally */
141+
CBMResolvedCallArray *out,
142+
const CBMPHPImportBindings *bindings);
143+
119144
/* --- Batch cross-file LSP --- */
120145

121146
/* Per-file input for batch PHP LSP processing. */

‎src/pipeline/lsp_resolve.h‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "graph_buffer/graph_buffer.h"
2828
#include "foundation/constants.h"
2929
#include "pipeline/pipeline.h" /* cbm_registry_unique_field_qn, cbm_resolution_t */
30+
#include "service_patterns.h" /* cbm_service_pattern_match for cbm_pipeline_service_name */
3031

3132
#include <ctype.h>
3233
#include <stdio.h>
@@ -333,6 +334,34 @@ static inline int cbm_pipeline_invocation_site_rank(const CBMResolvedCall *resol
333334
return call->requires_lsp_resolution || call->source_origin != resolved->source_origin ? 0 : 1;
334335
}
335336

337+
/* A resolver row that proves the call's receiver is a type OUTSIDE the project:
338+
* PHP `$r->m()` / `R::m()` where `R` is imported from a vendor namespace
339+
* (#1186). When its target has no graph node, it rules out every name-only
340+
* fallback: the method of a vendor class is not a same-named project method. */
341+
static inline bool cbm_pipeline_lsp_external_receiver(const CBMResolvedCall *resolved) {
342+
return resolved && resolved->strategy &&
343+
strcmp(resolved->strategy, "php_external_receiver") == 0;
344+
}
345+
346+
/* The service-pattern name of a call: its callee text, or, when that names no
347+
* service and the LSP proved the receiver a vendor class, the vendor method
348+
* (`GuzzleClient::get` -> `GuzzleHttp.Client.get`). The extractor names a
349+
* scoped call by its method alone, and such a call never reaches the
350+
* registry, so the vendor class is its only service evidence (#1186). */
351+
static inline const char *cbm_pipeline_service_name(const CBMCall *call,
352+
const CBMResolvedCall *external,
353+
cbm_svc_kind_t *kind) {
354+
*kind = cbm_service_pattern_match(call->callee_name);
355+
if (*kind == CBM_SVC_NONE && cbm_pipeline_lsp_external_receiver(external) &&
356+
external->callee_qn && external->callee_qn[0]) {
357+
*kind = cbm_service_pattern_match(external->callee_qn);
358+
if (*kind != CBM_SVC_NONE) {
359+
return external->callee_qn;
360+
}
361+
}
362+
return call->callee_name;
363+
}
364+
336365
static inline bool cbm_pipeline_invocation_reason_join_strategy(const char *strategy) {
337366
return strategy &&
338367
(strcmp(strategy, "lsp_func_ptr") == 0 || strcmp(strategy, "lsp_callable_alias") == 0 ||

‎src/pipeline/pass_calls.c‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -457,10 +457,11 @@ static int64_t create_svc_route_node(cbm_pipeline_ctx_t *ctx, const char *url, c
457457

458458
/* Insert an edge, splicing the call-site line (,"line":N) in before the closing
459459
* brace when one was captured. Mirrors finalize_and_emit() on the parallel path
460-
* so CALLS edges carry their source line regardless of resolution path. Restricted
461-
* to CALLS: route/config edge props feed full-only predump passes
462-
* (create_route_nodes/create_data_flows), so altering them desyncs full vs
463-
* incremental indexing. */
460+
* so CALLS edges carry their source line regardless of resolution path. Line and
461+
* args here are restricted to CALLS; HTTP_CALLS / ASYNC_CALLS props are finished
462+
* by the parallel formatter itself (emit_http_async_edge), because route/service
463+
* edge props feed full-only predump passes (create_route_nodes /
464+
* create_data_flows) and must not differ between the two passes. */
464465
/* Append a ,"args":[{"i":0,"e":"<expr>","v":"<value>"},...] field onto a CALLS
465466
* edge's JSON props (the props buffer ends in '}'). The sequential pass omitted
466467
* this, so data_flow mode had no argument expressions to surface for small
@@ -585,8 +586,10 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
585586
* emit_http_async_service_edge: the old single format string closed the
586587
* method value's quote but not the broker's, emitting
587588
* "broker":"bullmq} on EVERY brokered ASYNC_CALLS edge — and its fixup
588-
* only handled truncation, which never fires in the normal case (#898). */
589-
char props[CBM_SZ_512];
589+
* only handled truncation, which never fires in the normal case (#898).
590+
* The call's args and the closing brace come from the parallel path's own
591+
* formatter, so a service edge is byte-identical whichever pass emits it. */
592+
char props[CBM_SZ_2K];
590593
int n = snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"url_path\":\"%s\"", esc_callee,
591594
esc_url);
592595
if (method && n > 0 && (size_t)n < sizeof(props)) {
@@ -595,11 +598,8 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
595598
if (broker && n > 0 && (size_t)n < sizeof(props)) {
596599
n += snprintf(props + n, sizeof(props) - (size_t)n, ",\"broker\":\"%s\"", broker);
597600
}
598-
if (n > 0 && (size_t)n < sizeof(props) - 1) {
599-
props[n] = '}';
600-
props[n + 1] = '\0';
601-
}
602-
calls_emit_edge(ctx->gbuf, source->id, route_id, edge_type, props, sizeof(props), call);
601+
cbm_pipeline_close_call_props(props, n, call, false);
602+
cbm_gbuf_insert_edge(ctx->gbuf, source->id, route_id, edge_type, props);
603603
}
604604

605605
/* Classify a resolved call and emit the appropriate edge. */
@@ -671,7 +671,8 @@ static const cbm_gbuf_node_t *calls_find_source(cbm_pipeline_ctx_t *ctx, const c
671671
static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBMFileResult *result,
672672
const char *rel, const char *module_qn, const char **imp_keys,
673673
const char **imp_vals, int imp_count, CBMLanguage lang,
674-
const CBMImportArray *imports, const char *route_mount) {
674+
const CBMImportArray *imports, const char *route_mount,
675+
const cbm_php_vendor_names_t *php_vendor) {
675676
const CBMResolvedCallArray *lsp_calls = &result->resolved_calls;
676677
const cbm_gbuf_node_t *source_node = calls_find_source(ctx, rel, call->enclosing_func_qn);
677678
if (!source_node) {
@@ -683,6 +684,12 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
683684
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
684685
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution_in_graph(
685686
lsp_calls, call, allow_tail, ctx->gbuf, ctx->project_name);
687+
/* #1186: a PHP call on a vendor type (`new Request`, or a receiver the LSP
688+
* typed as an imported vendor class) takes the LSP result or no target: a
689+
* same-named project symbol found by name alone is never the vendor's.
690+
* Service/route classification below still runs. Mirrors pass_parallel.c. */
691+
bool php_vendor_call = cbm_pipeline_php_vendor_call(php_vendor, result, call) ||
692+
cbm_pipeline_lsp_external_receiver(lsp);
686693
if (lsp) {
687694
bool exact_external_target = call->requires_lsp_resolution &&
688695
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
@@ -750,14 +757,19 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
750757
emit_http_async_edge(ctx, &routed, source_node, NULL, &svc_res, CBM_SVC_HTTP, false);
751758
return SKIP_ONE;
752759
}
753-
cbm_svc_kind_t csvc = cbm_service_pattern_match(call->callee_name);
760+
/* A PHP call on a proven vendor class is classified by that class
761+
* (#1186): it skips the registry below, so the route-verb fallback would
762+
* otherwise mint a route registration for `GuzzleClient::get('/x')`. An
763+
* LSP row with a graph target returned above. MUST match pass_parallel.c. */
764+
cbm_svc_kind_t csvc;
765+
const char *svc_name = cbm_pipeline_service_name(call, lsp, &csvc);
754766
if (csvc == CBM_SVC_HTTP || csvc == CBM_SVC_ASYNC) {
755767
const char *cu = call->first_string_arg;
756768
bool chas_url = cu && cu[0] != '\0' &&
757769
(cu[0] == '/' || strstr(cu, "://") != NULL ||
758770
(csvc == CBM_SVC_ASYNC && strlen(cu) > PAIR_LEN));
759771
if (chas_url) {
760-
cbm_resolution_t svc_res = {.qualified_name = call->callee_name,
772+
cbm_resolution_t svc_res = {.qualified_name = svc_name,
761773
.confidence = PC_SVC_PATTERN_CONF,
762774
.strategy = "service_pattern",
763775
.candidate_count = 0};
@@ -774,7 +786,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
774786
bool rust_external = lsp && cbm_pipeline_rust_external_target(
775787
lang, lsp->strategy, lsp->callee_qn, ctx->project_name);
776788
cbm_resolution_t res = {0};
777-
if (!rust_external) {
789+
if (!rust_external && !php_vendor_call) {
778790
res = cbm_registry_resolve_lang(ctx->registry, call->callee_name, module_qn, imp_keys,
779791
imp_vals, imp_count, lang);
780792
/* Cross-language veto: a name-only guess never binds another
@@ -1147,6 +1159,8 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
11471159
const char **imp_vals = NULL;
11481160
int imp_count = 0;
11491161
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count);
1162+
cbm_php_vendor_names_t php_vendor;
1163+
cbm_pipeline_php_vendor_names_build(ctx->gbuf, ctx->project_name, rel, result, &php_vendor);
11501164

11511165
/* Compute module QN for same-module resolution (directory-based for
11521166
* Java/Go so it matches their def-node QNs in the registry). */
@@ -1166,14 +1180,16 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
11661180
}
11671181
total_calls++;
11681182
if (resolve_single_call(ctx, call, result, rel, module_qn, imp_keys, imp_vals,
1169-
imp_count, files[i].language, &result->imports, route_mount)) {
1183+
imp_count, files[i].language, &result->imports, route_mount,
1184+
&php_vendor)) {
11701185
resolved++;
11711186
} else {
11721187
unresolved++;
11731188
}
11741189
}
11751190

11761191
free(module_qn);
1192+
cbm_pipeline_php_vendor_names_free(&php_vendor);
11771193
free_import_map(imp_keys, imp_vals, imp_count);
11781194
if (result_owned) {
11791195
cbm_free_result(result);

‎src/pipeline/pass_definitions.c‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -556,20 +556,25 @@ static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileRe
556556
free(file_qn);
557557
return 0;
558558
}
559-
for (int j = 0; j < result->imports.count; j++) {
560-
const CBMImport *imp = &result->imports.items[j];
561-
if (!imp->module_path) {
562-
continue;
563-
}
564-
const cbm_gbuf_node_t *target =
565-
cbm_pipeline_resolve_import_node(ctx, rel, file_qn, imp, namespace_map);
566-
if (target && target->id != source_node->id) {
567-
char esc_ln[CBM_SZ_128];
568-
cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : "");
569-
char imp_props[CBM_SZ_256];
570-
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln);
571-
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
572-
count++;
559+
if (cbm_language_for_filename(rel) == CBM_LANG_PHP) {
560+
count = cbm_pipeline_php_create_import_edges(ctx, result, rel, file_qn, source_node,
561+
namespace_map);
562+
} else {
563+
for (int j = 0; j < result->imports.count; j++) {
564+
const CBMImport *imp = &result->imports.items[j];
565+
if (!imp->module_path) {
566+
continue;
567+
}
568+
const cbm_gbuf_node_t *target =
569+
cbm_pipeline_resolve_import_node(ctx, rel, file_qn, imp, namespace_map);
570+
if (target && target->id != source_node->id) {
571+
char esc_ln[CBM_SZ_128];
572+
cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : "");
573+
char imp_props[CBM_SZ_256];
574+
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln);
575+
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
576+
count++;
577+
}
573578
}
574579
}
575580
free(file_qn);

0 commit comments

Comments
 (0)