(cg: CodeGraph, projectRoot: string, nodeId: string)
| 75 | } |
| 76 | |
| 77 | export async function buildNode(cg: CodeGraph, projectRoot: string, nodeId: string): Promise<unknown> { |
| 78 | const node = cg.getNode(nodeId); |
| 79 | if (!node) { |
| 80 | throw notFound( |
| 81 | 'No symbol with that id is in this index.', |
| 82 | 'Symbol ids change whenever the file is re-indexed — search for the symbol by ' + |
| 83 | 'name instead of reusing an id from an older session.' |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | const incomingAll = cg.getIncomingEdges(nodeId); |
| 88 | const outgoingAll = cg.getOutgoingEdges(nodeId); |
| 89 | |
| 90 | // `contains` is structure, not dependency: upward it is the parent (already in |
| 91 | // `ancestors`), downward it is the members outline. Leaving it in the rails |
| 92 | // would put a symbol's own class in its caller list. |
| 93 | const incoming = incomingAll.filter((e) => e.kind !== 'contains'); |
| 94 | const outgoingRest: Edge[] = []; |
| 95 | const containsOut: Edge[] = []; |
| 96 | for (const edge of outgoingAll) { |
| 97 | if (edge.kind === 'contains') containsOut.push(edge); |
| 98 | else outgoingRest.push(edge); |
| 99 | } |
| 100 | |
| 101 | const ancestors = cg.getAncestors(nodeId); |
| 102 | |
| 103 | // --------------------------------------------------------------------------- |
| 104 | // One batched resolve for every endpoint this payload names. |
| 105 | // --------------------------------------------------------------------------- |
| 106 | const endpointIds = new Set<string>(); |
| 107 | for (const edge of incoming) endpointIds.add(edge.source); |
| 108 | for (const edge of outgoingRest) endpointIds.add(edge.target); |
| 109 | for (const edge of containsOut) endpointIds.add(edge.target); |
| 110 | const endpoints = cg.getNodesByIds([...endpointIds]); |
| 111 | |
| 112 | // A `references` edge into a type is "uses type X", not "calls X" — the |
| 113 | // header shows those as chips rather than as callee rows. Split at the EDGE |
| 114 | // level so a class that is both instantiated and named as a type appears in |
| 115 | // both places, which is what the source actually says. |
| 116 | const calleeEdges: Edge[] = []; |
| 117 | const typeRefs: Edge[] = []; |
| 118 | for (const edge of outgoingRest) { |
| 119 | const target = endpoints.get(edge.target); |
| 120 | if (edge.kind === 'references' && target && TYPE_KINDS.has(target.kind)) typeRefs.push(edge); |
| 121 | else calleeEdges.push(edge); |
| 122 | } |
| 123 | |
| 124 | // --------------------------------------------------------------------------- |
| 125 | // Rails |
| 126 | // --------------------------------------------------------------------------- |
| 127 | const focalFile = toPosixPath(node.filePath); |
| 128 | |
| 129 | const incomingGroups = groupRelations(incoming, (e) => e.source, endpoints); |
| 130 | incomingGroups.sort((a, b) => { |
| 131 | // The symbol's own file first ("same file" in the left rail), then |
| 132 | // production code, then tests — so a cap trims the least useful end. |
| 133 | const aSame = a.node.file === focalFile ? 0 : 1; |
| 134 | const bSame = b.node.file === focalFile ? 0 : 1; |
no test coverage detected