diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 6e647a040..df2130c8e 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -756,4 +756,61 @@ describe('Sync Module', () => { expect(callerCount('callee_two')).toBe(1); }); }); + + describe('Import-aware value references survive target sync', () => { + let testDir: string; + let cg: CodeGraph; + + const readers = (): string[] => { + const target = cg.searchNodes('sharedProvider') + .map((result) => result.node) + .find((node) => node.name === 'sharedProvider'); + if (!target) return []; + return cg.getIncomingEdges(target.id) + .filter((edge) => edge.kind === 'references' && edge.metadata?.valueRef === true) + .map((edge) => cg.getNode(edge.source)?.name) + .filter((name): name is string => !!name); + }; + + beforeEach(async () => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-value-ref-sync-')); + fs.mkdirSync(path.join(testDir, 'lib', 'providers'), { recursive: true }); + fs.mkdirSync(path.join(testDir, 'lib', 'pages'), { recursive: true }); + fs.writeFileSync( + path.join(testDir, 'lib', 'providers', 'shared.dart'), + 'final sharedProvider = Object();\n' + ); + fs.writeFileSync( + path.join(testDir, 'lib', 'pages', 'consumer.dart'), + "import '../providers/shared.dart';\nvoid usesShared() { print(sharedProvider); }\n" + ); + cg = CodeGraph.initSync(testDir, { + config: { include: ['**/*.dart'], exclude: [] }, + }); + await cg.indexAll(); + }); + + afterEach(() => { + cg?.destroy(); + if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it('drops and restores the edge when the imported value disappears and returns', async () => { + expect(readers()).toContain('usesShared'); + + fs.writeFileSync( + path.join(testDir, 'lib', 'providers', 'shared.dart'), + 'final renamedProvider = Object();\n' + ); + await cg.sync(); + expect(readers()).toEqual([]); + + fs.writeFileSync( + path.join(testDir, 'lib', 'providers', 'shared.dart'), + 'final sharedProvider = Object();\n' + ); + await cg.sync(); + expect(readers()).toContain('usesShared'); + }); + }); }); diff --git a/__tests__/value-reference-edges.test.ts b/__tests__/value-reference-edges.test.ts index 485a00bd9..b179f50ab 100644 --- a/__tests__/value-reference-edges.test.ts +++ b/__tests__/value-reference-edges.test.ts @@ -181,6 +181,41 @@ describe('value-reference edges', () => { expect(valueRefReaders(cg, 'DefaultLabels')).toEqual(expect.arrayContaining(['labels'])); }); + it('edges Go readers to an imported package value without crossing a local shadow', async () => { + fs.mkdirSync(path.join(dir, 'internal', 'realtime'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'p2p'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/app\n\ngo 1.24\n'); + fs.writeFileSync( + path.join(dir, 'internal', 'realtime', 'store.go'), + [ + 'package realtime', + '', + 'type Store struct{}', + 'var DefaultSessionStore = &Store{}', + ].join('\n'), + ); + fs.writeFileSync( + path.join(dir, 'p2p', 'service.go'), + [ + 'package p2p', + '', + 'import "example.com/app/internal/realtime"', + '', + 'type holder struct { DefaultSessionStore *realtime.Store }', + 'func usesDefault() *realtime.Store { return realtime.DefaultSessionStore }', + 'func shadowed(realtime holder) *realtime.Store { return realtime.DefaultSessionStore }', + 'func shadowedLocal() *realtime.Store { realtime := holder{}; return realtime.DefaultSessionStore }', + ].join('\n'), + ); + cg = index(); + await cg.indexAll(); + + const readers = valueRefReaders(cg, 'DefaultSessionStore'); + expect(readers).toContain('usesDefault'); + expect(readers).not.toContain('shadowed'); + expect(readers).not.toContain('shadowedLocal'); + }); + it('does NOT edge a Go package const shadowed by a local := of the same name', async () => { // `Timeout` is a package const AND a local `:=` (short_var_declaration) in // shadows(). The local read resolves to the inner binding, so a file-scope @@ -628,6 +663,73 @@ describe('value-reference edges', () => { expect(valueRefReaders(cg, 'instanceField')).toEqual([]); }); + it('edges Dart readers to a uniquely imported top-level final without crossing a parameter shadow', async () => { + fs.mkdirSync(path.join(dir, 'lib', 'providers'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'lib', 'pages'), { recursive: true }); + fs.writeFileSync( + path.join(dir, 'lib', 'providers', 'presence.dart'), + 'final agentBridgePresenceProvider = Object();\n', + ); + fs.writeFileSync( + path.join(dir, 'lib', 'pages', 'settings.dart'), + [ + "import '../providers/presence.dart';", + '', + 'void usesProvider() { print(agentBridgePresenceProvider); }', + 'void shadowed(Object agentBridgePresenceProvider) { print(agentBridgePresenceProvider); }', + 'void shadowedLocal() { final agentBridgePresenceProvider = Object(); print(agentBridgePresenceProvider); }', + ].join('\n'), + ); + fs.writeFileSync( + path.join(dir, 'lib', 'pages', 'prefixed.dart'), + [ + "import '../providers/presence.dart' as presence;", + 'void usesPrefixed() { print(presence.agentBridgePresenceProvider); }', + ].join('\n'), + ); + cg = index(); + await cg.indexAll(); + + const readers = valueRefReaders(cg, 'agentBridgePresenceProvider'); + expect(readers).toContain('usesProvider'); + expect(readers).toContain('usesPrefixed'); + expect(readers).not.toContain('shadowed'); + expect(readers).not.toContain('shadowedLocal'); + }); + + it('resolves Dart value readers through this package URI but not another package', async () => { + fs.mkdirSync(path.join(dir, 'lib', 'providers'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'test'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'pubspec.yaml'), 'name: example_app\n'); + fs.writeFileSync( + path.join(dir, 'lib', 'providers', 'presence.dart'), + 'final agentBridgePresenceProvider = Object();\n', + ); + fs.writeFileSync( + path.join(dir, 'test', 'presence_test.dart'), + [ + "import 'package:example_app/providers/presence.dart';", + 'void readsOwnPackage() { print(agentBridgePresenceProvider); }', + ].join('\n'), + ); + fs.writeFileSync( + path.join(dir, 'test', 'external_test.dart'), + [ + '/*', + "import 'package:example_app/providers/presence.dart';", + '*/', + "import 'package:another_app/providers/presence.dart';", + 'void readsExternalPackage() { print(agentBridgePresenceProvider); }', + ].join('\n'), + ); + cg = index(); + await cg.indexAll(); + + const readers = valueRefReaders(cg, 'agentBridgePresenceProvider'); + expect(readers).toContain('readsOwnPackage'); + expect(readers).not.toContain('readsExternalPackage'); + }); + it('does NOT edge a Dart const shadowed by a method-local const of the same name', async () => { fs.writeFileSync( path.join(dir, 'shadow.dart'), diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index e6520cc0f..29876199f 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -746,7 +746,8 @@ export class TreeSitterExtractor { this.fileScopeValues = new Map(); this.fileScopeValueCounts = new Map(); if (!this.valueRefsEnabled || !TreeSitterExtractor.VALUE_REF_LANGS.has(this.language)) return; - if (targets.size === 0 || scopes.length === 0 || isGeneratedFile(this.filePath)) return; + const crossFileEnabled = this.language === 'go' || this.language === 'dart'; + if ((targets.size === 0 && !crossFileEnabled) || scopes.length === 0 || isGeneratedFile(this.filePath)) return; // Prune SHADOWED targets. A target re-bound in an INNER scope (a // bundled/Emscripten `const Module` re-declared as a nested `var Module`; a @@ -838,11 +839,16 @@ export class TreeSitterExtractor { } } for (const [nm, c] of declCounts) if (c > (fileScopeCounts.get(nm) ?? 1)) targets.delete(nm); - if (targets.size === 0) return; + if (targets.size === 0 && !crossFileEnabled) return; } for (const scope of scopes) { const seen = new Set(); + const localBindings = new Set([scope.name]); + const crossFileCandidates = new Map< + string, + { referenceName: string; shadowName: string; line: number; column: number } + >(); const stack: SyntaxNode[] = [scope.node]; // Dart and Pascal attach a function/method BODY as a *next sibling* of the // signature node that is stored as the reader scope (Dart `method_signature` @@ -858,6 +864,85 @@ export class TreeSitterExtractor { while (stack.length > 0 && visited < TreeSitterExtractor.MAX_VALUE_REF_NODES) { const n = stack.pop()!; visited++; + + if (this.language === 'go') { + // A package-qualified value read is precise only while the receiver + // still names the imported package. Any local declaration with the + // same name shadows that import for the reader scope, so remember it + // and conservatively drop the candidate after the walk. + if (n.type === 'parameter_declaration' || n.type === 'var_spec') { + for (const child of n.namedChildren) { + if (child.type === 'identifier') localBindings.add(getNodeText(child, this.source)); + } + } else if (n.type === 'short_var_declaration' || n.type === 'range_clause') { + const left = getChildByField(n, 'left') ?? n.namedChild(0); + if (left?.type === 'identifier') localBindings.add(getNodeText(left, this.source)); + else if (left) { + for (const child of left.namedChildren) { + if (child.type === 'identifier') localBindings.add(getNodeText(child, this.source)); + } + } + } else if (n.type === 'selector_expression') { + const operand = getChildByField(n, 'operand'); + const field = getChildByField(n, 'field'); + if (operand?.type === 'identifier' && field?.type === 'field_identifier') { + const receiver = getNodeText(operand, this.source); + const member = getNodeText(field, this.source); + // Exported package values start uppercase. Calls are extracted by + // the normal call path and must not be duplicated as value refs. + const isCallTarget = n.parent?.type === 'call_expression' && + getChildByField(n.parent, 'function')?.id === n.id; + if (/^[A-Z]/.test(member) && !isCallTarget) { + const referenceName = `${receiver}.${member}`; + crossFileCandidates.set(referenceName, { + referenceName, + shadowName: receiver, + line: field.startPosition.row + 1, + column: field.startPosition.column, + }); + } + } + } + } else if (this.language === 'dart') { + if ( + n.type === 'formal_parameter' || + n.type === 'initialized_identifier' || + n.type === 'initialized_variable_definition' + ) { + const id = getChildByField(n, 'name') ?? + n.namedChildren.find((child) => child.type === 'identifier'); + if (id) localBindings.add(getNodeText(id, this.source)); + } + if (n.type === 'identifier') { + let referenceName = getNodeText(n, this.source); + let shadowName = referenceName; + const accessor = n.parent; + const selector = accessor?.parent; + const receiver = selector?.previousNamedSibling; + if ( + (accessor?.type === 'unconditional_assignable_selector' || + accessor?.type === 'conditional_assignable_selector') && + selector?.type === 'selector' && + receiver?.type === 'identifier' + ) { + shadowName = getNodeText(receiver, this.source); + referenceName = `${shadowName}.${referenceName}`; + } + if ( + referenceName.length >= 3 && + /[A-Z_]/.test(referenceName) && + !targets.has(referenceName) + ) { + crossFileCandidates.set(referenceName, { + referenceName, + shadowName, + line: n.startPosition.row + 1, + column: n.startPosition.column, + }); + } + } + } + // `constant` covers Ruby, where both a constant's definition and its // references are `constant`-typed nodes, not `identifier`. `name` covers // PHP, where a constant reference — bare `MAX_ITEMS` or the const half of @@ -891,6 +976,17 @@ export class TreeSitterExtractor { if (c) stack.push(c); } } + + for (const candidate of crossFileCandidates.values()) { + if (localBindings.has(candidate.shadowName)) continue; + this.unresolvedReferences.push({ + fromNodeId: scope.id, + referenceName: candidate.referenceName, + referenceKind: 'value_ref', + line: candidate.line, + column: candidate.column, + }); + } } } diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index bbb1303b3..337d90750 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -69,6 +69,16 @@ export function resolveImportPath( return resolveCobolCopybook(importPath, fromFile, context); } + // Dart's own package URI is a precise project-local mapping: + // `package:/x.dart` -> `lib/x.dart`. Other packages and + // `dart:` SDK libraries remain external. + if (language === 'dart') { + if (importPath.startsWith('dart:')) return null; + if (importPath.startsWith('package:')) { + return resolveDartPackageImport(importPath, context); + } + } + // Skip external/npm packages — but pass the context so the // bare-specifier heuristic can consult the project's tsconfig // alias map first (custom prefixes like `@components/*` would @@ -99,6 +109,25 @@ export function resolveImportPath( return null; } +function resolveDartPackageImport( + importPath: string, + context: ResolutionContext +): string | null { + const match = /^package:([^/]+)\/(.+)$/.exec(importPath); + if (!match) return null; + + let packageName = context.getDartPackageName?.(); + if (packageName === undefined) { + const pubspec = context.readFile('pubspec.yaml'); + packageName = pubspec?.match(/^\s*name\s*:\s*['"]?([A-Za-z_][\w-]*)['"]?\s*(?:#.*)?$/m)?.[1] ?? null; + } + if (!packageName || match[1] !== packageName) return null; + + const candidate = path.posix.normalize(`lib/${match[2]}`); + if (!candidate.startsWith('lib/')) return null; + return context.fileExists(candidate) ? candidate : null; +} + /** * COBOL copybook lookup: `COPY CVACT01Y` (or `EXEC SQL INCLUDE X`) names a * library member resolved by the compiler's copybook search path, so we match @@ -681,6 +710,8 @@ export function extractImportMappings( mappings.push(...extractPythonImports(content)); } else if (language === 'go') { mappings.push(...extractGoImports(content)); + } else if (language === 'dart') { + mappings.push(...extractDartImports(content)); } else if (language === 'java' || language === 'kotlin') { mappings.push(...extractJavaImports(content)); } else if (language === 'php') { @@ -894,6 +925,36 @@ function extractGoImports(content: string): ImportMapping[] { return mappings; } +/** + * Extract local Dart library imports. An unprefixed import exposes its public + * top-level names directly, represented by `localName: '*'`; `as prefix` + * imports use that prefix as a namespace. Conditional and show/hide imports + * are skipped for now because resolving them without their visibility/platform + * rules would trade missing edges for incorrect ones. + */ +function extractDartImports(content: string): ImportMapping[] { + const mappings: ImportMapping[] = []; + const cleaned = stripJsComments(content); + const importRegex = /^\s*import\s+['"]([^'"]+)['"]([^;]*);/gm; + let match: RegExpExecArray | null; + + while ((match = importRegex.exec(cleaned)) !== null) { + const source = match[1]!; + const suffix = match[2] ?? ''; + if (/\b(?:show|hide)\b/.test(suffix) || /\bif\s*\(/.test(suffix)) continue; + const prefix = suffix.match(/\bas\s+([A-Za-z_]\w*)\b/)?.[1]; + mappings.push({ + localName: prefix ?? '*', + exportedName: '*', + source, + isDefault: false, + isNamespace: true, + }); + } + + return mappings; +} + /** * Extract Java / Kotlin import mappings. * @@ -1341,6 +1402,11 @@ export function resolveViaImport( if (goResult) return goResult; } + if (ref.language === 'dart' && ref.referenceKind === 'value_ref') { + const dartResult = resolveDartImportedValue(ref, imports, context); + if (dartResult) return dartResult; + } + // Java / Kotlin: imports are FQNs (`import com.example.Foo;`) — no // resolvable file path the JS/TS-style chain below could follow. Look // up the symbol by name and filter to the candidate whose file path @@ -1928,7 +1994,17 @@ function resolveGoCrossPackageReference( const candidates = context.getNodesByName(memberName); for (const node of candidates) { if (node.language !== 'go') continue; - if (!node.isExported) continue; + if (ref.referenceKind === 'value_ref') { + // Go exports package values by identifier case. The generic extractor + // does not currently stamp isExported on const/var nodes, so use the + // language rule directly and keep value refs restricted to top-level + // value nodes. + if (!/^[A-Z]/.test(memberName)) continue; + if (node.kind !== 'constant' && node.kind !== 'variable') continue; + if (node.qualifiedName !== node.name) continue; + } else if (!node.isExported) { + continue; + } const fp = node.filePath.replace(/\\/g, '/'); const lastSlash = fp.lastIndexOf('/'); const fileDir = lastSlash >= 0 ? fp.substring(0, lastSlash) : ''; @@ -1945,6 +2021,50 @@ function resolveGoCrossPackageReference( return null; } +/** + * Resolve a Dart imported top-level value through an explicit local library + * import. Multiple imported libraries defining the same unprefixed name are + * treated as ambiguous and dropped. Private and class-static values are never + * importable as bare library members, so they are excluded. + */ +function resolveDartImportedValue( + ref: UnresolvedRef, + imports: ImportMapping[], + context: ResolutionContext +): ResolvedRef | null { + const matches = new Map(); + + for (const imp of imports) { + let memberName: string | null = null; + if (imp.localName === '*') { + if (!ref.referenceName.includes('.')) memberName = ref.referenceName; + } else if (ref.referenceName.startsWith(imp.localName + '.')) { + memberName = ref.referenceName.slice(imp.localName.length + 1); + } + if (!memberName || memberName.startsWith('_')) continue; + + const resolvedPath = resolveImportPath(imp.source, ref.filePath, 'dart', context); + if (!resolvedPath) continue; + const target = context.getNodesInFile(resolvedPath).find( + (node) => + node.language === 'dart' && + node.name === memberName && + node.qualifiedName === node.name && + (node.kind === 'constant' || node.kind === 'variable') + ); + if (target) matches.set(target.id, target); + } + + if (matches.size !== 1) return null; + const target = matches.values().next().value as Node; + return { + original: ref, + targetNodeId: target.id, + confidence: 0.95, + resolvedBy: 'import', + }; +} + /** Recursive depth cap for re-export chain following. Real codebases * rarely chain barrels more than 2–3 deep; 8 is a generous safety * net that still bounds worst-case work. */ diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 9f94c7297..960c68b74 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -250,6 +250,9 @@ export class ReferenceResolver { private projectAliases: AliasMap | null | undefined = undefined; // go.mod module path. Same lazy/immutable convention as projectAliases. private goModule: GoModule | null | undefined = undefined; + // Root pubspec package name. Cleared between resolution/sync passes because + // pubspec.yaml can change while the resolver instance stays alive. + private dartPackageName: string | null | undefined = undefined; // Monorepo workspace member packages. Same lazy/immutable convention. private workspacePackages: WorkspacePackages | null | undefined = undefined; @@ -372,6 +375,7 @@ export class ReferenceResolver { this.knownNames = null; this.knownFiles = null; this.cachesWarmed = false; + this.dartPackageName = undefined; } /** `readFile` through the LRU content cache (null = read failed, also cached). */ @@ -568,6 +572,16 @@ export class ReferenceResolver { return this.goModule; }, + getDartPackageName: () => { + if (this.dartPackageName === undefined) { + const pubspec = this.readFileCached('pubspec.yaml'); + this.dartPackageName = pubspec?.match( + /^\s*name\s*:\s*['"]?([A-Za-z_][\w-]*)['"]?\s*(?:#.*)?$/m + )?.[1] ?? null; + } + return this.dartPackageName; + }, + getWorkspacePackages: () => { if (this.workspacePackages === undefined) { this.workspacePackages = loadWorkspacePackages(this.projectRoot); @@ -809,6 +823,18 @@ export class ReferenceResolver { return null; } + // Imported value refs get a dedicated, strictly-gated path. They resolve + // only through an explicit language import/package mapping and may target + // only top-level constants/variables — never fuzzy name matching. + if (ref.referenceKind === 'value_ref') { + const viaImport = this.gateLanguage(resolveViaImport(ref, this.context), ref); + if (!viaImport) return null; + const target = this.queries.getNodeById(viaImport.targetNodeId); + return target && (target.kind === 'constant' || target.kind === 'variable') + ? viaImport + : null; + } + // Function-as-value refs (#756) get a dedicated, strictly-gated path: // import-based resolution first (an imported callback resolves through its // import, the most precise cross-file signal), then matchFunctionRef @@ -946,13 +972,12 @@ export class ReferenceResolver { */ createEdges(resolved: ResolvedRef[]): Edge[] { return resolved.map((ref) => { - // `function_ref` (#756) is internal-only: it persists as a `references` - // edge (the registration site depends on the callback), distinguishable - // by metadata.resolvedBy === 'function-ref'. callers/impact already - // traverse `references`, so registration sites surface with no - // graph-layer changes. + // Internal reference kinds persist as `references` edges. callers/impact + // already traverse `references`, so no graph-layer changes are needed. let kind: Edge['kind'] = - ref.original.referenceKind === 'function_ref' ? 'references' : ref.original.referenceKind; + ref.original.referenceKind === 'function_ref' || ref.original.referenceKind === 'value_ref' + ? 'references' + : ref.original.referenceKind; // Promote "extends" to "implements" when a class/struct targets an interface if (kind === 'extends') { @@ -988,7 +1013,7 @@ export class ReferenceResolver { resolvedBy: ref.resolvedBy, // The ORIGINAL reference text (and kind, when edge-kind promotion // rewrote it — calls→instantiates, extends→implements, - // function_ref→references). If this edge's target is later removed + // function_ref/value_ref→references). If this edge's target is later removed // by a re-index, the edge is resurrected as exactly this ref and // re-resolved (#1240 removal case) — a faithful resurrection, so // re-resolution can never bind anywhere a full re-index wouldn't. @@ -1003,6 +1028,7 @@ export class ReferenceResolver { // tooling label "callback registration" and lets validation diff // exactly the edges this feature added. ...(ref.original.referenceKind === 'function_ref' ? { fnRef: true } : {}), + ...(ref.original.referenceKind === 'value_ref' ? { valueRef: true } : {}), }, }; }); @@ -1794,7 +1820,10 @@ export class ReferenceResolver { if (!result) return result; const tgt = this.getLanguageFromNodeId(result.targetNodeId); if (!tgt || !ref.language) return result; - if ((ref.referenceKind === 'references' || ref.referenceKind === 'function_ref') && !sameLanguageFamily(tgt, ref.language)) return null; + if ( + (ref.referenceKind === 'references' || ref.referenceKind === 'function_ref' || ref.referenceKind === 'value_ref') && + !sameLanguageFamily(tgt, ref.language) + ) return null; if (ref.referenceKind === 'imports' && crossesKnownFamily(tgt, ref.language)) return null; return result; } diff --git a/src/resolution/types.ts b/src/resolution/types.ts index c8539168b..99bebd95b 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -145,6 +145,12 @@ export interface ResolutionContext { * cross-package imports from third-party packages. */ getGoModule?(): import('./go-module').GoModule | null; + /** + * Package name declared by the root `pubspec.yaml`, or `null` outside a Dart + * package. Used to distinguish this project's `package:/...` imports + * from third-party packages. + */ + getDartPackageName?(): string | null; /** * Monorepo workspace member packages, keyed by declared package name. * Returns `null` for single-package repos (no `workspaces` field). diff --git a/src/types.ts b/src/types.ts index d160d044e..bba9dd151 100644 --- a/src/types.ts +++ b/src/types.ts @@ -291,12 +291,12 @@ export interface ExtractionError { } /** - * Kinds an unresolved reference can carry. `function_ref` is internal-only — - * a function name used as a VALUE (callback registration, #756). It never - * becomes an edge kind: resolution maps it to a `references` edge targeting - * function/method nodes only (see `matchFunctionRef`). + * Kinds an unresolved reference can carry. Internal-only kinds never become + * edge kinds: `function_ref` maps callback values to function/method nodes; + * `value_ref` maps import-qualified value reads to constant/variable nodes. + * Both persist as `references` edges with a feature marker in metadata. */ -export type ReferenceKind = EdgeKind | 'function_ref'; +export type ReferenceKind = EdgeKind | 'function_ref' | 'value_ref'; /** * A reference that couldn't be resolved during extraction