mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 14:49:36 +08:00
fix(viewer): pages that went blank — repeated ids, and Steps on a function full of checks (#2264)
* fix(viewer): a repeated id no longer blanks Entry points or a component's Symbol view The viewer keys its lists by id and a repeat stops the page drawing. The routes list was a row per (route, edge), so an inline handler came back once per call in its body; it is now one row per route — its bound handler, or the route itself as an inline handler. A Vue/Svelte/Astro component's members listed its script's symbols twice, plus the component and its file; each member is now listed once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(viewer): Steps reads a function full of checks in one pass The in-order reading kept every way forward through an `if` with no `else` twice — under its condition and without — so eighty checks that draw nothing (jsoup's `parse`) made 2^80 ways and the page died on a stack overflow. Ways waiting at the same place are merged, keeping the conditions they all share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d45edca3fd
commit
81e08b76ed
@@ -244,10 +244,17 @@ function buildMembers(
|
||||
endpoints: Map<string, Node>,
|
||||
overrides?: Map<string, WireOverride>
|
||||
): { items: WireMember[]; total: number } {
|
||||
// Each member once, and never the symbol itself or a file: a component in a
|
||||
// Vue / Svelte / Astro file was recorded as containing its script's file node
|
||||
// and every symbol in it, so the walk one level down met those symbols
|
||||
// again — and a member listed twice is a repeated key the viewer cannot draw.
|
||||
const seen = new Set<string>([focal.id]);
|
||||
const admit = (child: Node | undefined): child is Node =>
|
||||
!!child && child.kind !== 'file' && !seen.has(child.id) && !!seen.add(child.id);
|
||||
const direct: Array<{ node: Node; parentId: string; depth: number }> = [];
|
||||
for (const edge of containsOut) {
|
||||
const child = endpoints.get(edge.target);
|
||||
if (child) direct.push({ node: child, parentId: focal.id, depth: 1 });
|
||||
if (admit(child)) direct.push({ node: child, parentId: focal.id, depth: 1 });
|
||||
}
|
||||
|
||||
const containerIds = direct
|
||||
@@ -260,7 +267,7 @@ function buildMembers(
|
||||
const grandNodes = cg.getNodesByIds(grandEdges.map((e) => e.target));
|
||||
for (const edge of grandEdges) {
|
||||
const child = grandNodes.get(edge.target);
|
||||
if (child) nested.push({ node: child, parentId: edge.source, depth: 2 });
|
||||
if (admit(child)) nested.push({ node: child, parentId: edge.source, depth: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
import type { CodeGraph } from '../../index';
|
||||
import { intParam } from './respond';
|
||||
import { toPosixPath } from './wire';
|
||||
import { routeRoots } from './route-roots';
|
||||
import { isTestFile } from '../../search/query-utils';
|
||||
|
||||
/**
|
||||
* HTTP verbs a route name may lead with, plus the two stand-ins the resolvers
|
||||
@@ -56,6 +58,8 @@ export interface WireRoute {
|
||||
routeFile: string;
|
||||
routeLine: number;
|
||||
routeId: string;
|
||||
/** The handler is written inline at the registration: the route stands in for it. */
|
||||
inline: boolean;
|
||||
}
|
||||
|
||||
export interface WireRoutes {
|
||||
@@ -86,11 +90,62 @@ export function splitRouteName(url: string): { method: string | null; path: stri
|
||||
*/
|
||||
const MIN_LIMIT = 3;
|
||||
|
||||
/** Manifest rows fetched per route shown, so folding repeats still fills the page. */
|
||||
const ROWS_PER_ROUTE = 4;
|
||||
|
||||
type ManifestRow = NonNullable<ReturnType<CodeGraph['getRoutingManifest']>>['entries'][number];
|
||||
|
||||
/**
|
||||
* The manifest's rows folded to one per route, in its order: the row naming
|
||||
* the route's root (the handler a resolver bound, or the component a screen
|
||||
* renders), else the route's first row. A route whose root is its own inline
|
||||
* body is that route, labelled as an inline handler — not whichever call in
|
||||
* the body sorted first — and is left out when it is written in a test, as
|
||||
* the manifest leaves out test handlers.
|
||||
*/
|
||||
function oneRowPerRoute(cg: CodeGraph, entries: readonly ManifestRow[]): Array<ManifestRow & { inline: boolean }> {
|
||||
const byRoute = new Map<string, ManifestRow[]>();
|
||||
for (const entry of entries) {
|
||||
const list = byRoute.get(entry.routeId);
|
||||
if (list) list.push(entry);
|
||||
else byRoute.set(entry.routeId, [entry]);
|
||||
}
|
||||
const routes = [...cg.getNodesByIds([...byRoute.keys()]).values()];
|
||||
const roots = routeRoots(cg, routes);
|
||||
const out: Array<ManifestRow & { inline: boolean }> = [];
|
||||
for (const [routeId, list] of byRoute) {
|
||||
const root = roots.get(routeId);
|
||||
if (root?.inline) {
|
||||
const first = list[0]!;
|
||||
if (isTestFile(first.routeFile)) continue;
|
||||
out.push({
|
||||
...first,
|
||||
handler: 'inline handler',
|
||||
handlerKind: 'route',
|
||||
handlerFile: first.routeFile,
|
||||
handlerLine: first.routeLine,
|
||||
inline: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const named = root
|
||||
? list.find((e) => e.handlerFile === root.node.filePath && e.handlerLine === root.node.startLine && e.handler === root.node.name)
|
||||
: undefined;
|
||||
out.push({ ...(named ?? list[0]!), inline: false });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildRoutes(cg: CodeGraph, query: URLSearchParams): WireRoutes {
|
||||
const limit = intParam(query, 'limit', { min: MIN_LIMIT, max: 500, default: 200 });
|
||||
|
||||
// One row over the limit, purely to learn whether there were more.
|
||||
const manifest = cg.getRoutingManifest(limit + 1);
|
||||
// The engine's manifest is a row per (route, edge): a route bound to two
|
||||
// symbols, or an inline handler whose every call reads as a "handler"
|
||||
// (hono's `GET /stream/text` came back three times, as `streamText`,
|
||||
// `writeln` and `sleep`), repeats. A route has ONE answer to "what serves
|
||||
// this" — route-roots.ts's — so rows are over-fetched and folded onto it.
|
||||
const fetched = limit * ROWS_PER_ROUTE + 1;
|
||||
const manifest = cg.getRoutingManifest(fetched);
|
||||
const routeCount = cg.getStats().nodesByKind.route ?? 0;
|
||||
|
||||
if (!manifest) {
|
||||
@@ -105,8 +160,9 @@ export function buildRoutes(cg: CodeGraph, query: URLSearchParams): WireRoutes {
|
||||
};
|
||||
}
|
||||
|
||||
const truncated = manifest.entries.length > limit;
|
||||
const rows = manifest.entries.slice(0, limit);
|
||||
const folded = oneRowPerRoute(cg, manifest.entries);
|
||||
const truncated = folded.length > limit || manifest.entries.length >= fetched;
|
||||
const rows = folded.slice(0, limit);
|
||||
|
||||
// Every row's handler file in one batched query, so a project that scatters
|
||||
// handlers across hundreds of files still costs one query per chunk, and no
|
||||
@@ -127,11 +183,13 @@ export function buildRoutes(cg: CodeGraph, query: URLSearchParams): WireRoutes {
|
||||
handlerKind: entry.handlerKind,
|
||||
file: toPosixPath(entry.handlerFile),
|
||||
line: entry.handlerLine,
|
||||
handlerId:
|
||||
byFileLineName.get(`${entry.handlerFile} ${entry.handlerLine} ${entry.handler}`) ?? null,
|
||||
handlerId: entry.inline
|
||||
? entry.routeId
|
||||
: byFileLineName.get(`${entry.handlerFile} ${entry.handlerLine} ${entry.handler}`) ?? null,
|
||||
routeFile: toPosixPath(entry.routeFile),
|
||||
routeLine: entry.routeLine,
|
||||
routeId: entry.routeId,
|
||||
inline: entry.inline,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user