`
res, err := NewVueExtractor().Extract("App.vue", []byte(sfc))
if err != nil {
t.Fatal(err)
}
refs := map[string]bool{}
for _, e := range res.Edges {
if e.Kind == graph.EdgeReferences && strings.HasPrefix(e.To, "unresolved::") {
refs[strings.TrimPrefix(e.To, "unresolved::")] = true
}
}
// Component tags become cross-file references; kebab-case is PascalCased.
for _, want := range []string{"Counter", "UserCard", "Modal"} {
if !refs[want] {
t.Errorf("missing component reference %q (got refs: %v)", want, refs)
}
}
// Framework builtins and plain HTML elements are not references.
if refs["Teleport"] {
t.Error("framework builtin should be skipped")
}
if refs["Button"] || refs["Div"] || refs["button"] {
t.Error("plain HTML elements should be skipped")
}
}
// TestTemplateUsageTwoRenderSitesPositioned proves the upgrade over a
// name-deduplicated single reference: rendering the same child component at two
// template locations emits TWO positioned edges — distinct line numbers, each
// AST-resolved provenance, each carrying the template role — so find_usages can
// report every render site rather than collapsing them into one position-less
// reference.
func TestTemplateUsageTwoRenderSitesPositioned(t *testing.T) {
const sfc = `
`
res, err := NewVueExtractor().Extract("App.vue", []byte(sfc))
if err != nil {
t.Fatal(err)
}
var sites []*graph.Edge
for _, e := range res.Edges {
if e.Kind == graph.EdgeReferences && e.To == "unresolved::Counter" {
sites = append(sites, e)
}
}
if len(sites) != 2 {
t.Fatalf("expected 2 positioned render edges for Counter, got %d", len(sites))
}
lines := map[int]bool{}
for _, e := range sites {
if e.Line == 0 {
t.Errorf("render edge for Counter has no line number")
}
if e.Origin != graph.OriginASTResolved {
t.Errorf("render edge at line %d origin=%v, want OriginASTResolved", e.Line, e.Origin)
}
if isTmpl, _ := e.Meta["template"].(bool); !isTmpl {
t.Errorf("render edge at line %d missing Meta[template]=true", e.Line)
}
if got := graph.RefContextOf(e, graph.KindType); got != graph.RefContextTemplate {
t.Errorf("render edge at line %d ref_context=%q, want %q", e.Line, got, graph.RefContextTemplate)
}
lines[e.Line] = true
}
if len(lines) != 2 {
t.Errorf("expected 2 distinct render-site lines, got %v", lines)
}
}
func templateExprCall(edges []*graph.Edge, from, name string) *graph.Edge {
for _, e := range edges {
if e.Kind != graph.EdgeCalls || e.From != from || e.To != "unresolved::"+name || e.Meta == nil {
continue
}
if v, _ := e.Meta["via"].(string); v == "template_expr" {
return e
}
}
return nil
}
func TestTemplateExpr_SvelteMarkupCalls(t *testing.T) {
src := []byte(`
{fmt(price)}
{items.map((i) => i)}
`)
res, err := NewSvelteExtractor().Extract("Button.svelte", src)
if err != nil {
t.Fatal(err)
}
const comp = "Button.svelte::Button"
if templateExprCall(res.Edges, comp, "cn") == nil {
t.Errorf("expected a template_expr call to cn from class={cn(active)}")
}
if templateExprCall(res.Edges, comp, "fmt") == nil {
t.Errorf("expected a template_expr call to fmt from {fmt(price)}")
}
// A method call's name is not a receiver-less free-function call.
if templateExprCall(res.Edges, comp, "map") != nil {
t.Errorf("method call items.map must not emit a template_expr call to map")
}
var sawCn bool
for _, n := range res.Nodes {
if n.Kind == graph.KindFunction && n.Name == "cn" {
sawCn = true
}
}
if !sawCn {
t.Errorf("the script function cn should be present for resolution")
}
}
func TestTemplateExpr_SvelteRuneSuppressed(t *testing.T) {
src := []byte(`
`)
res, err := NewSvelteExtractor().Extract("Counter.svelte", src)
if err != nil {
t.Fatal(err)
}
if templateExprCall(res.Edges, "Counter.svelte::Counter", "$state") != nil {
t.Errorf("svelte rune $state must be suppressed, not emitted as a template call")
}
}
func TestTemplateExpr_AstroMultiLineMapBlock(t *testing.T) {
// A helper called only inside an Astro multi-line `{collection.map(() => (...))}`
// render block must resolve — the open-brace span is captured in one pass and
// the frontmatter is not double-scanned.
src := []byte(`---
function fmt(p) { return p }
const posts = []
---
{posts.map((p) => (
{fmt(p)}
))}
`)
res, err := NewAstroExtractor().Extract("Page.astro", src)
if err != nil {
t.Fatal(err)
}
const comp = "Page.astro::Page"
e := templateExprCall(res.Edges, comp, "fmt")
if e == nil {
t.Fatalf("expected a template_expr call to fmt from the multi-line map block")
}
if e.Line != 7 {
t.Errorf("fmt call line = %d (want 7, the actual call site inside the span)", e.Line)
}
// The `.map(` is a method call, not a free function.
if templateExprCall(res.Edges, comp, "map") != nil {
t.Errorf("posts.map must not emit a template_expr call to map")
}
// Frontmatter is delegated TS — its own `fmt` definition must not be
// re-scanned as a markup call (only one template_expr fmt edge, from L7).
count := 0
for _, ed := range res.Edges {
if ed.Kind == graph.EdgeCalls && ed.To == "unresolved::fmt" && ed.Meta != nil {
if v, _ := ed.Meta["via"].(string); v == "template_expr" {
count++
}
}
}
if count != 1 {
t.Errorf("expected exactly 1 template_expr fmt call (no frontmatter double-scan), got %d", count)
}
}
func TestTemplateExpr_AstroFrontmatterCovered(t *testing.T) {
// The Astro frontmatter is delegated TS — the markup scan must not read it:
// a `Map` generic must not become a `` component
// tag, and a frontmatter call must not be double-counted against the markup.
src := []byte(`---
type Props = Map;
const Title = make();
function make() { return 1 }
---
{make()}
`)
res, err := NewAstroExtractor().Extract("Page.astro", src)
if err != nil {
t.Fatal(err)
}
const comp = "Page.astro::Page"
tagRef := func(name string) bool {
for _, e := range res.Edges {
if e.Kind == graph.EdgeReferences && e.From == comp && e.To == "unresolved::"+name {
return true
}
}
return false
}
if !tagRef("Card") {
t.Errorf("the markup tag should be referenced")
}
for _, leaked := range []string{"WidgetConfig", "Map", "Props", "Title"} {
if tagRef(leaked) {
t.Errorf("frontmatter identifier %s must not become a component reference", leaked)
}
}
// The markup `{make()}` is the only template_expr call — the frontmatter
// `make()` is delegated TS, not re-scanned.
count := 0
for _, e := range res.Edges {
if e.Kind == graph.EdgeCalls && e.From == comp && e.To == "unresolved::make" && e.Meta != nil {
if v, _ := e.Meta["via"].(string); v == "template_expr" {
count++
}
}
}
if count != 1 {
t.Errorf("expected exactly 1 template_expr make call (no frontmatter double-count), got %d", count)
}
}
func TestTemplateMustacheSpans_MultiLine(t *testing.T) {
// A group that opens on one line and closes several lines later is one span.
b := []byte("a {posts.map((p) => (\n fmt(p)\n))} b")
spans := templateMustacheSpans(b)
if len(spans) != 1 {
t.Fatalf("expected 1 multi-line span, got %d", len(spans))
}
body := string(b[spans[0].start:spans[0].end])
if want := "posts.map((p) => (\n fmt(p)\n))"; body != want {
t.Errorf("span body = %q (want %q)", body, want)
}
}