diff --git a/src/pipeline/fqn.c b/src/pipeline/fqn.c index f4603dc80..f7b5b5efb 100644 --- a/src/pipeline/fqn.c +++ b/src/pipeline/fqn.c @@ -329,6 +329,9 @@ static char *resolve_js_relative(char *buf, size_t buf_size, const char *module_ continue; } if (seg_len == FQN_DOTDOT_LEN && seg_start[0] == '.' && seg_start[FQN_SEP_LEN] == '.') { + if (!buf[0]) { + return NULL; + } path_pop_segment(buf); continue; } diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 18f2b3134..ef3ba0f10 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -2115,6 +2115,37 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx return found; } +/* The File an explicit relative import (`./x`, `../x`) names exactly, for + * files whose File QN differs from the module QN Strategy 1 tries + * (`../styles/global.css`, `../layouts/Layout.astro`, #2558). Resolved in + * stack buffers by cbm_repo_rel_join, which refuses a path above the + * repository root. */ +static const cbm_gbuf_node_t *resolve_relative_file(const cbm_pipeline_ctx_t *ctx, + const char *source_rel, const char *module_path, + bool php_only) { + if (!source_rel || !module_path || module_path[0] != '.' || + !(module_path[1] == '/' || (module_path[1] == '.' && module_path[2] == '/'))) { + return NULL; + } + char dir[PKGMAP_PATH_BUF]; + int dir_len = snprintf(dir, sizeof(dir), "%s", source_rel); + if (dir_len < 0 || (size_t)dir_len >= sizeof(dir)) { + return NULL; + } + char *slash = strrchr(dir, '/'); + if (slash) { + *slash = '\0'; + } else { + dir[0] = '\0'; + } + char path[PKGMAP_PATH_BUF]; + if (!cbm_repo_rel_join(dir, module_path, path, sizeof(path))) { + return NULL; + } + const cbm_gbuf_node_t *file = cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, path); + return file && (!php_only || node_in_php_file(file)) ? file : NULL; +} + /* ── PHP PSR-4 class imports (#1186) ──────────────────────────────── */ typedef enum { @@ -2389,6 +2420,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies( } } + /* Strategy 1c: relative non-module files can have a File QN that does not + * match the module QN above. Resolve the exact file after module and + * sibling lookup. */ + const cbm_gbuf_node_t *rel_file = + resolve_relative_file(ctx, source_rel, imp->module_path, php_only); + if (rel_file) { + return rel_file; + } + /* Strategy 2: namespace map. `using App.Utils`, `import com.example.Foo`, * `use App\Utils\Helper` name a NAMESPACE (or a member of it) that the * path-based QN cannot express. Try the full module path and progressively diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index b5f1e7fbf..266cc255a 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -267,6 +267,102 @@ static int ei_edge_present(const EILangFile *files, int nfiles, const char *edge return got >= floor; } +typedef struct { + const char *importer; + const char *target; +} EIFileEdgeExpect; + +static int ei_file_edges_match(const EILangFile *files, int nfiles, const EIFileEdgeExpect *want, + int nwant, int expected_count) { + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, files, nfiles); + int ok = store && cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") == expected_count; + if (store && !ok) { + int got = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS"); + fprintf(stderr, " [IMPORTS] FAIL count=%d expected==%d\n", got, expected_count); + } + for (int i = 0; store && i < nwant; i++) { + int64_t source_id = ei_node_id_for_file_label(store, lp.project, want[i].importer, "File"); + cbm_edge_t *edges = NULL; + int edge_count = 0; + int found = 0; + if (source_id > 0 && + cbm_store_find_edges_by_source_type(store, source_id, "IMPORTS", &edges, &edge_count) == + CBM_STORE_OK) { + for (int j = 0; j < edge_count; j++) { + cbm_node_t target; + memset(&target, 0, sizeof(target)); + if (cbm_store_find_node_by_id(store, edges[j].target_id, &target) == CBM_STORE_OK) { + found = target.file_path && strcmp(target.file_path, want[i].target) == 0; + cbm_node_free_fields(&target); + } + if (found) { + break; + } + } + } + cbm_store_free_edges(edges, edge_count); + if (!found) { + fprintf(stderr, " [IMPORTS] FAIL %s -> %s\n", want[i].importer, want[i].target); + ok = 0; + } + } + ei_cleanup(&lp, store); + return ok; +} + +TEST(ei_relative_astro_css_imports_issue2558) { + static const EILangFile f[] = { + {"Badge.astro", "outside\n"}, + {"src/components/shared/Button.astro", + "---\nexport function buttonLabel(t: string): string { return t; }\n---\n\n"}, + {"src/components/Badge.astro", "badge\n"}, + {"src/components/Card.astro", + "---\nimport Badge from './Badge.astro';\n" + "import Button from './shared/Button.astro';\n---\n