fix: suppress spurious route nodes

Tighten service-pattern matching so short framework ids such as gin. do not match unrelated QNs like plugin.*, while keeping separator-based wrappers such as requests_get working.

Reject bare HTTP-verb decorators like unittest.mock.patch as route decorators unless they have a receiver, and skip package manifests when promoting infra URL string refs to Route nodes.

Validation: build/c/test-runner builds; CBM_ONLY_SUITE=infrascan 4/4; extraction 195/195; depindex 32/32; lang_contract 31/31; edge_types_probe 52/52; isolated autorun index now stores 0 Route nodes.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-06-28 13:12:16 -04:00
parent cf481bd896
commit d15071c947
6 changed files with 91 additions and 6 deletions
+11
View File
@@ -1247,16 +1247,27 @@ static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char
if (!method) {
return false;
}
const char *dot = fn_text ? strrchr(fn_text, '.') : NULL;
bool has_receiver = dot && dot[SKIP_CHAR] != '\0';
bool is_generic_route = fn_text &&
(strcmp(dot ? dot + SKIP_CHAR : fn_text, "route") == 0 ||
strcmp(dot ? dot + SKIP_CHAR : fn_text, "api_route") == 0);
TSNode args = find_decorator_args(dchild);
if (!ts_node_is_null(args)) {
const char *path = extract_route_path_from_args(a, args, source);
if (path) {
if (!has_receiver && !is_generic_route) {
return false;
}
*out_path = path;
*out_method = method;
return true;
}
}
if (!has_receiver) {
return false;
}
*out_path = "/";
*out_method = method;
return true;
+31 -6
View File
@@ -95,6 +95,7 @@ static const lib_pattern_t http_libraries[] = {
{"Net::HTTP", CBM_SVC_HTTP, NULL},
/* PHP */
{"GuzzleHttp", CBM_SVC_HTTP, NULL},
{"Guzzle", CBM_SVC_HTTP, NULL},
{"guzzle", CBM_SVC_HTTP, NULL},
{"curl", CBM_SVC_HTTP, NULL},
@@ -534,17 +535,41 @@ static const method_suffix_t method_suffixes[] = {
/* ── Matching implementation ───────────────────────────────────── */
/* Check if any library identifier appears as a substring in the QN.
* Case-sensitive: "requests" matches "project.venv.requests.api.get"
* but not "Requests". Library names are specific enough to avoid
* false positives even with substring matching. */
static bool qn_token_char(char ch) {
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9');
}
static bool qn_pattern_occurrence_matches(const char *qn, const char *hit,
const char *pattern) {
size_t plen = strlen(pattern);
if (plen == 0) {
return false;
}
if (qn_token_char(pattern[0]) && hit > qn && qn_token_char(hit[-1])) {
return false;
}
if (qn_token_char(pattern[plen - 1]) && qn_token_char(hit[plen])) {
return false;
}
return true;
}
/* Check if a library identifier appears as a token-aligned substring in the QN.
* Case-sensitive: "requests" matches "project.venv.requests.api.get" but not
* "myrequests". The boundary check also prevents short framework ids like
* "gin." from firing inside unrelated names such as "plugin.". */
static const lib_pattern_t *match_qn(const char *qn, const lib_pattern_t *patterns) {
if (!qn || !qn[0]) {
return NULL;
}
for (int i = 0; patterns[i].library_id != NULL; i++) {
if (strstr(qn, patterns[i].library_id) != NULL) {
return &patterns[i];
const char *p = qn;
while ((p = strstr(p, patterns[i].library_id)) != NULL) {
if (qn_pattern_occurrence_matches(qn, p, patterns[i].library_id)) {
return &patterns[i];
}
p++;
}
}
return NULL;
+4
View File
@@ -24,6 +24,7 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6 };
#include "store/store.h"
#include "discover/discover.h"
#include "discover/userconfig.h"
#include "depindex/depindex.h"
#include "foundation/platform.h"
#include "foundation/compat_fs.h"
#include "foundation/log.h"
@@ -621,6 +622,9 @@ static void cbm_pipeline_process_infra_bindings(cbm_gbuf_t *gbuf, const cbm_file
}
static bool is_infra_file(const char *fp) {
if (cbm_is_manifest_path(fp)) {
return false;
}
return fp != NULL &&
(strstr(fp, ".yaml") != NULL || strstr(fp, ".yml") != NULL ||
strstr(fp, ".tf") != NULL || strstr(fp, ".hcl") != NULL || strstr(fp, ".toml") != NULL);
+27
View File
@@ -2645,6 +2645,32 @@ TEST(extract_java_method_annotations_issue382) {
PASS();
}
TEST(extract_python_mock_patch_is_not_route) {
CBMFileResult *r = extract("from unittest.mock import patch\n\n"
"@patch(\"subprocess.run\")\n"
"def test_cmd(mock_run):\n"
" pass\n\n"
"@app.patch(\"/items/{id}\")\n"
"def update_item():\n"
" pass\n",
CBM_LANG_PYTHON, "t", "test_routes.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *mocked = find_def_by_name(r, "test_cmd");
ASSERT_NOT_NULL(mocked);
ASSERT_NULL(mocked->route_path);
ASSERT_NULL(mocked->route_method);
const CBMDefinition *route = find_def_by_name(r, "update_item");
ASSERT_NOT_NULL(route);
ASSERT_STR_EQ(route->route_path, "/items/{id}");
ASSERT_STR_EQ(route->route_method, "PATCH");
cbm_free_result(r);
PASS();
}
/* Issue #213: large TS files were indexed as a File node with zero children. */
TEST(extract_large_ts_has_functions_issue213) {
enum { NFUNCS = 4000 };
@@ -3163,6 +3189,7 @@ SUITE(extraction) {
RUN_TEST(js_index_module_qn_not_collide_with_folder);
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
RUN_TEST(extract_python_mock_patch_is_not_route);
RUN_TEST(extract_large_ts_has_functions_issue213);
/* Per-function complexity metrics (Tier A) */
+15
View File
@@ -53,6 +53,20 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) {
PASS();
}
TEST(infrascan_service_pattern_match_uses_qn_boundaries) {
ASSERT_EQ(cbm_service_pattern_match(
"proj.plugins.autorun.tests.test_plugin._dispatch"),
CBM_SVC_NONE);
ASSERT_EQ(cbm_service_pattern_match("proj.myrequests.client.get"), CBM_SVC_NONE);
ASSERT_EQ(cbm_service_pattern_match("proj.gin.router.GET"), CBM_SVC_ROUTE_REG);
ASSERT_EQ(cbm_service_pattern_match("proj.express.router.get"), CBM_SVC_ROUTE_REG);
ASSERT_EQ(cbm_service_pattern_match("proj.venv.requests.api.get"), CBM_SVC_HTTP);
ASSERT_EQ(cbm_service_pattern_match("proj.service.requests_get"), CBM_SVC_HTTP);
ASSERT_EQ(cbm_service_pattern_match("proj.GuzzleHttp.Client.get"), CBM_SVC_HTTP);
PASS();
}
TEST(infrascan_route_nodes_skip_bad_http_url_paths) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/cbm_infrascan_route_guard");
ASSERT_NOT_NULL(gb);
@@ -133,6 +147,7 @@ TEST(infrascan_http_calls_join_matching_handler_route) {
SUITE(infrascan) {
RUN_TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths);
RUN_TEST(infrascan_service_pattern_match_uses_qn_boundaries);
RUN_TEST(infrascan_route_nodes_skip_bad_http_url_paths);
RUN_TEST(infrascan_http_calls_join_matching_handler_route);
}
+3
View File
@@ -165,6 +165,9 @@ int main(void) {
if (strstr("watcher", only_suite)) RUN_SUITE(watcher);
if (strstr("security", only_suite)) RUN_SUITE(security);
if (strstr("artifact", only_suite)) RUN_SUITE(artifact);
if (strstr("extraction", only_suite)) RUN_SUITE(extraction);
if (strstr("lang_contract", only_suite)) RUN_SUITE(lang_contract);
if (strstr("edge_types_probe", only_suite)) RUN_SUITE(edge_types_probe);
TEST_SUMMARY();
return 0;
}