chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package languages
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
func TestTemplateUsage(t *testing.T) {
|
||||
const sfc = `<script setup lang="ts">
|
||||
import Counter from './Counter.vue'
|
||||
function noop() {}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<Counter :start="0" />
|
||||
<user-card name="x" />
|
||||
<button @click="noop">plain html</button>
|
||||
<Teleport to="body"><Modal /></Teleport>
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
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 <Teleport> 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 = `<script setup lang="ts">
|
||||
import Counter from './Counter.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Counter :start="0" />
|
||||
<Counter :start="1" />
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
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(`<script>
|
||||
function cn(x) { return x }
|
||||
function fmt(p) { return p }
|
||||
</script>
|
||||
|
||||
<div class={cn(active)}>{fmt(price)}</div>
|
||||
<ul>{items.map((i) => i)}</ul>
|
||||
`)
|
||||
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(`<script>
|
||||
let count = $state(0)
|
||||
</script>
|
||||
<button>{$state(5)}</button>
|
||||
`)
|
||||
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 = []
|
||||
---
|
||||
<ul>
|
||||
{posts.map((p) => (
|
||||
<li>{fmt(p)}</li>
|
||||
))}
|
||||
</ul>
|
||||
`)
|
||||
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<WidgetConfig>` generic must not become a `<WidgetConfig>` component
|
||||
// tag, and a frontmatter call must not be double-counted against the markup.
|
||||
src := []byte(`---
|
||||
type Props = Map<WidgetConfig>;
|
||||
const Title = make();
|
||||
function make() { return 1 }
|
||||
---
|
||||
<div>{make()}</div>
|
||||
<Card />
|
||||
`)
|
||||
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 <Card /> 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user