package languages import ( "strings" "testing" "github.com/zzet/gortex/internal/graph" ) func TestRazorExtractor(t *testing.T) { const razor = `@page "/counter" @inherits ComponentBase @inject IWeatherService Weather

Counter

@code { private int count = 0; private void Increment() { count++; } } ` res, err := NewRazorExtractor().Extract("Counter.razor", []byte(razor)) if err != nil { t.Fatal(err) } var incr *graph.Node refs := map[string]bool{} for _, n := range res.Nodes { if n.Name == "Increment" { incr = n } if n.Name == "__RazorCode" { t.Errorf("synthetic wrapper class leaked into the graph: %+v", n) } } for _, e := range res.Edges { if e.Kind == graph.EdgeReferences && strings.HasPrefix(e.To, "unresolved::") { refs[strings.TrimPrefix(e.To, "unresolved::")] = true } } // The @code block's C# member is extracted, rebased into host coordinates. if incr == nil { t.Fatalf("@code method 'Increment' was not extracted from the Razor file") } if incr.Language != "razor" || incr.Meta["inline_script"] != true { t.Errorf("delegated symbol lang=%q meta=%v, want razor + inline_script", incr.Language, incr.Meta) } if incr.StartLine != 10 { t.Errorf("Increment StartLine = %d, want 10 (host-file coordinates)", incr.StartLine) } // @inherits and @inject directives become type references. for _, want := range []string{"ComponentBase", "IWeatherService"} { if !refs[want] { t.Errorf("missing directive type reference %q (refs: %v)", want, refs) } } } // TestRazorUsingExtraction pins `@using` directive extraction (with the // `@using static` member-import form skipped to its namespace) and the // path-derived component namespace. func TestRazorUsingExtraction(t *testing.T) { const razor = `@using App.Widgets @using static System.Math ` res, err := NewRazorExtractor().Extract("Components/Page.razor", []byte(razor)) if err != nil { t.Fatal(err) } usings := map[string]bool{} for _, e := range res.Edges { if e.Kind == graph.EdgeImports && strings.HasPrefix(e.To, "unresolved::razor_using::") { usings[strings.TrimPrefix(e.To, "unresolved::razor_using::")] = true } } for _, want := range []string{"App.Widgets", "System.Math"} { if !usings[want] { t.Errorf("missing @using namespace %q (got: %v)", want, usings) } } var comp *graph.Node for _, n := range res.Nodes { if n.Name == "Page" && n.Meta["component"] == true { comp = n } } if comp == nil { t.Fatal("Page component node not emitted") } if comp.Meta["scope_ns"] != "Components" { t.Errorf("component scope_ns = %v, want Components", comp.Meta["scope_ns"]) } } // TestRazorBraceMatcherSkipsStringsAndCarvesBareBlock is the B4 named test: a // `}` inside a C# string or comment in a @code block must not truncate the // block (which dropped every member after it), and a bare @{ } block is also // carved and delegated. The per-file Blazor component node is emitted. func TestRazorBraceMatcherSkipsStringsAndCarvesBareBlock(t *testing.T) { src := []byte("

Hi

\n" + "@code {\n" + " string Brace() { return \"}\"; }\n" + " void After() { }\n" + " // a comment with a } brace\n" + " void Last() { }\n" + "}\n" + "@{\n" + " void BareHelper() { }\n" + "}\n") res, err := NewRazorExtractor().Extract("Counter.razor", src) if err != nil { t.Fatal(err) } methods := map[string]bool{} for _, n := range res.Nodes { if n.Kind == graph.KindMethod || n.Kind == graph.KindFunction { methods[n.Name] = true } } // All three @code members survive — the brace inside the string / comment // no longer truncates the block. for _, want := range []string{"Brace", "After", "Last"} { if !methods[want] { t.Fatalf("method %s was dropped by brace truncation; got %v", want, methods) } } // The synthetic wrapper method/class are not leaked as symbols. if methods["__Body"] { t.Fatalf("synthetic __Body wrapper leaked as a method") } // The bare @{ } block is carved (two spans total). spans := razorCodeSpans(src) var bareSeen bool for _, s := range spans { if s.bare { bareSeen = true } } if !bareSeen { t.Fatalf("bare @{ } block was not carved; spans=%v", spans) } // The per-file component node exists and is navigable. var component bool for _, n := range res.Nodes { if n.Kind == graph.KindType && n.ID == "Counter.razor::Counter" { if c, _ := n.Meta["component"].(bool); c { component = true } } } if !component { t.Fatalf("expected a per-file component node Counter.razor::Counter") } } // TestRazorMatchBraceUnit checks the matcher directly on literals and comments. func TestRazorMatchBraceUnit(t *testing.T) { // open at index 0; the } inside the string must be ignored. src := []byte(`{ var s = "a}b"; var c = '}'; /* } */ }`) end := matchRazorBrace(src, 0) if end != len(src)-1 { t.Fatalf("matchRazorBrace = %d, want %d (final brace)", end, len(src)-1) } } func razorTagRef(edges []*graph.Edge, from, name string) *graph.Edge { for _, e := range edges { if e.Kind == graph.EdgeReferences && e.From == from && e.To == "unresolved::"+name { return e } } return nil } func razorAnyRefTo(edges []*graph.Edge, name string) bool { for _, e := range edges { if e.Kind == graph.EdgeReferences && e.To == "unresolved::"+name { return true } } return false } func TestRazorComponentTagAndGenericArg(t *testing.T) { src := []byte(`

Parent

@code { private List items = new(); } `) res, err := NewRazorExtractor().Extract("Pages/Parent.razor", src) if err != nil { t.Fatal(err) } const comp = "Pages/Parent.razor::Parent" if razorTagRef(res.Edges, comp, "Child") == nil { t.Errorf("expected a component-tag reference to Child") } if razorTagRef(res.Edges, comp, "Grid") == nil { t.Errorf("expected a component-tag reference to Grid") } // The TItem generic type-arg references the DTO. if !razorAnyRefTo(res.Edges, "CatalogItem") { t.Errorf("expected a type reference to the TItem value CatalogItem") } // The C# generic `List` inside @code must not be misread as a // markup tag — its body is blanked before the tag scan. if razorTagRef(res.Edges, comp, "DecoyType") != nil { t.Errorf("a generic type-arg inside @code must not become a component-tag reference") } if razorTagRef(res.Edges, comp, "List") != nil { t.Errorf("a generic container inside @code must not become a component-tag reference") } } func TestRazorBlazorBuiltinsSkipped(t *testing.T) { src := []byte(` `) res, err := NewRazorExtractor().Extract("Pages/Form.razor", src) if err != nil { t.Fatal(err) } const comp = "Pages/Form.razor::Form" for _, b := range []string{"EditForm", "InputText", "ValidationSummary", "Router"} { if razorTagRef(res.Edges, comp, b) != nil { t.Errorf("Blazor framework component %s must not emit a reference", b) } } if razorTagRef(res.Edges, comp, "MyWidget") == nil { t.Errorf("user component MyWidget should still be referenced") } } func TestBlazorBuiltinNotSuppressedInSvelte(t *testing.T) { // `EditForm` is a Blazor builtin but a plausible user component elsewhere — // the suppression must be scoped to Razor, not leak into other languages. res, err := NewSvelteExtractor().Extract("Form.svelte", []byte("\n")) if err != nil { t.Fatal(err) } if razorTagRef(res.Edges, "Form.svelte::Form", "EditForm") == nil { t.Errorf("a user EditForm component in Svelte must NOT be suppressed (language-scoping)") } }