a06f331eb8
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
164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
package forest
|
|
|
|
import (
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/alexaandru/go-sitter-forest/elm"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func TestExtractor_TagsPath(t *testing.T) {
|
|
// Elm ships a tags.scm — exercises the tags-driven path.
|
|
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
|
|
|
|
require.Equal(t, "elm", e.Language())
|
|
require.Equal(t, []string{".elm"}, e.Extensions())
|
|
|
|
src := []byte(`module M exposing (..)
|
|
|
|
type Color = Red | Green | Blue
|
|
|
|
double : Int -> Int
|
|
double n = n * 2
|
|
`)
|
|
|
|
res, err := e.Extract("M.elm", src)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, res.Nodes)
|
|
|
|
gotFile := false
|
|
gotDouble := false
|
|
gotColor := false
|
|
for _, n := range res.Nodes {
|
|
switch n.Kind {
|
|
case graph.KindFile:
|
|
gotFile = n.FilePath == "M.elm"
|
|
case graph.KindFunction:
|
|
if n.Name == "double" {
|
|
gotDouble = true
|
|
}
|
|
case graph.KindType:
|
|
if n.Name == "Color" {
|
|
gotColor = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, gotFile, "missing file node")
|
|
assert.True(t, gotDouble, "missing `double` function")
|
|
assert.True(t, gotColor, "missing `Color` type")
|
|
}
|
|
|
|
func TestExtractor_ParseError(t *testing.T) {
|
|
// Garbage input still returns a tree (forest grammars produce a
|
|
// best-effort tree with ERROR nodes), so Extract should succeed
|
|
// with at least the file node and not panic.
|
|
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
|
|
res, err := e.Extract("bad.elm", []byte("@@@@@@@@@"))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, res.Nodes)
|
|
assert.Equal(t, graph.KindFile, res.Nodes[0].Kind)
|
|
}
|
|
|
|
func TestExtractor_CallEdgesParentToEnclosingFunc(t *testing.T) {
|
|
// Two functions; calls inside `caller`'s body should land on
|
|
// `M.elm::caller`, not on the file. Confirms the second-pass
|
|
// attribution from buildFuncRanges/findEnclosingFunc.
|
|
//
|
|
// Note: elm's tags.scm also captures type-annotation references
|
|
// (`caller : Int -> Int`) as @reference.function — those land
|
|
// outside any function range and parent to the file, which is
|
|
// the documented module-level fallback. We only assert the
|
|
// in-body case here.
|
|
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
|
|
|
|
src := []byte(`module M exposing (..)
|
|
|
|
helper : Int -> Int
|
|
helper n = n + 1
|
|
|
|
caller : Int -> Int
|
|
caller x =
|
|
helper (helper x)
|
|
`)
|
|
|
|
res, err := e.Extract("M.elm", src)
|
|
require.NoError(t, err)
|
|
|
|
bodyCallToHelper := false
|
|
for _, ed := range res.Edges {
|
|
if ed.Kind != graph.EdgeCalls {
|
|
continue
|
|
}
|
|
if ed.To == "unresolved::helper" && ed.From == "M.elm::caller" {
|
|
bodyCallToHelper = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, bodyCallToHelper,
|
|
"expected a call edge from M.elm::caller to unresolved::helper; got %+v",
|
|
callEdgeSummary(res.Edges))
|
|
}
|
|
|
|
// callEdgeSummary builds a compact list of (from, to) pairs for
|
|
// every EdgeCalls edge — only used when an assertion fails to give
|
|
// the failure message something diagnostic to chew on.
|
|
func callEdgeSummary(edges []*graph.Edge) []string {
|
|
var out []string
|
|
for _, ed := range edges {
|
|
if ed.Kind == graph.EdgeCalls {
|
|
out = append(out, ed.From+" -> "+ed.To)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestExtractor_ModuleLevelCallsParentToFile(t *testing.T) {
|
|
// A call at module top-level (no enclosing function) should
|
|
// fall back to the file node — better than dropping the edge.
|
|
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
|
|
|
|
src := []byte(`module M exposing (..)
|
|
|
|
import X
|
|
|
|
result =
|
|
foo bar
|
|
`)
|
|
res, err := e.Extract("M.elm", src)
|
|
require.NoError(t, err)
|
|
|
|
// `result = foo bar` is a value declaration, treated as a function
|
|
// in elm tags.scm — so its calls would parent to it. We pick a
|
|
// file-level expression test that's harder: the assignment itself
|
|
// is the def, but if any @reference.call is captured outside any
|
|
// function, it should land on the file.
|
|
for _, ed := range res.Edges {
|
|
if ed.Kind == graph.EdgeCalls {
|
|
assert.True(t,
|
|
ed.From == "M.elm" || ed.From == "M.elm::result",
|
|
"unexpected call parent %q", ed.From)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractor_NilGetLanguage(t *testing.T) {
|
|
// Sticky-error path: a grammar that returns a nil pointer fails
|
|
// the first call and every subsequent call (cached error)
|
|
// without panicking.
|
|
e := New("broken", []string{".x"}, func() unsafe.Pointer { return nil }, nil)
|
|
|
|
_, err := e.Extract("a.x", []byte("anything"))
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, errNilLanguage)
|
|
|
|
// Sticky cache — second call returns same error without
|
|
// re-invoking the grammar.
|
|
_, err2 := e.Extract("b.x", []byte("again"))
|
|
require.ErrorIs(t, err2, errNilLanguage)
|
|
}
|