Skip to content

Commit 424ab76

Browse files
authored
Merge pull request #2595 from lorenzozanee/fix/relative-non-code-imports
fix(pipeline): resolve parent-relative Astro and CSS imports
2 parents 8fafd8a + ad178b2 commit 424ab76

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

‎src/pipeline/fqn.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ static char *resolve_js_relative(char *buf, size_t buf_size, const char *module_
329329
continue;
330330
}
331331
if (seg_len == FQN_DOTDOT_LEN && seg_start[0] == '.' && seg_start[FQN_SEP_LEN] == '.') {
332+
if (!buf[0]) {
333+
return NULL;
334+
}
332335
path_pop_segment(buf);
333336
continue;
334337
}

‎src/pipeline/pass_pkgmap.c‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,37 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
21152115
return found;
21162116
}
21172117

2118+
/* The File an explicit relative import (`./x`, `../x`) names exactly, for
2119+
* files whose File QN differs from the module QN Strategy 1 tries
2120+
* (`../styles/global.css`, `../layouts/Layout.astro`, #2558). Resolved in
2121+
* stack buffers by cbm_repo_rel_join, which refuses a path above the
2122+
* repository root. */
2123+
static const cbm_gbuf_node_t *resolve_relative_file(const cbm_pipeline_ctx_t *ctx,
2124+
const char *source_rel, const char *module_path,
2125+
bool php_only) {
2126+
if (!source_rel || !module_path || module_path[0] != '.' ||
2127+
!(module_path[1] == '/' || (module_path[1] == '.' && module_path[2] == '/'))) {
2128+
return NULL;
2129+
}
2130+
char dir[PKGMAP_PATH_BUF];
2131+
int dir_len = snprintf(dir, sizeof(dir), "%s", source_rel);
2132+
if (dir_len < 0 || (size_t)dir_len >= sizeof(dir)) {
2133+
return NULL;
2134+
}
2135+
char *slash = strrchr(dir, '/');
2136+
if (slash) {
2137+
*slash = '\0';
2138+
} else {
2139+
dir[0] = '\0';
2140+
}
2141+
char path[PKGMAP_PATH_BUF];
2142+
if (!cbm_repo_rel_join(dir, module_path, path, sizeof(path))) {
2143+
return NULL;
2144+
}
2145+
const cbm_gbuf_node_t *file = cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, path);
2146+
return file && (!php_only || node_in_php_file(file)) ? file : NULL;
2147+
}
2148+
21182149
/* ── PHP PSR-4 class imports (#1186) ──────────────────────────────── */
21192150

21202151
typedef enum {
@@ -2389,6 +2420,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
23892420
}
23902421
}
23912422

2423+
/* Strategy 1c: relative non-module files can have a File QN that does not
2424+
* match the module QN above. Resolve the exact file after module and
2425+
* sibling lookup. */
2426+
const cbm_gbuf_node_t *rel_file =
2427+
resolve_relative_file(ctx, source_rel, imp->module_path, php_only);
2428+
if (rel_file) {
2429+
return rel_file;
2430+
}
2431+
23922432
/* Strategy 2: namespace map. `using App.Utils`, `import com.example.Foo`,
23932433
* `use App\Utils\Helper` name a NAMESPACE (or a member of it) that the
23942434
* path-based QN cannot express. Try the full module path and progressively

‎tests/test_edge_imports.c‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,102 @@ static int ei_edge_present(const EILangFile *files, int nfiles, const char *edge
267267
return got >= floor;
268268
}
269269

270+
typedef struct {
271+
const char *importer;
272+
const char *target;
273+
} EIFileEdgeExpect;
274+
275+
static int ei_file_edges_match(const EILangFile *files, int nfiles, const EIFileEdgeExpect *want,
276+
int nwant, int expected_count) {
277+
EILangProj lp;
278+
cbm_store_t *store = ei_index_files(&lp, files, nfiles);
279+
int ok = store && cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") == expected_count;
280+
if (store && !ok) {
281+
int got = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
282+
fprintf(stderr, " [IMPORTS] FAIL count=%d expected==%d\n", got, expected_count);
283+
}
284+
for (int i = 0; store && i < nwant; i++) {
285+
int64_t source_id = ei_node_id_for_file_label(store, lp.project, want[i].importer, "File");
286+
cbm_edge_t *edges = NULL;
287+
int edge_count = 0;
288+
int found = 0;
289+
if (source_id > 0 &&
290+
cbm_store_find_edges_by_source_type(store, source_id, "IMPORTS", &edges, &edge_count) ==
291+
CBM_STORE_OK) {
292+
for (int j = 0; j < edge_count; j++) {
293+
cbm_node_t target;
294+
memset(&target, 0, sizeof(target));
295+
if (cbm_store_find_node_by_id(store, edges[j].target_id, &target) == CBM_STORE_OK) {
296+
found = target.file_path && strcmp(target.file_path, want[i].target) == 0;
297+
cbm_node_free_fields(&target);
298+
}
299+
if (found) {
300+
break;
301+
}
302+
}
303+
}
304+
cbm_store_free_edges(edges, edge_count);
305+
if (!found) {
306+
fprintf(stderr, " [IMPORTS] FAIL %s -> %s\n", want[i].importer, want[i].target);
307+
ok = 0;
308+
}
309+
}
310+
ei_cleanup(&lp, store);
311+
return ok;
312+
}
313+
314+
TEST(ei_relative_astro_css_imports_issue2558) {
315+
static const EILangFile f[] = {
316+
{"Badge.astro", "<span>outside</span>\n"},
317+
{"src/components/shared/Button.astro",
318+
"---\nexport function buttonLabel(t: string): string { return t; }\n---\n<button />\n"},
319+
{"src/components/Badge.astro", "<span>badge</span>\n"},
320+
{"src/components/Card.astro",
321+
"---\nimport Badge from './Badge.astro';\n"
322+
"import Button from './shared/Button.astro';\n---\n<div><Badge /><Button /></div>\n"},
323+
{"src/layouts/local.css", "body { margin: 0; }\n"},
324+
{"src/styles/global.css", "body { color: black; }\n"},
325+
{"src/layouts/Layout.astro",
326+
"---\nimport '../styles/global.css';\nimport './local.css';\n---\n"
327+
"<html><body><slot /></body></html>\n"},
328+
{"src/lib/format.ts", "export function formatTitle(t: string): string { return t.trim(); }\n"},
329+
{"src/lib/registry.ts",
330+
"import Badge from '../components/Badge.astro';\n"
331+
"import '../styles/global.css';\n"
332+
"import { formatTitle } from './format';\n"
333+
"export function registry(): unknown[] { return [Badge, formatTitle]; }\n"},
334+
{"src/lib/sub/deep.ts",
335+
"import { formatTitle } from '../format';\n"
336+
"import { registry } from '../registry.ts';\n"
337+
"export function deep(): string { registry(); return formatTitle('x'); }\n"},
338+
{"src/pages/index.astro",
339+
"---\nimport Layout from '../layouts/Layout.astro';\n"
340+
"import Card from '../components/Card.astro';\n"
341+
"import Outside from '../../../Badge.astro';\n"
342+
"import { formatTitle } from '../lib/format';\n"
343+
"const title = formatTitle(' Home ');\n---\n"
344+
"<Layout><h1>{title}</h1><Card /></Layout>\n"},
345+
};
346+
static const EIFileEdgeExpect want[] = {
347+
{"src/components/Card.astro", "src/components/Badge.astro"},
348+
{"src/components/Card.astro", "src/components/shared/Button.astro"},
349+
{"src/layouts/Layout.astro", "src/styles/global.css"},
350+
{"src/layouts/Layout.astro", "src/layouts/local.css"},
351+
{"src/lib/registry.ts", "src/components/Badge.astro"},
352+
{"src/lib/registry.ts", "src/styles/global.css"},
353+
{"src/lib/registry.ts", "src/lib/format.ts"},
354+
{"src/lib/sub/deep.ts", "src/lib/format.ts"},
355+
{"src/lib/sub/deep.ts", "src/lib/registry.ts"},
356+
{"src/pages/index.astro", "src/layouts/Layout.astro"},
357+
{"src/pages/index.astro", "src/components/Card.astro"},
358+
{"src/pages/index.astro", "src/lib/format.ts"},
359+
};
360+
361+
ASSERT_TRUE(ei_file_edges_match(f, (int)(sizeof(f) / sizeof(f[0])), want,
362+
(int)(sizeof(want) / sizeof(want[0])), 12));
363+
PASS();
364+
}
365+
270366
/* ═══════════════════════════════════════════════════════════════════════════
271367
* GREEN GUARD — Python
272368
*
@@ -2299,6 +2395,8 @@ TEST(ei_php_psr4_missing_class_leaves_type_evidence_unknown) {
22992395
* ═══════════════════════════════════════════════════════════════════════════ */
23002396

23012397
SUITE(edge_imports) {
2398+
RUN_TEST(ei_relative_astro_css_imports_issue2558);
2399+
23022400
/* ── GREEN GUARDS — Python (must stay passing) ── */
23032401
RUN_TEST(ei_python_relative_from_import);
23042402
RUN_TEST(ei_python_absolute_import);

0 commit comments

Comments
 (0)