feat(extraction): add Nix language support with module-system option wiring (#324, #332 via #648 — carries #1084) (#1190)

Carries @TyceHerrman's #1084 as the functional base. Extraction + file wiring (imports/modules lists, callPackage), module-system option-path synthesizer, lexical-scope resolution gates, ABI-15 wasm rebuilt from upstream source. Validated on agenix, nix-darwin, home-manager, and nixpkgs (44,368 files, 3m49s, 1.30M nodes).

Co-authored-by: Tyce Herrman <Tyce.Herrman@pm.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-06 12:41:32 -05:00
committed by GitHub
co-authored by Tyce Herrman Claude Fable 5
parent 99152212a9
commit 7f325134e0
17 changed files with 1262 additions and 7 deletions
+28 -3
View File
@@ -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, isCobolCopybookRef } from './import-resolver';
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef } from './import-resolver';
import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
import { createYielder, type MaybeYield } from './cooperative-yield';
@@ -747,11 +747,14 @@ export class ReferenceResolver {
// ArkTS chained-attribute refs carry a leading dot (`.titleStyle`) that
// routes them to the decorator-gated matcher; the symbol itself is
// indexed under the bare name, so the existence check strips the dot.
// Nix static path imports (`import ./x.nix`) name a FILE, not a symbol —
// they bypass the symbol-existence check and resolve via resolveViaImport.
const existenceName =
ref.language === 'arkts' && ref.referenceName.startsWith('.')
? ref.referenceName.slice(1)
: ref.referenceName;
if (
!isNixPathImportRef(ref) &&
!this.hasAnyPossibleMatch(existenceName) &&
!this.matchesAnyImport(ref) &&
!this.frameworks.some((f) => f.claimsReference?.(ref.referenceName))
@@ -826,7 +829,9 @@ export class ReferenceResolver {
// framework resolver IS the whole rulebook (`var.X` can never legally
// bind outside its module directory), so the name-matcher's
// qualified-name fallback would only ever add wrong cross-module edges.
if (isPhpIncludePathRef(ref) || isCobolCopybookRef(ref) || ref.language === 'terraform') {
// Nix static path imports are file references for the same reason —
// falling through would let "./x.nix" name-match an unrelated node.
if (isPhpIncludePathRef(ref) || isCobolCopybookRef(ref) || isNixPathImportRef(ref) || ref.language === 'terraform') {
return candidates.length > 0
? candidates.reduce((best, curr) =>
curr.confidence > best.confidence ? curr : best
@@ -835,7 +840,27 @@ export class ReferenceResolver {
}
// Strategy 3: Try name matching
const nameResult = this.gateLanguage(matchReference(ref, this.context), ref);
let nameResult = this.gateLanguage(matchReference(ref, this.context), ref);
// Nix has no ambient cross-file namespace — a callee binds lexically
// (same file) or through explicit import/callPackage wiring (the import
// path above). A cross-file name match is wrong by construction: every
// module `inherit (lib) mkOption`s the same nixpkgs helpers, so the
// matcher would link each `mkOption` call to whichever file's inherit
// binding it happened to pick. Same-file matches only.
if (nameResult) {
const target = this.queries.getNodeById(nameResult.targetNodeId);
if (ref.language === 'nix') {
if (!target || target.filePath !== ref.filePath) {
nameResult = null;
}
} else if (target && target.language === 'nix') {
// The reverse direction is just as impossible: no other language can
// symbolically call into a .nix binding (interop is eval/CLI, never a
// linkable symbol) — without this, a Python script's `split()` lands
// on some module's `split = ...` binding as a low-confidence match.
nameResult = null;
}
}
if (nameResult) {
candidates.push(nameResult);
}