fix(union): complete downstream container handling

This commit is contained in:
ctype_lab
2026-08-06 17:50:25 +09:00
parent e922563e05
commit e2195940fb
10 changed files with 136 additions and 32 deletions
+4 -4
View File
@@ -157,7 +157,7 @@ const DEFAULT_BUILD_OPTIONS: Required<BuildContextOptions> = {
* they tell you something exists, not how it works.
*/
const HIGH_VALUE_NODE_KINDS: NodeKind[] = [
'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'trait',
'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'union', 'trait',
'component', 'route', 'variable', 'constant', 'enum', 'module', 'namespace',
];
@@ -503,7 +503,7 @@ export class ContextBuilder {
// like RestController, BulkRequest, AllocationService — not nodes named exactly that.
// Also tries stem variants: "caching" → "cache" finds Cache, CacheBuilder.
if (symbolsFromQuery.length > 0) {
const definitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
const definitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'union', 'trait',
'protocol', 'enum', 'type_alias'];
// Expand symbols with stem variants for broader definition matching
const expandedSymbols = new Set(symbolsFromQuery);
@@ -754,7 +754,7 @@ export class ContextBuilder {
// LIKE reliably finds these substring matches. Results are appended with
// guaranteed slots so they don't compete with higher-scoring prefix matches.
if (symbolsFromQuery.length > 0) {
const camelDefinitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
const camelDefinitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'union', 'trait',
'protocol', 'enum', 'type_alias'];
// Callable kinds participate too: in service-layer codebases the
// camel-infix definers of a queried FIELD are methods/functions
@@ -977,7 +977,7 @@ export class ContextBuilder {
// before reaching extends/implements neighbors. This dedicated step
// ensures subclasses and superclasses always appear in results.
// Budget: up to maxNodes/4 hierarchy nodes to avoid flooding.
const typeHierarchyKinds = new Set<string>(['class', 'interface', 'struct', 'trait', 'protocol']);
const typeHierarchyKinds = new Set<string>(['class', 'interface', 'struct', 'union', 'trait', 'protocol']);
const maxHierarchyNodes = Math.ceil(opts.maxNodes / 4);
let hierarchyNodesAdded = 0;
for (const result of filteredResults) {
+2
View File
@@ -173,6 +173,7 @@ export class GraphQueryManager {
const allNodes: Node[] = [];
const kinds: Node['kind'][] = [
'class',
'union',
'function',
'method',
'interface',
@@ -347,6 +348,7 @@ export class GraphQueryManager {
'module',
'class',
'struct',
'union',
'interface',
'trait',
'function',
+1 -1
View File
@@ -564,7 +564,7 @@ export class GraphTraverser {
// into their children so that callers of contained methods appear in impact
const focalNode = this.queries.getNodeById(nodeId);
if (focalNode) {
const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
const containerKinds = new Set(['class', 'interface', 'struct', 'union', 'trait', 'protocol', 'module', 'enum']);
if (containerKinds.has(focalNode.kind)) {
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
if (containsEdges.length > 0) {
+4 -4
View File
@@ -118,7 +118,7 @@ const RUST_PATH_PREFIXES = new Set(['crate', 'super', 'self']);
* multi-thousand-character wall of source that bloats the agent's context.
*/
const CONTAINER_NODE_KINDS = new Set<NodeKind>([
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
'class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
]);
/** Last `::` / `.` / `/`-separated segment of a qualified symbol. */
@@ -2927,7 +2927,7 @@ export class ToolHandler {
const ROOT_CAP = 5; // only the symbols the query actually targeted
const FILE_CAP = 4; // caller files listed per symbol before "+N more"
const MEANINGFUL = new Set<string>([
'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol',
'function', 'method', 'class', 'interface', 'struct', 'union', 'trait', 'protocol',
'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field',
]);
const rel = (p: string) => p.replace(/\\/g, '/');
@@ -3452,7 +3452,7 @@ export class ToolHandler {
// displaces a flow-central file. Bounded: only the few named seeds, only the
// types in their signatures.
const CALLABLE_KINDS = new Set(['method', 'function', 'component', 'constructor']);
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
const TYPE_KINDS = new Set(['class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
const SIG_EDGE = new Set(['references', 'type_of', 'returns']);
const changeSurfaceCandidates: Node[] = [];
const seenChangeSurface = new Set<string>();
@@ -4538,7 +4538,7 @@ export class ToolHandler {
// query actually asked about (#185 follow-up — Session.swift in
// Alamofire is the canonical case: the `Session` class spans ~1,400
// lines). We want the granular symbols inside, not the envelope.
const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']);
const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'union', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']);
// Cluster from this file's gathered nodes PLUS any callable the agent NAMED that
// lives here. Explore's relevance gather can miss a named method def in a huge
// non-sibling file — Django's query.py is 3,040 lines and `_fetch_all` (L2237)
+14 -12
View File
@@ -296,7 +296,7 @@ function resolveTypeName(name: string, objEnv: Map<string, string> | undefined):
let n = name;
for (let i = 0; objEnv && i < 5; i++) {
const v = objEnv.get(n);
const t = v?.trim().match(/^(?:struct\s+)?(\w+)$/);
const t = v?.trim().match(/^(?:(?:struct|union)\s+)?(\w+)$/);
if (!t) break;
n = t[1]!;
}
@@ -370,20 +370,20 @@ const INCLUDABLE_EXT = /\.(def|inc|h|hh|hpp|hxx|c|cc|cpp|cxx|ipp|tcc|tbl)$/i;
* are excluded: `resolveTypeName` would rewrite to a dead-end token that can
* never name a struct, so skipping them is exact, and it drops the register
* flood. */
const OBJ_ALIAS_RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:struct[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm;
const OBJ_ALIAS_RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:(?:struct|union)[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm;
/** `(?:struct )?TYPE name[opt] = {` initializers, where TYPE is a struct that
* has ≥1 fn-pointer field. Handles both single (`= {…}`) and array
* (`[] = { {…}, {…} }`) forms. Macro calls inside an element are expanded first. */
const INIT_RE =
/(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{/g;
/(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(?:(?:struct|union)\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{/g;
/** `struct TAG { … } var[opt] [= {…}]` — the struct is defined INLINE with the
* table (vim's `cmdname`/`nv_cmd`); its layout never became a node, so parse it
* here and register it before reading the entries. No leading anchor: a
* `struct TAG {` with a brace body is always a definition (it may be preceded
* by a `#define …` line ending in a digit, as in vim), and the trailing
* `var … = {` check below is what distinguishes a TABLE from a plain type. */
const INLINE_STRUCT_RE = /\bstruct\s+(\w+)\s*\{/g;
const INLINE_STRUCT_RE = /\b(?:struct|union)\s+(\w+)\s*\{/g;
/** `(?:static …)* ELEMTYPE [*] name[…] = { … }` — a bare array of function
* pointers (no struct wrapper). The optional `*` covers a function-TYPE
* typedef element (`opcode_t *opcodes[]`); a function-pointer typedef element
@@ -854,12 +854,14 @@ export async function cFnPointerDispatchEdges(
if (fields.some((f) => f.isFnPtr)) structLayout.set(name, fields);
};
for (const st of (ctx.iterateNodesByKind?.('struct') ?? ctx.getNodesByKind('struct'))) {
if ((++scannedFiles & 255) === 0) await onYield();
if (!C_CPP_EXT.test(st.filePath)) continue;
const rawFields = rawFieldsByNode.get(st.id);
if (!rawFields) continue; // file unreadable or body unparsable at sweep time — the old pass skipped it too
registerStructLayout(st.name, classifyFields(rawFields));
for (const kind of ['struct', 'union'] as const) {
for (const st of (ctx.iterateNodesByKind?.(kind) ?? ctx.getNodesByKind(kind))) {
if ((++scannedFiles & 255) === 0) await onYield();
if (!C_CPP_EXT.test(st.filePath)) continue;
const rawFields = rawFieldsByNode.get(st.id);
if (!rawFields) continue; // file unreadable or body unparsable at sweep time — the old pass skipped it too
registerStructLayout(st.name, classifyFields(rawFields));
}
}
rawFieldsByNode.clear();
if (prof) { prof.B = Date.now() - tPass; tPass = Date.now(); }
@@ -1211,7 +1213,7 @@ export async function cFnPointerDispatchEdges(
const recvTypeIn = (fnSrc: string, recv: string): string | null => {
let re = recvReCache.get(recv);
if (!re) {
re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
re = new RegExp(`(?:(?:struct|union)\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
recvReCache.set(recv, re);
}
re.lastIndex = 0;
@@ -1230,7 +1232,7 @@ export async function cFnPointerDispatchEdges(
const varTypeIn = (fnSrc: string, v: string): string | null => {
let re = varReCache.get(v);
if (!re) {
re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
re = new RegExp(`(?:(?:struct|union)\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
varReCache.set(v, re);
}
re.lastIndex = 0;
+1 -1
View File
@@ -2192,7 +2192,7 @@ function findExportedSymbolWalk(
/** Node kinds that own static members reachable as `Container.member`. */
const STATIC_MEMBER_CONTAINERS = new Set<Node['kind']>([
'class', 'struct', 'interface', 'enum', 'trait', 'protocol',
'class', 'struct', 'union', 'interface', 'enum', 'trait', 'protocol',
]);
/**