Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion graph-ui/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/controltab.tsx","./src/components/edgelines.tsx","./src/components/errorboundary.tsx","./src/components/filterpanel.tsx","./src/components/graphscene.test.ts","./src/components/graphscene.tsx","./src/components/graphtab.test.ts","./src/components/graphtab.tsx","./src/components/nodecloud.tsx","./src/components/nodedetailpanel.tsx","./src/components/nodelabels.tsx","./src/components/nodetooltip.tsx","./src/components/projectcard.tsx","./src/components/resizehandle.tsx","./src/components/sidebar.tsx","./src/components/statstab.test.tsx","./src/components/statstab.tsx","./src/components/tabbar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/usegraphdata.test.ts","./src/hooks/usegraphdata.ts","./src/hooks/useprojects.ts","./src/lib/colors.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/types.ts","./src/lib/utils.ts"],"version":"5.9.3"}
{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/ControlTab.tsx","./src/components/DisplaySettingsMenu.tsx","./src/components/EdgeLines.tsx","./src/components/ErrorBoundary.tsx","./src/components/FilterPanel.tsx","./src/components/GraphLoader.tsx","./src/components/GraphScene.test.ts","./src/components/GraphScene.tsx","./src/components/GraphTab.deadcode.test.tsx","./src/components/GraphTab.filters.test.tsx","./src/components/GraphTab.selection.test.tsx","./src/components/GraphTab.test.ts","./src/components/GraphTab.tsx","./src/components/MissedCallout.tsx","./src/components/NodeCloud.tsx","./src/components/NodeDetailPanel.test.tsx","./src/components/NodeDetailPanel.tsx","./src/components/NodeLabels.tsx","./src/components/NodeTooltip.tsx","./src/components/ProjectCard.tsx","./src/components/ResizeHandle.tsx","./src/components/Sidebar.tsx","./src/components/StatsTab.test.tsx","./src/components/StatsTab.tsx","./src/components/TabBar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/useGraphData.test.ts","./src/hooks/useGraphData.ts","./src/hooks/useProjects.test.tsx","./src/hooks/useProjects.ts","./src/lib/colors.ts","./src/lib/density.test.ts","./src/lib/density.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/types.ts","./src/lib/utils.ts"],"version":"5.9.3"}
23 changes: 21 additions & 2 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -6794,8 +6794,11 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.is_exported = cbm_is_exported(name, ctx->language);
if (ctx->language == CBM_LANG_RUST &&
strcmp(ts_node_type(child), "function_signature_item") == 0) {
if ((ctx->language == CBM_LANG_RUST &&
strcmp(ts_node_type(child), "function_signature_item") == 0) ||
((ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX) &&
(strcmp(ts_node_type(child), "method_signature") == 0 ||
strcmp(ts_node_type(child), "abstract_method_signature") == 0))) {
def.is_abstract = true;
}

Expand Down Expand Up @@ -6949,6 +6952,22 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
continue;
}

// TS interface members and abstract class members are signatures, not
// method_definition, so function_node_types skips them. Emit them as
// Methods so a call through an interface-typed field (NestJS token
// injection, #514) has a target. Class-body method_signature nodes are
// overloads of a real method_definition and stay skipped.
if ((ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX) &&
((strcmp(ts_node_type(child), "method_signature") == 0 &&
strcmp(ts_node_type(class_node), "interface_declaration") == 0) ||
strcmp(ts_node_type(child), "abstract_method_signature") == 0)) {
TSNode sname = ts_node_child_by_field_name(child, TS_FIELD("name"));
if (!ts_node_is_null(sname)) {
push_method_def(ctx, child, class_node, class_qn, spec, sname);
}
continue;
}

// ObjectScript UDL wraps each method/classmethod in a class_statement.
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL &&
strcmp(ts_node_type(child), "class_statement") == 0) {
Expand Down
10 changes: 10 additions & 0 deletions internal/cbm/lsp/ts_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4569,6 +4569,16 @@ static void register_file_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMFileRes
}
cbm_registry_add_type(reg, rt);
} else if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) {
/* Interface members are registered by ast_sweep_shapes from the
* AST, whose signature resolves names against this module first;
* the def text would type `status(): Response` as the global
* lib.dom Response and break chained calls. */
if (strcmp(d->label, "Method") == 0 && d->parent_class) {
const CBMRegisteredType *owner = cbm_registry_lookup_type(reg, d->parent_class);
if (owner && owner->is_interface) {
continue;
}
}
CBMRegisteredFunc rf;
memset(&rf, 0, sizeof(rf));
rf.qualified_name = d->qualified_name;
Expand Down
18 changes: 18 additions & 0 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *n
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
}

/* Whether the raw-name fallback must drop a blocked usage. An exact
* `import_map` hit is not a guess: the registry bound the name through the
* very import that blocked it, so the edge stays (an imported class passed to
* a decorator or used as a parameter type, #514). */
static inline bool cbm_pipeline_blocked_usage_vetoed(const CBMUsage *usage, const char *strategy,
const cbm_gbuf_node_t *target) {
if (!usage->semantic_reference_blocked) {
return false;
}
if (usage->semantic_reference_local_shadow) {
return true;
}
if (strategy && strcmp(strategy, "import_map") == 0) {
return false;
}
return cbm_pipeline_node_is_callable_target(target);
}

static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
return qt && tail && strcmp(qt, tail) == 0;
Expand Down
110 changes: 106 additions & 4 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,93 @@ static char *pxc_import_value_qn(CBMLanguage lang, const CBMFileResult *result,
return qualified;
}

/* JS/TS barrels. An import that lands on a Folder (`./svc` -> svc/index.ts)
* or on a module that only re-exports the name never reaches the declaring
* module, so `module.Name` is not in the registry and DI member calls on the
* imported type stay unresolved. Follow the barrel's own IMPORTS edges (its
* `export ... from` statements) to the one module that defines `name`. Those
* edges do not record which names each statement re-exports, so the
* definition node is the proof; two different definitions fail closed. */
enum { PXC_BARREL_MAX_DEPTH = 8 };

static bool pxc_is_js_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX;
}

/* The graph node whose QN is cbm_pipeline_fqn_compute(project, rel_path, name),
* or NULL. The one place this pass computes a transient QN for a lookup. */
static const cbm_gbuf_node_t *pxc_find_by_computed_qn(const cbm_gbuf_t *gbuf, const char *project,
const char *rel_path, const char *name) {
char *qn = cbm_pipeline_fqn_compute(project, rel_path, name);
const cbm_gbuf_node_t *n = qn ? cbm_gbuf_find_by_qn(gbuf, qn) : NULL;
free(qn);
return n;
}

/* Node of `name` when the file at rel_path defines it, else NULL. */
static const cbm_gbuf_node_t *pxc_defined_symbol(const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path, const char *name) {
const cbm_gbuf_node_t *node = pxc_find_by_computed_qn(gbuf, project_name, rel_path, name);
if (node && node->label && strcmp(node->label, "Module") != 0 &&
strcmp(node->label, "Folder") != 0 && strcmp(node->label, "File") != 0) {
return node;
}
return NULL;
}

/* `visited` holds barrel file QNs already searched for `name`: barrels form
* DAGs (and cycles) where one lib index is reachable through many paths, and
* re-walking it on each path is exponential in the depth cap. */
static const cbm_gbuf_node_t *pxc_barrel_symbol(const cbm_gbuf_t *gbuf, const char *project_name,
const cbm_gbuf_node_t *target, const char *name,
int depth, CBMHashTable *visited) {
if (!target || !target->label || !target->file_path || depth > PXC_BARREL_MAX_DEPTH) {
return NULL;
}
static const char *const index_names[] = {"index.ts", "index.tsx", "index.mts",
"index.js", "index.jsx", "index.mjs"};
char rel[CBM_SZ_1K];
const cbm_gbuf_node_t *file_node = NULL;
if (strcmp(target->label, "Module") == 0) {
snprintf(rel, sizeof(rel), "%s", target->file_path);
file_node = pxc_find_by_computed_qn(gbuf, project_name, rel, "__file__");
} else if (strcmp(target->label, "Folder") == 0) {
for (size_t i = 0; i < sizeof(index_names) / sizeof(index_names[0]) && !file_node; i++) {
snprintf(rel, sizeof(rel), "%s/%s", target->file_path, index_names[i]);
file_node = pxc_find_by_computed_qn(gbuf, project_name, rel, "__file__");
}
}
if (!file_node || cbm_ht_has(visited, file_node->qualified_name)) {
return NULL;
}
cbm_ht_set(visited, file_node->qualified_name, (void *)file_node);
const cbm_gbuf_node_t *defined = pxc_defined_symbol(gbuf, project_name, rel, name);
if (defined) {
return defined;
}

const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count) !=
0) {
return NULL;
}
const cbm_gbuf_node_t *found = NULL;
for (int i = 0; i < edge_count; i++) {
const cbm_gbuf_node_t *next = cbm_gbuf_find_by_id(gbuf, edges[i]->target_id);
const cbm_gbuf_node_t *def =
pxc_barrel_symbol(gbuf, project_name, next, name, depth + 1, visited);
if (!def) {
continue;
}
if (found && found != def) {
return NULL;
}
found = def;
}
return found;
}

static bool pxc_import_map_has_local(const char *const *keys, int count, const char *local_name) {
if (!keys || !local_name)
return false;
Expand Down Expand Up @@ -960,9 +1047,8 @@ int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, c

const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
char *file_qn = cbm_pipeline_fqn_compute(project_name, rel_path, "__file__");
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL;
free(file_qn);
const cbm_gbuf_node_t *file_node =
pxc_find_by_computed_qn(gbuf, project_name, rel_path, "__file__");
if (file_node && cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges,
&edge_count) != 0) {
edges = NULL;
Expand Down Expand Up @@ -1001,7 +1087,23 @@ int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, c
continue;
memcpy(local, start, n);
local[n] = '\0';
char *value = pxc_import_value_qn(lang, result, local, target);
const cbm_gbuf_node_t *value_node = target;
if (pxc_is_js_family(lang) && target->label && target->file_path) {
const cbm_gbuf_node_t *direct =
strcmp(target->label, "Module") == 0
? pxc_defined_symbol(gbuf, project_name, target->file_path, local)
: NULL;
CBMHashTable *visited = direct ? NULL : cbm_ht_create(16);
if (visited) {
const cbm_gbuf_node_t *barrel =
pxc_barrel_symbol(gbuf, project_name, target, local, 0, visited);
cbm_ht_free(visited);
if (barrel) {
value_node = barrel;
}
}
}
char *value = pxc_import_value_qn(lang, result, local, value_node);
if (!value) {
free(local);
continue;
Expand Down
3 changes: 1 addition & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3375,8 +3375,7 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
if (cbm_pipeline_blocked_usage_vetoed(usage, res.strategy, tgt)) {
continue;
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/pipeline/pass_pkgmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 +1438,10 @@ char *cbm_pipeline_resolve_module(const cbm_pipeline_ctx_t *ctx, const char *sou
if (amap) {
char *aliased = cbm_path_alias_resolve(amap, module_path);
if (aliased) {
char *qn = cbm_pipeline_fqn_module(ctx->project_name, aliased);
/* Like step 1: the alias resolver already removed an explicit
* file extension, so keep dotted basenames such as
* `cat.service` intact. */
char *qn = cbm_pipeline_fqn_folder(ctx->project_name, aliased);
free(aliased);
return qn;
}
Expand Down
3 changes: 1 addition & 2 deletions src/pipeline/pass_usages.c
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
if (cbm_pipeline_blocked_usage_vetoed(usage, res.strategy, tgt)) {
continue;
}
}
Expand Down
21 changes: 12 additions & 9 deletions src/pipeline/path_alias.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,23 @@ enum {

/* ── Helpers ───────────────────────────────────────────────────── */

/* Strip .ts/.tsx/.js/.jsx in place. Returns its argument. */
/* Strip an explicit JS/TS module extension in place, keeping dotted
* basenames such as "cat.service". Same set as the relative resolver in
* fqn.c. Returns its argument. */
static char *strip_resolved_ext(char *path) {
if (!path) {
return path;
}
static const char *const extensions[] = {
".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json",
};
size_t len = strlen(path);
if (len > 3 && path[len - 3] == '.' && (path[len - 2] == 't' || path[len - 2] == 'j') &&
path[len - 1] == 's') {
path[len - 3] = '\0';
return path;
}
if (len > 4 && path[len - 4] == '.' && (path[len - 3] == 't' || path[len - 3] == 'j') &&
path[len - 2] == 's' && path[len - 1] == 'x') {
path[len - 4] = '\0';
for (size_t i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) {
size_t ext_len = strlen(extensions[i]);
if (len > ext_len && strcmp(path + len - ext_len, extensions[i]) == 0) {
path[len - ext_len] = '\0';
break;
}
}
return path;
}
Expand Down
Loading
Loading