diff --git a/README.md b/README.md index 49cf8d54..ce1617e5 100644 --- a/README.md +++ b/README.md @@ -457,6 +457,7 @@ The `.codegraph/config.json` file controls indexing: | Vue | `.vue` | Full support (script + script-setup extraction, Nuxt page/API/middleware routes) | | Liquid | `.liquid` | Full support | | Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) | +| Nix | `.nix` | Full support | ## Troubleshooting diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index b08408a4..fe7cfcb9 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -94,6 +94,10 @@ describe('Language Detection', () => { expect(detectLanguage('main.dart')).toBe('dart'); }); + it('should detect Nix files', () => { + expect(detectLanguage('default.nix')).toBe('nix'); + }); + it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); @@ -105,6 +109,7 @@ describe('Language Support', () => { expect(isLanguageSupported('typescript')).toBe(true); expect(isLanguageSupported('python')).toBe(true); expect(isLanguageSupported('go')).toBe(true); + expect(isLanguageSupported('nix')).toBe(true); expect(isLanguageSupported('unknown')).toBe(false); }); @@ -122,6 +127,28 @@ describe('Language Support', () => { expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); + expect(languages).toContain('nix'); + }); +}); + +describe('Nix Extraction', () => { + it('should extract Nix bindings and imports', () => { + const code = `{ + hello = "world"; + package = import ./default.nix; + }`; + + const result = extractFromSource('default.nix', code); + const hello = result.nodes.find((n) => n.kind === 'variable' && n.name === 'hello'); + const packageNode = result.nodes.find((n) => n.kind === 'variable' && n.name === 'package'); + const importNode = result.nodes.find((n) => n.kind === 'import'); + + expect(hello).toBeDefined(); + expect(packageNode).toBeDefined(); + expect(importNode).toMatchObject({ + kind: 'import', + name: './default.nix', + }); }); }); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index d1540424..039027be 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -33,6 +33,7 @@ const WASM_GRAMMAR_FILES: Record = { swift: 'tree-sitter-swift.wasm', kotlin: 'tree-sitter-kotlin.wasm', dart: 'tree-sitter-dart.wasm', + nix: 'tree-sitter-nix.wasm', pascal: 'tree-sitter-pascal.wasm', scala: 'tree-sitter-scala.wasm', }; @@ -67,6 +68,7 @@ export const EXTENSION_MAP: Record = { '.kt': 'kotlin', '.kts': 'kotlin', '.dart': 'dart', + '.nix': 'nix', '.liquid': 'liquid', '.svelte': 'svelte', '.vue': 'vue', @@ -125,8 +127,8 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise> = { typescript: typescriptExtractor, @@ -41,6 +42,7 @@ export const EXTRACTORS: Partial> = { swift: swiftExtractor, kotlin: kotlinExtractor, dart: dartExtractor, + nix: nixExtractor, pascal: pascalExtractor, scala: scalaExtractor, }; diff --git a/src/extraction/languages/nix.ts b/src/extraction/languages/nix.ts new file mode 100644 index 00000000..89e7c23f --- /dev/null +++ b/src/extraction/languages/nix.ts @@ -0,0 +1,51 @@ +import { getNodeText, getChildByField } from '../tree-sitter-helpers'; +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { LanguageExtractor, ImportInfo } from '../tree-sitter-types'; + +export const nixExtractor: LanguageExtractor = { + functionTypes: [], + classTypes: [], + methodTypes: [], + interfaceTypes: [], + structTypes: [], + enumTypes: [], + typeAliasTypes: [], + importTypes: ['apply_expression'], + callTypes: [], + variableTypes: ['binding'], + nameField: 'name', + bodyField: 'body', + paramsField: 'arguments', + + visitNode: (node: SyntaxNode, ctx) => { + if (node.type !== 'binding') return false; + + const attrpath = getChildByField(node, 'attrpath'); + const name = attrpath ? getNodeText(attrpath, ctx.source) : getNodeText(node, ctx.source); + const valueNode = getChildByField(node, 'expression'); + const signature = valueNode ? getNodeText(valueNode, ctx.source) : undefined; + + ctx.createNode('variable', name, node, { signature }); + + if (valueNode) { + ctx.visitNode(valueNode); + } + + return true; + }, + + extractImport: (node: SyntaxNode, source: string) => { + if (node.type !== 'apply_expression') return null; + const functionNode = getChildByField(node, 'function'); + if (!functionNode || getNodeText(functionNode, source) !== 'import') return null; + + const argument = getChildByField(node, 'argument'); + if (!argument) return null; + + const moduleName = getNodeText(argument, source).replace(/^['"]|['"]$/g, ''); + return { + moduleName, + signature: getNodeText(node, source).trim(), + } as ImportInfo; + }, +}; diff --git a/src/extraction/wasm/tree-sitter-nix.wasm b/src/extraction/wasm/tree-sitter-nix.wasm new file mode 100755 index 00000000..fb541ab6 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-nix.wasm differ diff --git a/src/types.ts b/src/types.ts index 328f7432..2b68c1c1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,11 +80,13 @@ export const LANGUAGES = [ 'swift', 'kotlin', 'dart', + 'nix', 'svelte', 'vue', 'liquid', 'pascal', 'scala', + 'nix', 'unknown', ] as const; @@ -529,6 +531,8 @@ export const DEFAULT_CONFIG: CodeGraphConfig = { '**/*.kts', // Dart '**/*.dart', + // Nix + '**/*.nix', // Svelte '**/*.svelte', // Vue