mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-11 09:25:35 +08:00
fix(pkgmap+routes): resolve workspace imports + extract SvelteKit routes
Two structural fixes for TS monorepos (pnpm/yarn workspaces) and SvelteKit projects, measured to take a 885-file SvelteKit+Turborepo repo from 319 edges to 31802 (100x) with no fixture regressions. - pkgmap: package.json / composer.json live in IGNORED_JSON_FILES, so the manifest-driven pkgmap builders never saw them and every '@my/pkg' workspace import was silently dropped. Add a symlink-safe, skip-dir-aware filesystem walker (cbm_pkgmap_scan_repo / cbm_pkgmap_build_from_repo) that harvests manifests directly from the repo regardless of the discovery filter. Sequential and parallel paths both feed it now. - definitions: extract every file's defs (creating Module nodes) BEFORE resolving imports, so a workspace import in the first file can resolve against the complete in-memory graph (two-phase, cache-backed). - routes: synthesise Route nodes + HANDLES edges from SvelteKit's filesystem layout (+server / +page.server / +layout.server), which has no app.get(...) call site for pass_calls to pick up. Distilled from #369 onto current main. The PR's CBM_DISABLE_LSP_CROSS escape hatch was rebased onto the v0.7.0 fused cross-LSP architecture: it now gates run_cross_lsp (NULL all_defs makes the fused resolver no-op cross-file resolution) instead of wrapping the removed standalone pass_lsp_cross call. Relates to #271 / #56.
This commit is contained in:
committed by
Martin Vogel
parent
2389d82214
commit
9bcfaabe4c
@@ -343,6 +343,21 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
int total_imports = 0;
|
||||
int errors = 0;
|
||||
|
||||
/* Sequential pass must extract all defs (which create Module/Function/...
|
||||
* nodes) BEFORE resolving imports — otherwise a workspace import in the
|
||||
* first file processed can't find the target Module node, because the
|
||||
* target file's defs haven't been extracted yet. Result cache is
|
||||
* required for this two-phase ordering. */
|
||||
CBMFileResult **local_cache = ctx->result_cache;
|
||||
bool owns_local_cache = false;
|
||||
if (!local_cache) {
|
||||
local_cache = (CBMFileResult **)calloc((size_t)file_count, sizeof(CBMFileResult *));
|
||||
owns_local_cache = (local_cache != NULL);
|
||||
}
|
||||
|
||||
/* Phase 1: extract every file and create def-derived nodes (Modules,
|
||||
* Functions, ...) so any file's IMPORTS can resolve against the
|
||||
* complete in-memory graph in Phase 2. */
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (cbm_pipeline_check_cancel(ctx)) {
|
||||
return CBM_NOT_FOUND;
|
||||
@@ -381,17 +396,45 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
/* Store calls for pass_calls (we save them in the extraction results
|
||||
* for now — a future optimization would batch these) */
|
||||
total_calls += result->calls.count;
|
||||
total_imports += create_import_edges_for_file(ctx, result, rel);
|
||||
create_channel_edges_for_file(ctx, result, rel);
|
||||
|
||||
/* Cache or free the extraction result */
|
||||
if (ctx->result_cache) {
|
||||
ctx->result_cache[i] = result;
|
||||
if (local_cache) {
|
||||
local_cache[i] = result;
|
||||
} else {
|
||||
/* Cache unavailable: imports for this file can still only
|
||||
* resolve to defs already in the graph, but the file's
|
||||
* own defs are now persisted before the lookup. */
|
||||
total_imports += create_import_edges_for_file(ctx, result, rel);
|
||||
create_channel_edges_for_file(ctx, result, rel);
|
||||
cbm_free_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 2: now that all extraction results are cached and Module
|
||||
* nodes for every file are in the graph, walk the cache again to
|
||||
* create IMPORTS / channel edges. Imports resolve against the full
|
||||
* project graph. */
|
||||
if (local_cache) {
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (cbm_pipeline_check_cancel(ctx)) {
|
||||
break;
|
||||
}
|
||||
CBMFileResult *result = local_cache[i];
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
total_imports += create_import_edges_for_file(ctx, result, files[i].rel_path);
|
||||
create_channel_edges_for_file(ctx, result, files[i].rel_path);
|
||||
}
|
||||
if (owns_local_cache) {
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (local_cache[i]) {
|
||||
cbm_free_result(local_cache[i]);
|
||||
}
|
||||
}
|
||||
free(local_cache);
|
||||
}
|
||||
}
|
||||
|
||||
cbm_log_info("pass.done", "pass", "definitions", "defs", itoa_log(total_defs), "calls",
|
||||
itoa_log(total_calls), "imports", itoa_log(total_imports), "errors",
|
||||
itoa_log(errors));
|
||||
|
||||
@@ -594,6 +594,11 @@ static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_en
|
||||
if (!pkg_entries) {
|
||||
return;
|
||||
}
|
||||
/* Supplement with a repo-wide filesystem walk so manifests filtered
|
||||
* by the main discoverer (package.json, composer.json — in
|
||||
* IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's
|
||||
* array so the existing merge below sees them. */
|
||||
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0]);
|
||||
cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name));
|
||||
for (int i = 0; i < worker_count; i++) {
|
||||
cbm_pkg_entries_free(&pkg_entries[i]);
|
||||
|
||||
+136
-9
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "discover/discover.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/hash_table.h"
|
||||
@@ -22,10 +23,12 @@
|
||||
|
||||
#include <yyjson/yyjson.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* Read an entire file into a malloc'd buffer. Returns NULL on failure. */
|
||||
static char *pkgmap_read_file(const char *path, int *out_len) {
|
||||
@@ -747,6 +750,100 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
|
||||
return map;
|
||||
}
|
||||
|
||||
/* Returns true if basename is a package manifest we know how to parse.
|
||||
* Used by the filesystem walker; cbm_pkgmap_try_parse is the source of
|
||||
* truth for which basenames produce entries. */
|
||||
static bool is_pkgmap_manifest_basename(const char *basename) {
|
||||
if (!basename) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(basename, "package.json") == 0 || strcmp(basename, "go.mod") == 0 ||
|
||||
strcmp(basename, "Cargo.toml") == 0 || strcmp(basename, "pyproject.toml") == 0 ||
|
||||
strcmp(basename, "composer.json") == 0 || strcmp(basename, "pubspec.yaml") == 0 ||
|
||||
strcmp(basename, "pom.xml") == 0 || strcmp(basename, "build.gradle") == 0 ||
|
||||
strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0) {
|
||||
return true;
|
||||
}
|
||||
return ends_with(basename, ".gemspec");
|
||||
}
|
||||
|
||||
/* Recursive filesystem walker that finds and parses package manifest
|
||||
* files independently of the main discovery filter. The main discovery
|
||||
* filter intentionally hides package.json / composer.json etc. from
|
||||
* code indexing (they're config, not source), but pass_pkgmap still
|
||||
* needs to read them to resolve workspace imports. Skips directories
|
||||
* matched by the shared cbm_should_skip_dir helper so we don't walk
|
||||
* node_modules, .git, build, etc. Returns the number of manifests
|
||||
* parsed, accumulated across the whole walk. */
|
||||
static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir,
|
||||
cbm_pkg_entries_t *entries) {
|
||||
DIR *dir = opendir(abs_dir);
|
||||
if (!dir) {
|
||||
return 0;
|
||||
}
|
||||
int parsed = 0;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
const char *name = entry->d_name;
|
||||
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
char abs_path[PKGMAP_PATH_BUF];
|
||||
char rel_path[PKGMAP_PATH_BUF];
|
||||
snprintf(abs_path, sizeof(abs_path), "%s/%s", abs_dir, name);
|
||||
if (rel_dir && rel_dir[0]) {
|
||||
snprintf(rel_path, sizeof(rel_path), "%s/%s", rel_dir, name);
|
||||
} else {
|
||||
snprintf(rel_path, sizeof(rel_path), "%s", name);
|
||||
}
|
||||
struct stat st;
|
||||
if (lstat(abs_path, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (cbm_should_skip_dir(name, CBM_MODE_FULL)) {
|
||||
continue;
|
||||
}
|
||||
parsed += pkgmap_walk_dir(abs_path, rel_path, entries);
|
||||
continue;
|
||||
}
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_pkgmap_manifest_basename(name)) {
|
||||
continue;
|
||||
}
|
||||
int source_len = 0;
|
||||
char *source = pkgmap_read_file(abs_path, &source_len);
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_pkgmap_try_parse(name, rel_path, source, source_len, entries)) {
|
||||
parsed++;
|
||||
}
|
||||
free(source);
|
||||
}
|
||||
closedir(dir);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Scan a repository for package manifest files via the filesystem
|
||||
* walker above. Always-available companion to the parallel path's
|
||||
* per-worker manifest parsing, which is bound to whatever `files[]`
|
||||
* the discoverer produces and therefore misses ignored manifests like
|
||||
* package.json. NULL-safe; returns 0 entries when repo_path is unset. */
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries) {
|
||||
if (!repo_path || !entries) {
|
||||
return 0;
|
||||
}
|
||||
int parsed = pkgmap_walk_dir(repo_path, "", entries);
|
||||
cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Build pkgmap for sequential path (reads manifest files directly) */
|
||||
CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count,
|
||||
const char *project_name) {
|
||||
@@ -755,15 +852,7 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
|
||||
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
const char *basename = path_basename(files[i].rel_path);
|
||||
/* Quick check: is this a manifest file? */
|
||||
bool is_manifest =
|
||||
(strcmp(basename, "package.json") == 0 || strcmp(basename, "go.mod") == 0 ||
|
||||
strcmp(basename, "Cargo.toml") == 0 || strcmp(basename, "pyproject.toml") == 0 ||
|
||||
strcmp(basename, "composer.json") == 0 || strcmp(basename, "pubspec.yaml") == 0 ||
|
||||
strcmp(basename, "pom.xml") == 0 || strcmp(basename, "build.gradle") == 0 ||
|
||||
strcmp(basename, "build.gradle.kts") == 0 || strcmp(basename, "mix.exs") == 0 ||
|
||||
ends_with(basename, ".gemspec"));
|
||||
if (!is_manifest) {
|
||||
if (!is_pkgmap_manifest_basename(basename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -782,6 +871,44 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
|
||||
return map;
|
||||
}
|
||||
|
||||
/* Variant of cbm_pkgmap_build_from_files that ALSO walks the repo
|
||||
* filesystem to pick up manifests filtered out by the main discoverer
|
||||
* (the canonical case: package.json, which is in IGNORED_JSON_FILES).
|
||||
* Falls back to the files[]-only behaviour if repo_path is NULL. */
|
||||
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
|
||||
int file_count, const char *project_name) {
|
||||
cbm_pkg_entries_t entries;
|
||||
cbm_pkg_entries_init(&entries);
|
||||
|
||||
/* Manifests already visible through discovery (Cargo.toml, go.mod,
|
||||
* pyproject.toml, ...). package.json typically isn't, but we still
|
||||
* harvest whatever the discovery filter exposed in case downstream
|
||||
* filters change. */
|
||||
int from_files = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
const char *basename = path_basename(files[i].rel_path);
|
||||
if (!is_pkgmap_manifest_basename(basename)) {
|
||||
continue;
|
||||
}
|
||||
from_files++;
|
||||
int source_len = 0;
|
||||
char *source = pkgmap_read_file(files[i].path, &source_len);
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
cbm_pkgmap_try_parse(basename, files[i].rel_path, source, source_len, &entries);
|
||||
free(source);
|
||||
}
|
||||
|
||||
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries);
|
||||
cbm_log_info("pkgmap.scan", "manifests_from_files", pkgmap_itoa(from_files),
|
||||
"manifests_from_walk", pkgmap_itoa(from_walk),
|
||||
"entries", pkgmap_itoa(entries.count));
|
||||
CBMHashTable *map = cbm_pkgmap_build(&entries, SKIP_ONE, project_name);
|
||||
cbm_pkg_entries_free(&entries);
|
||||
return map;
|
||||
}
|
||||
|
||||
static void pkgmap_free_entry(const char *key, void *value, void *userdata) {
|
||||
(void)userdata;
|
||||
free((void *)key);
|
||||
|
||||
@@ -779,6 +779,281 @@ static void create_data_flows(cbm_gbuf_t *gb) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── SvelteKit filesystem-based route extractor ──────────────────────
|
||||
*
|
||||
* SvelteKit derives REST endpoints and SSR loaders from the filesystem
|
||||
* layout under `src/routes/`. No `app.get(...)` style call exists for
|
||||
* pass_calls.c to pick up, so we walk File nodes and synthesise Route
|
||||
* nodes + HANDLES edges directly here.
|
||||
*
|
||||
* Recognised filenames (TypeScript and JavaScript variants):
|
||||
*
|
||||
* +server.{ts,js} → REST endpoints, exports GET/POST/...
|
||||
* +page.server.{ts,js} → SSR page server module: load + actions
|
||||
* +layout.server.{ts,js} → SSR layout server module: load
|
||||
*
|
||||
* Route path is derived from the directory chain under `routes/`:
|
||||
*
|
||||
* apps/x/src/routes/foo/+server.ts → /foo
|
||||
* apps/x/src/routes/api/items/+server.ts → /api/items
|
||||
* apps/x/src/routes/(grp)/foo/+server.ts → /foo (group stripped)
|
||||
* apps/x/src/routes/[slug]/+server.ts → /:slug (param rewrite)
|
||||
* apps/x/src/routes/+server.ts → / (root)
|
||||
*/
|
||||
|
||||
enum {
|
||||
SKR_PATH_BUF = 1024,
|
||||
SKR_NAME_BUF = 64,
|
||||
};
|
||||
|
||||
/* Detect the SvelteKit kind of a file path. Returns 1 for +server,
|
||||
* 2 for +page.server, 3 for +layout.server; 0 if not a SvelteKit
|
||||
* server-side route file. */
|
||||
static int sveltekit_file_kind(const char *file_path) {
|
||||
if (!file_path) {
|
||||
return 0;
|
||||
}
|
||||
/* The basename must match one of the three patterns. We also require
|
||||
* a "/routes/" segment somewhere in the path so we don't snag files
|
||||
* in unrelated directories that happen to be named "+server.ts". */
|
||||
if (!strstr(file_path, "/routes/")) {
|
||||
return 0;
|
||||
}
|
||||
const char *slash = strrchr(file_path, '/');
|
||||
const char *base = slash ? slash + 1 : file_path;
|
||||
if (strcmp(base, "+server.ts") == 0 || strcmp(base, "+server.js") == 0) {
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(base, "+page.server.ts") == 0 || strcmp(base, "+page.server.js") == 0) {
|
||||
return 2;
|
||||
}
|
||||
if (strcmp(base, "+layout.server.ts") == 0 || strcmp(base, "+layout.server.js") == 0) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Compute the URL route path for a SvelteKit file path. Writes into
|
||||
* `out` (caller-provided buffer) and returns the buffer or NULL on
|
||||
* error. The leading "/" is always present; "/" is used for the root
|
||||
* route. Group segments wrapped in parentheses are stripped. Dynamic
|
||||
* params `[slug]` become `:slug`, and rest params `[...slug]` become
|
||||
* `*slug` so the result is recognisable as a path pattern. */
|
||||
static const char *sveltekit_route_path(const char *file_path, char *out, int outsz) {
|
||||
if (!file_path || !out || outsz <= 1) {
|
||||
return NULL;
|
||||
}
|
||||
const char *routes_seg = strstr(file_path, "/routes/");
|
||||
if (!routes_seg) {
|
||||
return NULL;
|
||||
}
|
||||
/* Walk segment-by-segment between "/routes/" and the trailing "+...". */
|
||||
const char *p = routes_seg + strlen("/routes/");
|
||||
const char *last_slash = strrchr(file_path, '/');
|
||||
if (!last_slash || last_slash < p) {
|
||||
/* File is directly under /routes/ — root route. */
|
||||
out[0] = '/';
|
||||
out[1] = '\0';
|
||||
return out;
|
||||
}
|
||||
/* p points at first char after /routes/, last_slash points at the
|
||||
* slash before the +...filename. Iterate segments between them. */
|
||||
int pos = 0;
|
||||
out[pos] = '\0';
|
||||
while (p < last_slash) {
|
||||
const char *seg_end = strchr(p, '/');
|
||||
if (!seg_end || seg_end > last_slash) {
|
||||
seg_end = last_slash;
|
||||
}
|
||||
size_t seg_len = (size_t)(seg_end - p);
|
||||
if (seg_len > 0) {
|
||||
/* Group `(name)` — skip entirely. */
|
||||
if (p[0] == '(' && p[seg_len - 1] == ')') {
|
||||
p = seg_end + 1;
|
||||
continue;
|
||||
}
|
||||
/* Emit "/" then rewritten segment. */
|
||||
if (pos + 1 < outsz - 1) {
|
||||
out[pos++] = '/';
|
||||
}
|
||||
/* Dynamic param: `[slug]` → `:slug`, `[...slug]` → `*slug`. */
|
||||
if (seg_len >= 2 && p[0] == '[' && p[seg_len - 1] == ']') {
|
||||
const char *inner = p + 1;
|
||||
size_t inner_len = seg_len - 2;
|
||||
if (inner_len >= 3 && strncmp(inner, "...", 3) == 0) {
|
||||
if (pos < outsz - 1) {
|
||||
out[pos++] = '*';
|
||||
}
|
||||
inner += 3;
|
||||
inner_len -= 3;
|
||||
} else {
|
||||
if (pos < outsz - 1) {
|
||||
out[pos++] = ':';
|
||||
}
|
||||
}
|
||||
/* Strip an optional `=matcher` suffix after the param name. */
|
||||
size_t copy_len = inner_len;
|
||||
for (size_t i = 0; i < inner_len; i++) {
|
||||
if (inner[i] == '=') {
|
||||
copy_len = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((int)copy_len > outsz - 1 - pos) {
|
||||
copy_len = (size_t)(outsz - 1 - pos);
|
||||
}
|
||||
memcpy(out + pos, inner, copy_len);
|
||||
pos += (int)copy_len;
|
||||
} else {
|
||||
size_t copy_len = seg_len;
|
||||
if ((int)copy_len > outsz - 1 - pos) {
|
||||
copy_len = (size_t)(outsz - 1 - pos);
|
||||
}
|
||||
memcpy(out + pos, p, copy_len);
|
||||
pos += (int)copy_len;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
}
|
||||
p = seg_end + 1;
|
||||
}
|
||||
if (pos == 0) {
|
||||
out[pos++] = '/';
|
||||
out[pos] = '\0';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Returns the HTTP method string a function name maps to for a +server
|
||||
* file, or NULL if the function isn't a SvelteKit REST handler. The set
|
||||
* mirrors SvelteKit's documented HTTP verb exports. */
|
||||
static const char *sveltekit_server_method(const char *name) {
|
||||
if (!name) {
|
||||
return NULL;
|
||||
}
|
||||
static const char *const verbs[] = {
|
||||
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(verbs) / sizeof(verbs[0]); i++) {
|
||||
if (strcmp(name, verbs[i]) == 0) {
|
||||
return verbs[i];
|
||||
}
|
||||
}
|
||||
/* `fallback` catches any verb not explicitly exported. */
|
||||
if (strcmp(name, "fallback") == 0) {
|
||||
return "ANY";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
cbm_gbuf_t *gb;
|
||||
int routes_created;
|
||||
int handles_created;
|
||||
int files_seen;
|
||||
} sveltekit_ctx_t;
|
||||
|
||||
/* Process one File node: if it matches a SvelteKit server-side file,
|
||||
* synthesise a Route node and HANDLES edges from any handler functions
|
||||
* (or actions Variable) defined in the file. */
|
||||
static void sveltekit_file_visitor(const cbm_gbuf_node_t *node, void *userdata) {
|
||||
sveltekit_ctx_t *ctx = (sveltekit_ctx_t *)userdata;
|
||||
if (!node || !node->label || strcmp(node->label, "File") != 0) {
|
||||
return;
|
||||
}
|
||||
int kind = sveltekit_file_kind(node->file_path);
|
||||
if (kind == 0) {
|
||||
return;
|
||||
}
|
||||
ctx->files_seen++;
|
||||
|
||||
char route_path[SKR_PATH_BUF];
|
||||
if (!sveltekit_route_path(node->file_path, route_path, sizeof(route_path))) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find every DEFINES edge from this file to a Function or Variable. */
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int edge_count = 0;
|
||||
if (cbm_gbuf_find_edges_by_source_type(ctx->gb, node->id, "DEFINES", &edges, &edge_count) != 0
|
||||
|| edge_count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
const cbm_gbuf_node_t *child = cbm_gbuf_find_by_id(ctx->gb, edges[i]->target_id);
|
||||
if (!child || !child->name || !child->label) {
|
||||
continue;
|
||||
}
|
||||
bool is_fn = strcmp(child->label, "Function") == 0;
|
||||
bool is_var = strcmp(child->label, "Variable") == 0;
|
||||
if (!is_fn && !is_var) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *method = NULL;
|
||||
bool is_actions = false;
|
||||
if (kind == 1) {
|
||||
method = sveltekit_server_method(child->name);
|
||||
} else if (kind == 2) {
|
||||
if (is_fn && strcmp(child->name, "load") == 0) {
|
||||
method = "LOAD";
|
||||
} else if (strcmp(child->name, "actions") == 0) {
|
||||
/* SvelteKit form actions: emit a single Route at this
|
||||
* path with method=ACTIONS; finer-grained action names
|
||||
* aren't exposed as top-level identifiers. */
|
||||
method = "ACTIONS";
|
||||
is_actions = is_var;
|
||||
}
|
||||
} else if (kind == 3) {
|
||||
if (is_fn && strcmp(child->name, "load") == 0) {
|
||||
method = "LOAD";
|
||||
}
|
||||
}
|
||||
if (!method) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, route_path);
|
||||
char route_props[CBM_SZ_256];
|
||||
snprintf(route_props, sizeof(route_props),
|
||||
"{\"method\":\"%s\",\"framework\":\"sveltekit\"}", method);
|
||||
int64_t route_id = cbm_gbuf_upsert_node(ctx->gb, "Route", route_path, route_qn, "", 0, 0,
|
||||
route_props);
|
||||
if (route_id == 0) {
|
||||
continue;
|
||||
}
|
||||
ctx->routes_created++;
|
||||
|
||||
char hprops[CBM_SZ_256];
|
||||
snprintf(hprops, sizeof(hprops),
|
||||
"{\"handler\":\"%s\",\"framework\":\"sveltekit\"%s}",
|
||||
child->qualified_name ? child->qualified_name : child->name,
|
||||
is_actions ? ",\"via\":\"actions_object\"" : "");
|
||||
cbm_gbuf_insert_edge(ctx->gb, child->id, route_id, "HANDLES", hprops);
|
||||
ctx->handles_created++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Public entry point: scan all File nodes for SvelteKit server modules
|
||||
* and synthesise Route + HANDLES nodes from the filesystem convention. */
|
||||
static void create_sveltekit_routes(cbm_gbuf_t *gb) {
|
||||
if (!gb) {
|
||||
return;
|
||||
}
|
||||
sveltekit_ctx_t ctx = {.gb = gb, .routes_created = 0, .handles_created = 0, .files_seen = 0};
|
||||
cbm_gbuf_foreach_node(gb, sveltekit_file_visitor, &ctx);
|
||||
if (ctx.files_seen > 0) {
|
||||
char b1[CBM_SZ_16];
|
||||
char b2[CBM_SZ_16];
|
||||
char b3[CBM_SZ_16];
|
||||
snprintf(b1, sizeof(b1), "%d", ctx.files_seen);
|
||||
snprintf(b2, sizeof(b2), "%d", ctx.routes_created);
|
||||
snprintf(b3, sizeof(b3), "%d", ctx.handles_created);
|
||||
cbm_log_info("pass.sveltekit_routes", "files", b1, "routes", b2, "handles", b3);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) {
|
||||
if (!gb) {
|
||||
return;
|
||||
@@ -812,4 +1087,9 @@ void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) {
|
||||
* Scans Class nodes from .proto files, follows DEFINES_METHOD edges
|
||||
* to find rpc methods, creates __grpc__ServiceName/MethodName Route nodes. */
|
||||
create_grpc_routes(gb);
|
||||
|
||||
/* Phase 5: filesystem-based SvelteKit routes (+server / +page.server /
|
||||
* +layout.server) — no call-site equivalent for pass_calls.c to pick
|
||||
* up, so we walk File nodes directly here. */
|
||||
create_sveltekit_routes(gb);
|
||||
}
|
||||
|
||||
+17
-5
@@ -497,9 +497,12 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
struct timespec *t) {
|
||||
cbm_log_info("pipeline.mode", "mode", "sequential", "files", itoa_buf(file_count));
|
||||
|
||||
/* Build package map from manifest files (sequential: read manifests directly) */
|
||||
/* Build package map from manifest files (sequential: read manifests directly) */
|
||||
cbm_pipeline_set_pkgmap(cbm_pkgmap_build_from_files(files, file_count, ctx->project_name));
|
||||
/* Build package map from manifest files (sequential: read manifests directly).
|
||||
* Use the repo-walking variant so manifests filtered out by the main
|
||||
* discoverer (package.json, composer.json) still feed pkgmap and let
|
||||
* workspace imports like `@my/pkg` resolve to their target Module. */
|
||||
cbm_pipeline_set_pkgmap(
|
||||
cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count, ctx->project_name));
|
||||
|
||||
CBMFileResult **seq_cache = (CBMFileResult **)calloc(file_count, sizeof(CBMFileResult *));
|
||||
if (seq_cache) {
|
||||
@@ -590,8 +593,17 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
* extract. */
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, t);
|
||||
/* Cross-file LSP (type-aware call/usage resolution across files) — the
|
||||
* most expensive phase — runs in every mode. */
|
||||
const bool run_cross_lsp = true;
|
||||
* most expensive phase. CBM_DISABLE_LSP_CROSS=1 opts out (it can SIGSEGV
|
||||
* on large TS projects — see #340/#344); with cross-LSP off, all_defs
|
||||
* stays NULL and the fused resolver simply no-ops cross-file resolution
|
||||
* (per-file LSP already ran during extract). */
|
||||
char cbm_lsp_cross_env[CBM_SZ_16];
|
||||
const bool run_cross_lsp =
|
||||
cbm_safe_getenv("CBM_DISABLE_LSP_CROSS", cbm_lsp_cross_env, sizeof(cbm_lsp_cross_env),
|
||||
NULL) == NULL;
|
||||
if (!run_cross_lsp) {
|
||||
cbm_log_info("lsp_cross.skipped", "reason", "CBM_DISABLE_LSP_CROSS env set");
|
||||
}
|
||||
char **def_modules = NULL;
|
||||
int def_count = 0;
|
||||
CBMLSPDef *all_defs = NULL;
|
||||
|
||||
@@ -90,6 +90,9 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
|
||||
const char *project_name);
|
||||
|
||||
/* Build pkgmap by reading manifest files from the files array (sequential path). */
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries);
|
||||
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
|
||||
int file_count, const char *project_name);
|
||||
CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count,
|
||||
const char *project_name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user