Skip to content

Commit d98f3a0

Browse files
author
Your Name
committed
feat(pipeline): resolve relative import paths for IMPORTS edge creation
Root cause: cbm_pipeline_fqn_module() received raw import paths like './utils/trace' or '../controllers/auth' and converted them directly to QNs without resolving against the importing file's directory. The resulting QN never matched any Module node, so IMPORTS edges were silently dropped. New function cbm_pipeline_resolve_import_path() in fqn.c: - Resolves ./ and ../ segments against the importer's directory - Normalizes path (collapses a/b/../c → a/c) - Bare module specifiers (no ./ prefix) pass through unchanged Extension probing in pass_parallel.c and pass_definitions.c: - After resolving the path, tries exact match first - Then probes: .js, .ts, .tsx, .jsx, .mjs, .mts, .css, .scss, .json - Then probes /index variants: /index.js, /index.ts, /index.tsx, etc. - Then probes C/C++ headers: .h, .hpp, .hh Results: - JS service: 0 → 335 IMPORTS edges - TS monolith: 153 → 11,770 IMPORTS edges (77x increase) - TS/React monorepo: 0 → 344 IMPORTS edges - TS/Electron app: 1 → 161 IMPORTS edges
1 parent 949d663 commit d98f3a0

4 files changed

Lines changed: 207 additions & 25 deletions

File tree

src/pipeline/fqn.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,91 @@ char *cbm_pipeline_fqn_folder(const char *project, const char *rel_dir) {
158158
return result;
159159
}
160160

161+
/**
162+
* Resolve an import module_path relative to the importing file's directory.
163+
*
164+
* For relative paths (starting with ./ or ../), resolves against the importer's
165+
* directory. For bare module specifiers (no ./ prefix), returns a copy unchanged.
166+
*
167+
* Examples (importer_rel_path="src/routes/api.js"):
168+
* "./controllers/auth" → "src/routes/controllers/auth"
169+
* "../utils/helpers" → "src/utils/helpers"
170+
* "lodash" → "lodash" (bare module, unchanged)
171+
* "@hapi/hapi" → "@hapi/hapi" (scoped package, unchanged)
172+
*
173+
* Returns: heap-allocated resolved path. Caller must free().
174+
*/
175+
char *cbm_pipeline_resolve_import_path(const char *importer_rel_path, const char *module_path) {
176+
if (!module_path || !module_path[0]) {
177+
return strdup("");
178+
}
179+
180+
/* Bare module specifier — no relative path resolution needed */
181+
if (module_path[0] != '.') {
182+
return strdup(module_path);
183+
}
184+
185+
/* Get the importing file's directory */
186+
char *importer_dir = strdup(importer_rel_path ? importer_rel_path : "");
187+
cbm_normalize_path_sep(importer_dir);
188+
char *last_slash = strrchr(importer_dir, '/');
189+
if (last_slash) {
190+
*(last_slash + 1) = '\0'; /* keep trailing slash */
191+
} else {
192+
importer_dir[0] = '\0'; /* file is at root */
193+
}
194+
195+
/* Concatenate: importer_dir + module_path */
196+
size_t dir_len = strlen(importer_dir);
197+
size_t mod_len = strlen(module_path);
198+
char *combined = malloc(dir_len + mod_len + 2);
199+
snprintf(combined, dir_len + mod_len + 2, "%s%s", importer_dir, module_path);
200+
free(importer_dir);
201+
202+
/* Normalize: resolve . and .. segments */
203+
cbm_normalize_path_sep(combined);
204+
const char *segments[256];
205+
int seg_count = 0;
206+
207+
char *tok = combined;
208+
while (tok && *tok) {
209+
char *slash = strchr(tok, '/');
210+
if (slash) *slash = '\0';
211+
212+
if (strcmp(tok, ".") == 0) {
213+
/* skip */
214+
} else if (strcmp(tok, "..") == 0) {
215+
if (seg_count > 0) seg_count--; /* pop parent */
216+
} else if (tok[0] != '\0') {
217+
if (seg_count < 255) {
218+
segments[seg_count++] = tok;
219+
}
220+
}
221+
222+
tok = slash ? slash + 1 : NULL;
223+
}
224+
225+
/* Rebuild path */
226+
if (seg_count == 0) {
227+
free(combined);
228+
return strdup("");
229+
}
230+
231+
size_t total = 0;
232+
for (int i = 0; i < seg_count; i++) {
233+
total += strlen(segments[i]) + 1;
234+
}
235+
char *result = malloc(total + 1);
236+
result[0] = '\0';
237+
for (int i = 0; i < seg_count; i++) {
238+
if (i > 0) strcat(result, "/");
239+
strcat(result, segments[i]);
240+
}
241+
242+
free(combined);
243+
return result;
244+
}
245+
161246
char *cbm_project_name_from_path(const char *abs_path) {
162247
if (!abs_path || !abs_path[0]) {
163248
return strdup("root");

src/pipeline/pass_definitions.c

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -288,28 +288,76 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
288288
total_imports += result->imports.count;
289289

290290
/* Store per-file import map for later use by pass_calls.
291-
* For each import, create an IMPORTS edge: File → imported module. */
292-
for (int j = 0; j < result->imports.count; j++) {
293-
CBMImport *imp = &result->imports.items[j];
294-
if (!imp->module_path) {
295-
continue;
296-
}
297-
298-
/* Find or create the target module node */
299-
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path);
300-
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
301-
291+
* For each import, create an IMPORTS edge: File → imported module.
292+
* Resolve relative paths (./ ../) and probe common extensions. */
293+
{
302294
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
303295
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
296+
free(file_qn);
297+
298+
for (int j = 0; j < result->imports.count && source_node; j++) {
299+
CBMImport *imp = &result->imports.items[j];
300+
if (!imp->module_path) {
301+
continue;
302+
}
303+
304+
/* Resolve relative paths against importing file's directory */
305+
char *resolved = cbm_pipeline_resolve_import_path(rel, imp->module_path);
306+
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, resolved);
307+
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
308+
309+
/* Probe common extensions */
310+
if (!target) {
311+
static const char *exts[] = {
312+
".js", ".ts", ".tsx", ".jsx", ".mjs", ".mts",
313+
".css", ".scss", ".json", NULL
314+
};
315+
for (int e = 0; !target && exts[e]; e++) {
316+
char buf[2048];
317+
snprintf(buf, sizeof(buf), "%s%s", resolved, exts[e]);
318+
free(target_qn);
319+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
320+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
321+
}
322+
}
304323

305-
if (source_node && target) {
306-
char imp_props[256];
307-
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}",
308-
imp->local_name ? imp->local_name : "");
309-
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
324+
/* Probe /index variants */
325+
if (!target) {
326+
static const char *idx[] = {
327+
"/index.js", "/index.ts", "/index.tsx", "/index.jsx",
328+
"/index.mjs", "/index", NULL
329+
};
330+
for (int e = 0; !target && idx[e]; e++) {
331+
char buf[2048];
332+
snprintf(buf, sizeof(buf), "%s%s", resolved, idx[e]);
333+
free(target_qn);
334+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
335+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
336+
}
337+
}
338+
339+
/* C/C++ include: try .h, .hpp */
340+
if (!target) {
341+
static const char *hdr[] = {".h", ".hpp", ".hh", NULL};
342+
for (int e = 0; !target && hdr[e]; e++) {
343+
char buf[2048];
344+
snprintf(buf, sizeof(buf), "%s%s", resolved, hdr[e]);
345+
free(target_qn);
346+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
347+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
348+
}
349+
}
350+
351+
if (target) {
352+
char imp_props[256];
353+
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}",
354+
imp->local_name ? imp->local_name : "");
355+
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS",
356+
imp_props);
357+
}
358+
free(target_qn);
359+
free(resolved);
310360
}
311-
free(target_qn);
312-
free(file_qn);
313361
}
314362

315363
/* Cache or free the extraction result */

src/pipeline/pass_parallel.c

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -953,28 +953,73 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
953953
}
954954
}
955955

956-
/* IMPORTS edges */
957-
for (int j = 0; j < result->imports.count; j++) {
956+
/* IMPORTS edges — resolve relative paths and probe extensions */
957+
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
958+
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
959+
free(file_qn);
960+
961+
for (int j = 0; j < result->imports.count && source_node; j++) {
958962
CBMImport *imp = &result->imports.items[j];
959963
if (!imp->module_path) {
960964
continue;
961965
}
962966

963-
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path);
967+
/* Resolve relative paths (./ ../) against importing file's directory */
968+
char *resolved = cbm_pipeline_resolve_import_path(rel, imp->module_path);
969+
char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, resolved);
964970
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
965971

966-
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
967-
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
972+
/* Probe common extensions if no exact match: .js, .ts, .tsx, .jsx, .mjs */
973+
if (!target) {
974+
static const char *exts[] = {
975+
".js", ".ts", ".tsx", ".jsx", ".mjs", ".mts",
976+
".css", ".scss", ".json", NULL
977+
};
978+
for (int e = 0; !target && exts[e]; e++) {
979+
char buf[2048];
980+
snprintf(buf, sizeof(buf), "%s%s", resolved, exts[e]);
981+
free(target_qn);
982+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
983+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
984+
}
985+
}
968986

969-
if (source_node && target) {
987+
/* Probe /index variants (directory imports) */
988+
if (!target) {
989+
static const char *idx[] = {
990+
"/index.js", "/index.ts", "/index.tsx", "/index.jsx",
991+
"/index.mjs", "/index", NULL
992+
};
993+
for (int e = 0; !target && idx[e]; e++) {
994+
char buf[2048];
995+
snprintf(buf, sizeof(buf), "%s%s", resolved, idx[e]);
996+
free(target_qn);
997+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
998+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
999+
}
1000+
}
1001+
1002+
/* C/C++ include: try .h, .hpp variants */
1003+
if (!target && (resolved[0] != '.' || resolved[1] == '.')) {
1004+
static const char *hdr[] = {".h", ".hpp", ".hh", NULL};
1005+
for (int e = 0; !target && hdr[e]; e++) {
1006+
char buf[2048];
1007+
snprintf(buf, sizeof(buf), "%s%s", resolved, hdr[e]);
1008+
free(target_qn);
1009+
target_qn = cbm_pipeline_fqn_module(ctx->project_name, buf);
1010+
target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
1011+
}
1012+
}
1013+
1014+
if (target) {
9701015
char imp_props[256];
9711016
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}",
9721017
imp->local_name ? imp->local_name : "");
9731018
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
9741019
imports_edges++;
9751020
}
9761021
free(target_qn);
977-
free(file_qn);
1022+
free(resolved);
9781023
}
9791024
}
9801025

src/pipeline/pipeline.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ char *cbm_pipeline_fqn_module(const char *project, const char *rel_path);
8282
/* Folder QN: project.dir.parts. Caller must free(). */
8383
char *cbm_pipeline_fqn_folder(const char *project, const char *rel_dir);
8484

85+
/* Resolve an import module_path relative to the importing file's directory.
86+
* Handles ./ and ../ resolution. Bare modules returned unchanged. Caller must free(). */
87+
char *cbm_pipeline_resolve_import_path(const char *importer_rel_path, const char *module_path);
88+
8589
/* Derive project name from an absolute path.
8690
* Replaces / and : with -, collapses --, trims leading -.
8791
* Caller must free() the returned string. */

0 commit comments

Comments
 (0)