mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-03 08:03:38 +08:00
Merge pull request #1811 from pcristin/fix/issue-1682-dotted-ts-imports
fix(pipeline): preserve dotted TypeScript import basenames
This commit is contained in:
+12
-13
@@ -282,20 +282,19 @@ static char *resolve_python_relative(char *buf, size_t buf_size, const char *mod
|
||||
return strdup(buf);
|
||||
}
|
||||
|
||||
/* Strip a trailing file extension from a segment (e.g. "helpers.ts" → "helpers").
|
||||
* Returns the new segment length. */
|
||||
static size_t strip_ext(const char *seg_start, size_t seg_len) {
|
||||
const char *seg_end = seg_start + seg_len;
|
||||
const char *dot = NULL;
|
||||
for (const char *d = seg_end - FQN_SEP_LEN; d >= seg_start; d--) {
|
||||
if (*d == '.') {
|
||||
dot = d;
|
||||
break;
|
||||
/* Strip an explicit JS/TS module file extension while preserving dots that are
|
||||
* part of an extensionless basename (e.g. "featureX.engine"). */
|
||||
static size_t strip_js_module_ext(const char *seg_start, size_t seg_len) {
|
||||
static const char *const extensions[] = {
|
||||
".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) {
|
||||
size_t ext_len = strlen(extensions[i]);
|
||||
if (seg_len > ext_len &&
|
||||
memcmp(seg_start + seg_len - ext_len, extensions[i], ext_len) == 0) {
|
||||
return seg_len - ext_len;
|
||||
}
|
||||
}
|
||||
if (dot && dot > seg_start) {
|
||||
return (size_t)(dot - seg_start);
|
||||
}
|
||||
return seg_len;
|
||||
}
|
||||
|
||||
@@ -322,7 +321,7 @@ static char *resolve_js_relative(char *buf, size_t buf_size, const char *module_
|
||||
continue;
|
||||
}
|
||||
if (*p == '\0') {
|
||||
seg_len = strip_ext(seg_start, seg_len);
|
||||
seg_len = strip_js_module_ext(seg_start, seg_len);
|
||||
}
|
||||
if (seg_len > 0 && !path_append_segment(buf, buf_size, seg_start, seg_len)) {
|
||||
return NULL;
|
||||
|
||||
@@ -1397,7 +1397,11 @@ char *cbm_pipeline_resolve_module(const cbm_pipeline_ctx_t *ctx, const char *sou
|
||||
/* 1. Try relative import resolution (existing logic) */
|
||||
char *resolved = cbm_pipeline_resolve_relative_import(source_rel, module_path);
|
||||
if (resolved) {
|
||||
char *qn = cbm_pipeline_fqn_module(ctx->project_name, resolved);
|
||||
/* The relative resolver has already removed an explicit JS/TS file
|
||||
* extension. Treat the remaining path as a module path verbatim so a
|
||||
* dotted extensionless basename such as `featureX.engine` is not
|
||||
* stripped a second time by cbm_pipeline_fqn_module. */
|
||||
char *qn = cbm_pipeline_fqn_folder(ctx->project_name, resolved);
|
||||
free(resolved);
|
||||
return qn;
|
||||
}
|
||||
|
||||
@@ -365,6 +365,96 @@ TEST(ei_typescript_named_relative_import) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1682: extensionless dotted basenames are part of the module name. The
|
||||
* resolver used to strip `.engine`, miss the module, and bind both imports to
|
||||
* the same-named fixture Function in the sibling spec file. */
|
||||
TEST(ei_typescript_dotted_relative_import_targets_source_module_issue1682) {
|
||||
static const char *engine_path = "packages/api/src/modules/featureX/featureX.engine.ts";
|
||||
static const char *consumer_path = "packages/api/src/modules/consumer/consumer.service.ts";
|
||||
static const EILangFile f[] = {
|
||||
{"packages/api/src/modules/featureX/featureX.engine.ts",
|
||||
"export interface SomeType { id: string; qty: number; }\n"
|
||||
"export interface Evaluation { rateByItem: Record<string, number>; }\n"
|
||||
"export function helperB(configs: SomeType[], lines: SomeType[]): Evaluation {\n"
|
||||
" return { rateByItem: { [lines[0].id]: lines[0].qty + configs.length } };\n"
|
||||
"}\n"},
|
||||
{"packages/api/src/modules/featureX/featureX.service.ts",
|
||||
"import { SomeType, Evaluation, helperB } from './featureX.engine';\n"
|
||||
"export class FeatureXService {\n"
|
||||
" evaluate(configs: SomeType[], lines: SomeType[]): Evaluation {\n"
|
||||
" return helperB(configs, lines);\n"
|
||||
" }\n"
|
||||
"}\n"},
|
||||
{"packages/api/src/modules/featureX/featureX.service.spec.ts",
|
||||
"import { SomeType, helperB } from './featureX.engine';\n"
|
||||
"function featureX(overrides: Partial<SomeType>): SomeType {\n"
|
||||
" return { id: 'x', qty: 1, ...overrides };\n"
|
||||
"}\n"
|
||||
"export function exerciseFixture(): number {\n"
|
||||
" return helperB([featureX({})], [featureX({ qty: 2 })]).rateByItem.x;\n"
|
||||
"}\n"},
|
||||
{"packages/api/src/modules/consumer/consumer.service.ts",
|
||||
"import { helperB, type SomeType } from '../featureX/featureX.engine';\n"
|
||||
"export class ConsumerService {\n"
|
||||
" callerMethod(items: SomeType[]): number {\n"
|
||||
" return helperB(items, [{ id: 'p1', qty: 1 }]).rateByItem.p1;\n"
|
||||
" }\n"
|
||||
"}\n"},
|
||||
{"packages/mobile/src/api.ts",
|
||||
"export function helperB(token: string): Promise<unknown> {\n"
|
||||
" return fetch('/api/x', { method: 'POST', body: token });\n"
|
||||
"}\n"},
|
||||
};
|
||||
|
||||
EILangProj lp;
|
||||
cbm_store_t *store = ei_index_files(&lp, f, (int)(sizeof(f) / sizeof(f[0])));
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
int64_t consumer_id = ei_node_id_for_file_label(store, lp.project, consumer_path, "File");
|
||||
ASSERT_GT(consumer_id, 0);
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
ASSERT_EQ(
|
||||
cbm_store_find_edges_by_source_type(store, consumer_id, "IMPORTS", &edges, &edge_count),
|
||||
CBM_STORE_OK);
|
||||
|
||||
bool saw_helper = false;
|
||||
bool saw_type = false;
|
||||
bool helper_target_ok = false;
|
||||
bool type_target_ok = false;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
const char *props = edges[i].properties_json ? edges[i].properties_json : "";
|
||||
bool is_helper = strstr(props, "\"local_name\":\"helperB\"") != NULL;
|
||||
bool is_type = strstr(props, "\"local_name\":\"SomeType\"") != NULL;
|
||||
if (!is_helper && !is_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t));
|
||||
ASSERT_NOT_NULL(target);
|
||||
ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK);
|
||||
bool target_ok = target->file_path && strcmp(target->file_path, engine_path) == 0;
|
||||
if (is_helper) {
|
||||
saw_helper = true;
|
||||
helper_target_ok = target_ok;
|
||||
}
|
||||
if (is_type) {
|
||||
saw_type = true;
|
||||
type_target_ok = target_ok;
|
||||
}
|
||||
cbm_store_free_nodes(target, 1);
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
ei_cleanup(&lp, store);
|
||||
|
||||
ASSERT_TRUE(saw_helper);
|
||||
ASSERT_TRUE(saw_type);
|
||||
ASSERT_TRUE(helper_target_ok);
|
||||
ASSERT_TRUE(type_target_ok);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* TypeScript: default import `import helper from './util'`. */
|
||||
TEST(ei_typescript_default_import) {
|
||||
static const EILangFile f[] = {
|
||||
@@ -1063,6 +1153,7 @@ SUITE(edge_imports) {
|
||||
|
||||
/* ── GREEN GUARDS — TypeScript (must stay passing) ── */
|
||||
RUN_TEST(ei_typescript_named_relative_import);
|
||||
RUN_TEST(ei_typescript_dotted_relative_import_targets_source_module_issue1682);
|
||||
RUN_TEST(ei_typescript_default_import);
|
||||
RUN_TEST(ei_typescript_namespace_import);
|
||||
RUN_TEST(ei_typescript_aliased_import);
|
||||
|
||||
@@ -84,6 +84,21 @@ TEST(fqn_module_qn_still_strips_extension) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_relative_js_import_preserves_dotted_basename_issue1682) {
|
||||
ASSERT_FQN(
|
||||
cbm_pipeline_resolve_relative_import(
|
||||
"packages/api/src/modules/consumer/consumer.service.ts", "../featureX/featureX.engine"),
|
||||
"packages/api/src/modules/featureX/featureX.engine");
|
||||
ASSERT_FQN(
|
||||
cbm_pipeline_resolve_relative_import("packages/api/src/modules/moduleA/moduleA.service.ts",
|
||||
"../moduleQ/moduleQ.service"),
|
||||
"packages/api/src/modules/moduleQ/moduleQ.service");
|
||||
ASSERT_FQN(cbm_pipeline_resolve_relative_import(
|
||||
"packages/api/src/modules/moduleA/moduleA.service.ts", "./create-thing.dto"),
|
||||
"packages/api/src/modules/moduleA/create-thing.dto");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_rs) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib.rs", "new"), "proj.lib.new");
|
||||
PASS();
|
||||
@@ -665,6 +680,7 @@ SUITE(fqn) {
|
||||
RUN_TEST(fqn_file_qn_preserves_dotfile_variants_issue1077);
|
||||
RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964);
|
||||
RUN_TEST(fqn_module_qn_still_strips_extension);
|
||||
RUN_TEST(fqn_relative_js_import_preserves_dotted_basename_issue1682);
|
||||
RUN_TEST(fqn_compute_basic_rs);
|
||||
RUN_TEST(fqn_compute_file_sibling_distinct);
|
||||
RUN_TEST(fqn_compute_symbol_still_strips);
|
||||
|
||||
Reference in New Issue
Block a user