diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index f7ee7bafa..692aeb0e9 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -1707,6 +1707,24 @@ static const char *extract_keyword_url(CBMExtractCtx *ctx, TSNode arg) { return extract_string_value(ctx, val_node); } +// `prefixVar + "/route"` (Go's idiomatic configurable-base-path pattern): +// recover the literal suffix. A right side that is not itself a literal is +// left unresolved rather than guessed (issue #1249). +static const char *extract_binary_concat_suffix(CBMExtractCtx *ctx, TSNode node) { + TSNode op_node = ts_node_child_by_field_name(node, TS_FIELD("operator")); + if (!ts_node_is_null(op_node)) { + char *op = cbm_node_text(ctx->arena, op_node, ctx->source); + if (!op || strcmp(op, "+") != 0) { + return NULL; + } + } + TSNode rhs = ts_node_child_by_field_name(node, TS_FIELD("right")); + if (ts_node_is_null(rhs) || !is_string_like(ts_node_type(rhs))) { + return NULL; + } + return strip_and_validate_string_arg(ctx->arena, cbm_node_text(ctx->arena, rhs, ctx->source)); +} + // Try to extract URL/topic from a positional argument (string or constant). static const char *extract_positional_url(CBMExtractCtx *ctx, TSNode arg, const char *ak) { /* JS/TS template literals: `/things/${id}` normalizes to "/things/{}" so the @@ -1717,6 +1735,12 @@ static const char *extract_positional_url(CBMExtractCtx *ctx, TSNode arg, const return strip_and_validate_string_arg(ctx->arena, (char *)flat); } } + if (strcmp(ak, "binary_expression") == 0) { + const char *suffix = extract_binary_concat_suffix(ctx, arg); + if (suffix) { + return suffix; + } + } if (is_string_like(ak)) { char *text = cbm_node_text(ctx->arena, arg, ctx->source); const char *validated = strip_and_validate_string_arg(ctx->arena, text); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 0122200e2..66aa7f5fa 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3181,6 +3181,57 @@ TEST(extract_ts_template_string_url_issue1006) { PASS(); } +/* Issue #1249: a mux route built as `configVar + "/literal"` (Go's idiomatic + * configurable-base-path pattern) must index the literal suffix, both for a + * route registration and for an outbound URL built the same way. A real BFF + * with 47 such registrations produced only 9 Route nodes before this fix. */ +TEST(extract_go_binary_concat_url_issue1249) { + CBMFileResult *r = extract("package main\n" + "import \"net/http\"\n" + "func setup(mux *http.ServeMux, base string) {\n" + " mux.HandleFunc(base+\"/login\", loginHandler)\n" + "}\n" + "func report(host string, port string) {\n" + " http.Get(\"http://\" + host + \":\" + port + \"/log\")\n" + "}\n", + CBM_LANG_GO, "t", "routes.go"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + const CBMCall *reg = find_call_by_callee(r, "mux.HandleFunc"); + ASSERT_NOT_NULL(reg); + ASSERT_NOT_NULL(reg->first_string_arg); + ASSERT_STR_EQ(reg->first_string_arg, "/login"); + + const CBMCall *out = find_call_by_callee(r, "http.Get"); + ASSERT_NOT_NULL(out); + ASSERT_NOT_NULL(out->first_string_arg); + ASSERT_STR_EQ(out->first_string_arg, "/log"); + + cbm_free_result(r); + PASS(); +} + +/* Same issue: when the right side of the concatenation is not itself a + * literal (`base + suffixVar`), there is no literal route to recover. The + * fix must leave this unresolved rather than fabricate a path. */ +TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249) { + CBMFileResult *r = extract("package main\n" + "func setup(mux *http.ServeMux, base string, suffix string) {\n" + " mux.HandleFunc(base+suffix, dynHandler)\n" + "}\n", + CBM_LANG_GO, "t", "routes.go"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + const CBMCall *reg = find_call_by_callee(r, "mux.HandleFunc"); + ASSERT_NOT_NULL(reg); + ASSERT_NULL(reg->first_string_arg); + + cbm_free_result(r); + PASS(); +} + /* Reproduce-first: Java module QN must derive from the CONTAINING DIRECTORY, not * the filename stem, so a top-level class `Outer` in `Outer.java` is `t.Outer`, @@ -5315,6 +5366,8 @@ SUITE(extraction) { RUN_TEST(extract_java_method_annotations_issue382); RUN_TEST(extract_java_jaxrs_path_composition_issue1005); RUN_TEST(extract_ts_template_string_url_issue1006); + RUN_TEST(extract_go_binary_concat_url_issue1249); + RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249); RUN_TEST(extract_java_no_double_class_qn); RUN_TEST(extract_go_no_filename_in_module_qn); RUN_TEST(extract_large_ts_has_functions_issue213);