Merge pull request #2595 from lorenzozanee/fix/relative-non-code-imports

fix(pipeline): resolve parent-relative Astro and CSS imports
This commit is contained in:
Martin Vogel
2026-10-20 19:39:32 +02:00
committed by GitHub
3 changed files with 141 additions and 0 deletions
+3
View File
@@ -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;
}
+40
View File
@@ -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
+98
View File
@@ -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", "<span>outside</span>\n"},
{"src/components/shared/Button.astro",
"---\nexport function buttonLabel(t: string): string { return t; }\n---\n<button />\n"},
{"src/components/Badge.astro", "<span>badge</span>\n"},
{"src/components/Card.astro",
"---\nimport Badge from './Badge.astro';\n"
"import Button from './shared/Button.astro';\n---\n<div><Badge /><Button /></div>\n"},
{"src/layouts/local.css", "body { margin: 0; }\n"},
{"src/styles/global.css", "body { color: black; }\n"},
{"src/layouts/Layout.astro",
"---\nimport '../styles/global.css';\nimport './local.css';\n---\n"
"<html><body><slot /></body></html>\n"},
{"src/lib/format.ts", "export function formatTitle(t: string): string { return t.trim(); }\n"},
{"src/lib/registry.ts",
"import Badge from '../components/Badge.astro';\n"
"import '../styles/global.css';\n"
"import { formatTitle } from './format';\n"
"export function registry(): unknown[] { return [Badge, formatTitle]; }\n"},
{"src/lib/sub/deep.ts",
"import { formatTitle } from '../format';\n"
"import { registry } from '../registry.ts';\n"
"export function deep(): string { registry(); return formatTitle('x'); }\n"},
{"src/pages/index.astro",
"---\nimport Layout from '../layouts/Layout.astro';\n"
"import Card from '../components/Card.astro';\n"
"import Outside from '../../../Badge.astro';\n"
"import { formatTitle } from '../lib/format';\n"
"const title = formatTitle(' Home ');\n---\n"
"<Layout><h1>{title}</h1><Card /></Layout>\n"},
};
static const EIFileEdgeExpect want[] = {
{"src/components/Card.astro", "src/components/Badge.astro"},
{"src/components/Card.astro", "src/components/shared/Button.astro"},
{"src/layouts/Layout.astro", "src/styles/global.css"},
{"src/layouts/Layout.astro", "src/layouts/local.css"},
{"src/lib/registry.ts", "src/components/Badge.astro"},
{"src/lib/registry.ts", "src/styles/global.css"},
{"src/lib/registry.ts", "src/lib/format.ts"},
{"src/lib/sub/deep.ts", "src/lib/format.ts"},
{"src/lib/sub/deep.ts", "src/lib/registry.ts"},
{"src/pages/index.astro", "src/layouts/Layout.astro"},
{"src/pages/index.astro", "src/components/Card.astro"},
{"src/pages/index.astro", "src/lib/format.ts"},
};
ASSERT_TRUE(ei_file_edges_match(f, (int)(sizeof(f) / sizeof(f[0])), want,
(int)(sizeof(want) / sizeof(want[0])), 12));
PASS();
}
/* ═══════════════════════════════════════════════════════════════════════════
* GREEN GUARD — Python
*
@@ -2299,6 +2395,8 @@ TEST(ei_php_psr4_missing_class_leaves_type_evidence_unknown) {
* ═══════════════════════════════════════════════════════════════════════════ */
SUITE(edge_imports) {
RUN_TEST(ei_relative_astro_css_imports_issue2558);
/* ── GREEN GUARDS — Python (must stay passing) ── */
RUN_TEST(ei_python_relative_from_import);
RUN_TEST(ei_python_absolute_import);