Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions bound_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ func (bt *BoundTree) Source() []byte {
return bt.tree.Source()
}

// ParseStopReason reports why parsing ended.
func (bt *BoundTree) ParseStopReason() ParseStopReason {
if bt == nil || bt.tree == nil {
return ParseStopNone
}
return bt.tree.ParseStopReason()
}

// ParseStoppedEarly reports whether parsing hit an early-stop condition.
func (bt *BoundTree) ParseStoppedEarly() bool {
return bt != nil && bt.tree != nil && bt.tree.ParseStoppedEarly()
}

// ParseRuntime returns diagnostics from the parse that built the tree.
func (bt *BoundTree) ParseRuntime() ParseRuntime {
if bt == nil || bt.tree == nil {
return ParseRuntime{StopReason: ParseStopNone}
}
return bt.tree.ParseRuntime()
}

// NodeType returns the node's type name, resolved via the bound language.
func (bt *BoundTree) NodeType(n *Node) string {
if bt == nil || bt.tree == nil || n == nil {
Expand Down
29 changes: 29 additions & 0 deletions bound_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,35 @@ func TestBoundTreeSource(t *testing.T) {
}
}

func TestBoundTreeParseReceipt(t *testing.T) {
tree := buildSimpleTree(queryTestLanguage())
tree.setParseStopReason(ParseStopTimeout)
bt := Bind(tree)

if got := bt.ParseStopReason(); got != ParseStopTimeout {
t.Fatalf("ParseStopReason() = %q, want %q", got, ParseStopTimeout)
}
if !bt.ParseStoppedEarly() {
t.Fatal("ParseStoppedEarly() = false, want true")
}
if got := bt.ParseRuntime().StopReason; got != ParseStopTimeout {
t.Fatalf("ParseRuntime().StopReason = %q, want %q", got, ParseStopTimeout)
}
}

func TestNilBoundTreeParseReceipt(t *testing.T) {
var bt *BoundTree
if got := bt.ParseStopReason(); got != ParseStopNone {
t.Fatalf("ParseStopReason() = %q, want %q", got, ParseStopNone)
}
if bt.ParseStoppedEarly() {
t.Fatal("ParseStoppedEarly() = true, want false")
}
if got := bt.ParseRuntime().StopReason; got != ParseStopNone {
t.Fatalf("ParseRuntime().StopReason = %q, want %q", got, ParseStopNone)
}
}

func TestBindNil(t *testing.T) {
bt := Bind(nil)
if bt.RootNode() != nil {
Expand Down
9 changes: 9 additions & 0 deletions fact_program.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ func (p *FactProgram) Extract(tree *Tree) FactSet {
return facts
}

// ExtractBound emits selected facts from a BoundTree.
// It uses the same guards and traversal as Extract.
func (p *FactProgram) ExtractBound(tree *BoundTree) FactSet {
if tree == nil {
return p.Extract(nil)
}
return p.Extract(tree.tree)
}

func (p *FactProgram) compileFields() {
if p == nil || p.language == nil {
return
Expand Down
23 changes: 23 additions & 0 deletions fact_program_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,29 @@ func TestFactProgramSelectionAndLanguageGuard(t *testing.T) {
}
}

func TestFactProgramExtractBoundMatchesExtract(t *testing.T) {
tree := parseUnderstandingTree(t, "main.go", []byte("package main\nfunc run() { helper() }\n"))
defer tree.Release()

program, err := gotreesitter.NewFactProgram(tree.Language(), gotreesitter.FactAll)
if err != nil {
t.Fatalf("NewFactProgram failed: %v", err)
}
want := program.Extract(tree)
got := program.ExtractBound(gotreesitter.Bind(tree))
if !slices.Equal(got.Definitions, want.Definitions) ||
!slices.Equal(got.Calls, want.Calls) ||
!slices.Equal(got.Heritage, want.Heritage) ||
!slices.Equal(got.Imports, want.Imports) {
t.Fatalf("ExtractBound = %#v, want %#v", got, want)
}

empty := program.ExtractBound(nil)
if empty.Definitions != nil || empty.Calls != nil || empty.Heritage != nil || empty.Imports != nil {
t.Fatalf("ExtractBound(nil) = %#v, want empty set", empty)
}
}

func TestNewFactProgramRejectsInvalidConfiguration(t *testing.T) {
if _, err := gotreesitter.NewFactProgram(nil, gotreesitter.FactAll); err == nil {
t.Fatal("NewFactProgram accepted a nil language")
Expand Down
9 changes: 9 additions & 0 deletions outline.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ func (o *Outliner) OutlineTree(tree *Tree) ([]OutlineSymbol, OutlineReport) {
return symbols, report
}

// OutlineBound projects an outline from a BoundTree.
// It uses the same validation, query, and receipt path as OutlineTree.
func (o *Outliner) OutlineBound(tree *BoundTree) ([]OutlineSymbol, OutlineReport) {
if tree == nil {
return o.OutlineTree(nil)
}
return o.OutlineTree(tree.tree)
}

// treeLanguageMatches reports whether the tree came from the same language the
// query compiled against. Query symbol identifiers are language specific, so
// running a query over a foreign tree yields nonsense; the outliner declines
Expand Down
19 changes: 19 additions & 0 deletions outline_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ func TestOutlineDeclinesWithoutTagsQuery(t *testing.T) {
}
}

func TestOutlineBoundMatchesOutlineTree(t *testing.T) {
tree, lang, query := loadOutlineFixture(t, "go", "go/service.go.fixture")
outliner, err := gts.NewOutliner(lang, query)
if err != nil {
t.Fatalf("NewOutliner failed: %v", err)
}

wantSymbols, wantReport := outliner.OutlineTree(tree)
gotSymbols, gotReport := outliner.OutlineBound(gts.Bind(tree))
if !reflect.DeepEqual(gotSymbols, wantSymbols) || gotReport != wantReport {
t.Fatalf("OutlineBound = (%#v, %#v), want (%#v, %#v)", gotSymbols, gotReport, wantSymbols, wantReport)
}

_, nilReport := outliner.OutlineBound(nil)
if nilReport.DeclineReason != gts.OutlineDeclineNilTree {
t.Fatalf("OutlineBound(nil) decline = %q, want %q", nilReport.DeclineReason, gts.OutlineDeclineNilTree)
}
}

// TestOutlineDeclineReasonsAreDistinguishable is the fix for a receipt shape
// that made four different failures byte identical. A caller must be able to
// tell a genuinely empty file from a projection that never ran.
Expand Down
Loading