package resolver import ( "strings" "github.com/zzet/gortex/internal/graph" ) // resolveRazorUsings binds Razor / Blazor simple-type references — component // tags (``), `@model` / `@inherits` / `@inject` / `@typeof` targets — // that are reachable only through an imported namespace. // // The Razor extractor lowers each `@using Some.Namespace` directive to a // per-file `unresolved::razor_using::` marker import edge. This pass // computes every `.razor` / `.cshtml` file's effective namespace set — its own // `@using` directives unioned with the cascade of `_Imports.razor` files from // its directory up to the root — then, for each still-unresolved simple-type // reference, binds it to the unique indexed type (or Razor component) whose // namespace is in that set, refusing on ambiguity. The marker edges are // scaffolding and are removed once consumed. // // Runs serially in ResolveAll's relative-import settle window, gated on the // presence of Razor in the graph. func (r *Resolver) resolveRazorUsings() { if !r.graphHasLanguage("razor") { return } // Collect each file's @using namespaces from the marker edges. usingsByFile := map[string][]string{} var markerEdges []*graph.Edge for e := range r.graph.EdgesByKind(graph.EdgeImports) { if e == nil || !strings.HasPrefix(e.To, "unresolved::razor_using::") { continue } // Scoped warm pass: only the changed repos carry fresh @using markers; an // unchanged repo's were consumed (and removed) by a prior full pass. if !r.edgeFromInScope(e.From) { continue } if ns := strings.TrimPrefix(e.To, "unresolved::razor_using::"); ns != "" { usingsByFile[e.From] = append(usingsByFile[e.From], ns) } markerEdges = append(markerEdges, e) } if len(markerEdges) == 0 { return } // The _Imports.razor cascade sources, keyed by their directory. importDirs := map[string][]string{} for fileID, nss := range usingsByFile { if razorBaseName(fileID) == "_Imports.razor" { d := razorDir(fileID) importDirs[d] = append(importDirs[d], nss...) } } // effectiveNS = a file's own @using set ∪ every _Imports.razor in an // ancestor (or the same) directory. effectiveNS := func(fileID string) map[string]struct{} { set := map[string]struct{}{} for _, ns := range usingsByFile[fileID] { set[ns] = struct{}{} } fileDir := razorDir(fileID) for impDir, nss := range importDirs { if impDir == fileDir || impDir == "" || strings.HasPrefix(fileDir, impDir+"/") { for _, ns := range nss { set[ns] = struct{}{} } } } return set } // Type index: simple name → candidate (id, namespace, language). Razor // components and C# classes/structs/records/enums are KindType; interfaces // KindInterface. type typeCand struct{ id, ns, lang string } typesByName := map[string][]typeCand{} for _, kind := range []graph.NodeKind{graph.KindType, graph.KindInterface} { for n := range r.graph.NodesByKind(kind) { if n == nil || n.Name == "" { continue } ns, _ := n.Meta["scope_ns"].(string) typesByName[n.Name] = append(typesByName[n.Name], typeCand{id: n.ID, ns: ns, lang: n.Language}) } } fileLang := r.collectFileLanguages() var reindexBatch []graph.EdgeReindex for e := range r.graph.EdgesByKind(graph.EdgeReferences) { if e == nil || !strings.HasPrefix(e.To, "unresolved::") { continue } if !r.edgeFromInScope(e.From) { continue } name := strings.TrimPrefix(e.To, "unresolved::") if name == "" { continue } fileID := e.From if i := strings.Index(fileID, "::"); i >= 0 { fileID = fileID[:i] } refLang := fileLang[fileID] if refLang != "razor" { continue } cands := typesByName[name] if len(cands) == 0 { continue } eff := effectiveNS(fileID) if len(eff) == 0 { continue } match, ambiguous := "", false for _, c := range cands { if c.ns == "" { continue } // Only bind within the same language family — a Razor reference may // bind a C# type (both dotnet) but never a coincidentally-named // TypeScript component. if !sameLanguageFamily(refLang, c.lang) { continue } if _, ok := eff[c.ns]; !ok { continue } if match != "" && match != c.id { ambiguous = true break } match = c.id } if ambiguous || match == "" { continue } oldTo := e.To e.To = match e.Origin = graph.OriginASTResolved if e.Meta == nil { e.Meta = map[string]any{} } e.Meta["resolved_via"] = "razor_using" reindexBatch = append(reindexBatch, graph.EdgeReindex{Edge: e, OldTo: oldTo}) } if len(reindexBatch) > 0 { r.graph.ReindexEdges(reindexBatch) } // The marker edges were scaffolding for this pass — remove them so they do // not linger as unresolved imports. for _, e := range markerEdges { r.graph.RemoveEdge(e.From, e.To, e.Kind) } } func razorBaseName(id string) string { if i := strings.LastIndex(id, "/"); i >= 0 { return id[i+1:] } return id } func razorDir(id string) string { if i := strings.LastIndex(id, "/"); i >= 0 { return id[:i] } return "" }