From dbfe117771f335a360ca703c79c061ac45b37b8e Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Tue, 30 Jun 2026 16:44:44 -0400 Subject: [PATCH] Add Nix language support --- README.md | 3 +- __tests__/extraction.test.ts | 105 +++++++++ __tests__/resolution.test.ts | 82 +++++++ src/extraction/grammars.ts | 5 +- src/extraction/languages/index.ts | 2 + src/extraction/languages/nix.ts | 259 +++++++++++++++++++++++ src/extraction/wasm/tree-sitter-nix.wasm | Bin 0 -> 81459 bytes src/resolution/import-resolver.ts | 34 +++ src/resolution/index.ts | 12 +- src/types.ts | 1 + 10 files changed, 500 insertions(+), 3 deletions(-) create mode 100644 src/extraction/languages/nix.ts create mode 100755 src/extraction/wasm/tree-sitter-nix.wasm diff --git a/README.md b/README.md index 999f352ba..e0056194b 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ The reliable, universal payoff is **surgical context and speed**: CodeGraph coll | **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 | | **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes | | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config | -| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Svelte, Vue, Astro, Liquid, Pascal/Delphi | +| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Svelte, Vue, Astro, Liquid, Pascal/Delphi | | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks | | **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules | | **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only | @@ -714,6 +714,7 @@ is written): | Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) | | R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) | | Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) | +| Nix | `.nix` | Full support | ## Measured cross-file coverage diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 2f37d25ab..a4127150f 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -102,6 +102,12 @@ describe('Language Detection', () => { expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c'); }); + it('should detect Nix files', () => { + expect(detectLanguage('default.nix')).toBe('nix'); + expect(detectLanguage('pkgs/development/tools/misc/codegraph/default.nix')).toBe('nix'); + expect(isSourceFile('default.nix')).toBe(true); + }); + it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); @@ -130,6 +136,105 @@ describe('Language Support', () => { expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); + expect(languages).toContain('nix'); + }); +}); + +describe('Nix Extraction', () => { + it('should distinguish Nix variable and function bindings', () => { + const code = ` +let + plainValue = 10; + simpleFn = arg: arg + 1; + destructuredFn = { lib, stdenv }: lib.getName stdenv; + curriedFn = a: b: builtins.toString (a + b); +in +{ + exportedValue = plainValue; + exportedFn = curriedFn; +} +`; + + const result = extractFromSource('default.nix', code); + + expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'plainValue')).toBeDefined(); + expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'exportedValue')).toBeDefined(); + + const simpleFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'simpleFn'); + const destructuredFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'destructuredFn'); + const curriedFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'curriedFn'); + + expect(simpleFn?.signature).toBe('(arg)'); + expect(destructuredFn?.signature).toBe('{ lib, stdenv }'); + expect(curriedFn?.signature).toBe('a : b'); + + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls).toContain('lib.getName'); + expect(calls.filter((name) => name === 'builtins.toString')).toHaveLength(1); + }); + + it('should extract inherited Nix attributes as variables', () => { + const code = ` +let + inherit lib; + inherit (pkgs) stdenv writeShellScriptBin; +in +stdenv.mkDerivation {} +`; + + const result = extractFromSource('default.nix', code); + const variables = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); + + expect(variables).toContain('lib'); + expect(variables).toContain('stdenv'); + expect(variables).toContain('writeShellScriptBin'); + }); + + it('should emit only static project path imports for Nix import calls', () => { + const code = ` +let + local = import ./x.nix; + defaultFile = builtins.import ./dir; + packageSet = import {}; + fromSources = import sources.nixpkgs {}; + dynamic = import selectedPath; +in +local +`; + + const result = extractFromSource('default.nix', code); + const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName); + + expect(imports).toEqual(['./x.nix', './dir']); + expect(importRefs).toEqual(['./x.nix', './dir']); + }); + + it('should mark returned top-level Nix attrset members exported and keep let or nested attrs private', () => { + const code = ` +{ lib, stdenv }: +let + localValue = 10; +in +{ + exported = localValue; + package = { name }: stdenv.mkDerivation { inherit name; }; + nested = { + privateNested = true; + }; + inherit (lib) licenses; +} +`; + + const result = extractFromSource('default.nix', code); + const node = (name: string) => result.nodes.find((n) => n.name === name); + + expect(node('localValue')?.isExported).toBe(false); + expect(node('exported')?.isExported).toBe(true); + expect(node('package')?.kind).toBe('function'); + expect(node('package')?.isExported).toBe(true); + expect(node('privateNested')?.isExported).toBe(false); + expect(node('licenses')?.isExported).toBe(true); }); }); diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index a6d455499..dff4a1f9a 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -3714,4 +3714,86 @@ procedure Helper; var t: TTgt; begin t.Hit; end; expect(callerNamesOf('TTgt::Hit')).toEqual(['DoStuff', 'Helper']); }); }); + + describe('Nix path import resolution', () => { + function fileNode(filePath: string) { + return cg.getNodesByKind('file').find((n) => n.filePath === filePath); + } + + function importedFilePaths(fromFile: string): string[] { + const source = fileNode(fromFile); + expect(source, `${fromFile} file node`).toBeDefined(); + return cg + .getOutgoingEdges(source!.id) + .filter((edge) => edge.kind === 'imports') + .map((edge) => cg.getNodesByKind('file').find((n) => n.id === edge.target)?.filePath) + .filter((filePath): filePath is string => Boolean(filePath)) + .sort(); + } + + it('resolves relative Nix imports to indexed file nodes', async () => { + fs.mkdirSync(path.join(tempDir, 'core'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, 'data'), { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'core', 'ports.nix'), '{ http = 80; https = 443; }'); + fs.writeFileSync( + path.join(tempDir, 'data', 'postgresql.nix'), + `let + ports = import ../core/ports.nix; +in +{ + port = ports.https; +} +` + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + + expect(importedFilePaths('data/postgresql.nix')).toEqual(['core/ports.nix']); + }); + + it('resolves Nix directory imports through default.nix and deduplicates called imports', async () => { + fs.mkdirSync(path.join(tempDir, 'dir'), { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'dir', 'default.nix'), '{ value = 1; }'); + fs.writeFileSync(path.join(tempDir, 'x.nix'), '{ value = 2; }'); + fs.writeFileSync( + path.join(tempDir, 'main.nix'), + `let + dir = import ./dir; + x = import ./x.nix {}; +in +{ + inherit dir x; +} +` + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + + expect(importedFilePaths('main.nix')).toEqual(['dir/default.nix', 'x.nix']); + }); + + it('does not resolve Nix angle-bracket, attribute, or variable imports as project file edges', async () => { + fs.writeFileSync(path.join(tempDir, 'nixpkgs.nix'), '{ bogus = true; }'); + fs.writeFileSync(path.join(tempDir, 'selectedPath.nix'), '{ bogus = true; }'); + fs.writeFileSync( + path.join(tempDir, 'main.nix'), + `let + pkgs = import {}; + fromSources = import sources.nixpkgs {}; + dynamic = import selectedPath; +in +{ + inherit pkgs fromSources dynamic; +} +` + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + + expect(importedFilePaths('main.nix')).toEqual([]); + }); + }); }); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 1b15996c0..932ddd1f6 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -39,6 +39,7 @@ const WASM_GRAMMAR_FILES: Record = { r: 'tree-sitter-r.wasm', luau: 'tree-sitter-luau.wasm', objc: 'tree-sitter-objc.wasm', + nix: 'tree-sitter-nix.wasm', }; /** @@ -108,6 +109,7 @@ export const EXTENSION_MAP: Record = { '.luau': 'luau', '.m': 'objc', '.mm': 'objc', + '.nix': 'nix', // XML: file-level tracking; the MyBatis extractor matches `` // shape and emits SQL-statement nodes (other XML returns empty). '.xml': 'xml', @@ -221,7 +223,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise> = { typescript: typescriptExtractor, @@ -51,4 +52,5 @@ export const EXTRACTORS: Partial> = { r: rExtractor, luau: luauExtractor, objc: objcExtractor, + nix: nixExtractor, }; diff --git a/src/extraction/languages/nix.ts b/src/extraction/languages/nix.ts new file mode 100644 index 000000000..8cec800cb --- /dev/null +++ b/src/extraction/languages/nix.ts @@ -0,0 +1,259 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText } from '../tree-sitter-helpers'; +import type { LanguageExtractor } from '../tree-sitter-types'; + +function unwrapVariableExpression(node: SyntaxNode): SyntaxNode { + if (node.type !== 'variable_expression') return node; + return node.namedChild(0) ?? node; +} + +function getCalleeName(node: SyntaxNode, source: string): string | null { + let current = node; + while (current.type === 'apply_expression') { + const funcNode = current.childForFieldName('function') || current.namedChild(0); + if (!funcNode) break; + current = funcNode; + } + current = unwrapVariableExpression(current); + if (current.type === 'identifier' || current.type === 'select_expression') { + return getNodeText(current, source).trim(); + } + return null; +} + +function getDirectCalleeName(node: SyntaxNode, source: string): string | null { + let funcNode = node.childForFieldName('function') || node.namedChild(0); + if (!funcNode) return null; + funcNode = unwrapVariableExpression(funcNode); + return getNodeText(funcNode, source).trim(); +} + +function isStaticProjectPath(value: string): boolean { + return ( + (value.startsWith('./') || value.startsWith('../')) && + !/[\s{}()[\];"'<>$]/.test(value) + ); +} + +function getStaticImportPath(argNode: SyntaxNode, source: string): string | null { + let current = argNode; + while (current.type === 'parenthesized_expression') { + const inner = current.namedChild(0); + if (!inner) break; + current = inner; + } + + let text = getNodeText(current, source).trim(); + if ( + ((text.startsWith('"') && text.endsWith('"')) || + (text.startsWith("'") && text.endsWith("'"))) && + text.length >= 2 + ) { + text = text.slice(1, -1); + } + + return isStaticProjectPath(text) ? text : null; +} + +function isReturnedAttrsetMember(node: SyntaxNode): boolean { + let current: SyntaxNode | null = node; + let seenReturnedAttrset = false; + + while (current) { + const parent: SyntaxNode | null = current.parent; + if (!parent) break; + + if (parent.type === 'let_expression') { + const bodyNode = parent.childForFieldName('body') || parent.childForFieldName('expression'); + if (!bodyNode || !bodyNode.equals(current)) return false; + } + + if (parent.type === 'binding' && !current.equals(node)) return false; + if (parent.type === 'formal_parameters' || parent.type === 'formals') return false; + + if ( + parent.type === 'attrset' || + parent.type === 'rec_attrset' || + parent.type === 'attrset_expression' || + parent.type === 'rec_attrset_expression' + ) { + seenReturnedAttrset = true; + } + + current = parent; + } + + return seenReturnedAttrset; +} + +function getCurriedParamsAndBody(node: SyntaxNode, source: string): { params: string[]; bodyNode: SyntaxNode | null } { + const params: string[] = []; + let current = node; + + while (current.type === 'function_expression' && current.namedChildCount > 0) { + const bodyNode = current.namedChild(current.namedChildCount - 1); + if (!bodyNode) break; + + const paramPart = source.substring(current.startIndex, bodyNode.startIndex).trim(); + const paramText = paramPart.endsWith(':') ? paramPart.slice(0, -1).trim() : paramPart; + if (paramText) params.push(paramText); + + if (bodyNode.type === 'function_expression') { + current = bodyNode; + } else { + return { params, bodyNode }; + } + } + + return { + params, + bodyNode: current.namedChildCount > 0 ? current.namedChild(current.namedChildCount - 1) : null, + }; +} + +function formatFunctionSignature(params: string[]): string { + if (params.length === 0) return '()'; + if (params.length > 1) return params.join(' : '); + + const [param] = params; + if (!param) return '()'; + return param.startsWith('(') || param.includes('{') || param.includes('@') ? param : `(${param})`; +} + +function inheritedAttrs(node: SyntaxNode): SyntaxNode | null { + return node.namedChildren.find((child) => child.type === 'inherited_attrs') ?? null; +} + +export const nixExtractor: LanguageExtractor = { + functionTypes: [], + classTypes: [], + methodTypes: [], + interfaceTypes: [], + structTypes: [], + enumTypes: [], + typeAliasTypes: [], + importTypes: [], + callTypes: [], + variableTypes: [], + nameField: '', + bodyField: '', + paramsField: '', + + visitNode: (node, ctx) => { + const { source } = ctx; + + if (node.type === 'binding') { + const attrpath = node.childForFieldName('attrpath') || node.namedChild(0); + if (!attrpath) return false; + + const name = getNodeText(attrpath, source).trim(); + if (!name) return false; + + const valueNode = node.childForFieldName('expression') || node.childForFieldName('value') || node.namedChild(1); + if (!valueNode) return false; + + if (valueNode.type === 'function_expression') { + const { params, bodyNode } = getCurriedParamsAndBody(valueNode, source); + const funcNode = ctx.createNode('function', name, node, { + signature: formatFunctionSignature(params), + isExported: isReturnedAttrsetMember(node), + }); + + if (funcNode) { + ctx.pushScope(funcNode.id); + if (bodyNode) ctx.visitNode(bodyNode); + ctx.popScope(); + } + } else { + const initValue = getNodeText(valueNode, source).slice(0, 100); + ctx.createNode('variable', name, node, { + signature: initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined, + isExported: isReturnedAttrsetMember(node), + }); + ctx.visitNode(valueNode); + } + + return true; + } + + if (node.type === 'function_expression') { + const bodyNode = node.namedChild(node.namedChildCount - 1); + if (bodyNode) ctx.visitNode(bodyNode); + return true; + } + + if (node.type === 'inherit' || node.type === 'inherit_from') { + const attrs = inheritedAttrs(node); + if (attrs) { + for (const child of attrs.namedChildren) { + const name = getNodeText(child, source).trim(); + if (name) { + ctx.createNode('variable', name, child, { + isExported: isReturnedAttrsetMember(child), + }); + } + } + } + + for (const child of node.namedChildren) { + if (child.type !== 'inherited_attrs') ctx.visitNode(child); + } + return true; + } + + if (node.type === 'apply_expression') { + const directCallee = getDirectCalleeName(node, source); + const isDirectImport = directCallee === 'import' || directCallee === 'builtins.import'; + const isCalleeOfParent = + node.parent?.type === 'apply_expression' && + (node.parent.childForFieldName('function') === node || node.parent.namedChild(0) === node); + + if (!(isCalleeOfParent && !isDirectImport)) { + if (isDirectImport) { + const argNode = node.childForFieldName('argument') || node.namedChild(1); + const importPath = argNode ? getStaticImportPath(argNode, source) : null; + + if (importPath) { + const impNode = ctx.createNode('import', importPath, node, { + signature: getNodeText(node, source).trim().slice(0, 100), + }); + + if (impNode && ctx.nodeStack.length > 0) { + const fromNodeId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (fromNodeId) { + ctx.addUnresolvedReference({ + fromNodeId, + referenceName: importPath, + referenceKind: 'imports', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + } + } + } else { + const calleeName = getCalleeName(node, source); + if (calleeName && calleeName !== 'import' && calleeName !== 'builtins.import' && ctx.nodeStack.length > 0) { + const fromNodeId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (fromNodeId) { + ctx.addUnresolvedReference({ + fromNodeId, + referenceName: calleeName, + referenceKind: 'calls', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + } + } + } + + for (const child of node.namedChildren) { + ctx.visitNode(child); + } + return true; + } + + return false; + }, +}; diff --git a/src/extraction/wasm/tree-sitter-nix.wasm b/src/extraction/wasm/tree-sitter-nix.wasm new file mode 100755 index 0000000000000000000000000000000000000000..fb541ab65c48a66cc999da7bb19789ee0eae0274 GIT binary patch literal 81459 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�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ⅇ8?(Pz%_vX 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 F{|Ar8$fW=P literal 0 HcmV?d00001 diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index badbe4b02..454eae504 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -35,8 +35,18 @@ const EXTENSION_RESOLUTION: Record = { php: ['.php'], ruby: ['.rb'], objc: ['.h', '.m', '.mm'], + nix: ['.nix', '/default.nix'], }; +export function isNixPathImportRef(ref: UnresolvedRef): boolean { + return ( + ref.language === 'nix' && + ref.referenceKind === 'imports' && + (ref.referenceName.startsWith('./') || ref.referenceName.startsWith('../')) && + !/[\s{}()[\];"'<>$]/.test(ref.referenceName) + ); +} + /** * Resolve an import path to an actual file */ @@ -1195,6 +1205,30 @@ export function resolveViaImport( return null; } + // Nix static project-path imports (`import ./x.nix`, `builtins.import ./dir`, + // `import ./x.nix {}`) resolve to file nodes only. Do not resolve + // angle-bracket channels, attribute expressions, variables, or other dynamic + // expressions as project files. + if (isNixPathImportRef(ref)) { + const resolvedPath = resolveImportPath(ref.referenceName, ref.filePath, ref.language, context); + if (!resolvedPath) return null; + + const basename = resolvedPath.split('/').pop()!; + const fileNode = context + .getNodesByName(basename) + .find((n) => n.kind === 'file' && n.filePath === resolvedPath); + + if (fileNode) { + return { + original: ref, + targetNodeId: fileNode.id, + confidence: 0.9, + resolvedBy: 'import', + }; + } + return null; + } + // Use cached import mappings (avoids re-reading and re-parsing per ref) const imports = context.getImportMappings(ref.filePath, ref.language); if (imports.length === 0 && !context.readFile(ref.filePath)) { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 0d7ec4309..ca84204bd 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -17,7 +17,7 @@ import { ImportMapping, } from './types'; import { matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, sameLanguageFamily, crossesKnownFamily } from './name-matcher'; -import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef } from './import-resolver'; +import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isNixPathImportRef } from './import-resolver'; import { detectFrameworks } from './frameworks'; import { synthesizeCallbackEdges } from './callback-synthesizer'; import { loadProjectAliases, type AliasMap } from './path-aliases'; @@ -664,6 +664,15 @@ export class ReferenceResolver { return null; } + const isPathImportRef = + ref.referenceKind === 'imports' && + ( + ref.language === 'c' || + ref.language === 'cpp' || + isPhpIncludePathRef(ref) || + isNixPathImportRef(ref) + ); + // Fast pre-filter: skip if no symbol with this name exists anywhere // AND the name doesn't match a local import. The import escape is // necessary because re-export rename chains (`import { login } @@ -671,6 +680,7 @@ export class ReferenceResolver { // from './auth'`) intentionally call a name that has no // declaration anywhere — only the renamed upstream symbol does. if ( + !isPathImportRef && !this.hasAnyPossibleMatch(ref.referenceName) && !this.matchesAnyImport(ref) && !this.frameworks.some((f) => f.claimsReference?.(ref.referenceName)) diff --git a/src/types.ts b/src/types.ts index a3122bf9a..904a6ee7c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -91,6 +91,7 @@ export const LANGUAGES = [ 'luau', 'objc', 'r', + 'nix', 'yaml', 'twig', 'xml',