diff --git a/bound_tree.go b/bound_tree.go index 05b88b0ee..3ce75966c 100644 --- a/bound_tree.go +++ b/bound_tree.go @@ -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 { diff --git a/bound_tree_test.go b/bound_tree_test.go index 6a2851eab..67b6541fb 100644 --- a/bound_tree_test.go +++ b/bound_tree_test.go @@ -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 { diff --git a/fact_program.go b/fact_program.go index ddfa27b87..6fbcdf4d4 100644 --- a/fact_program.go +++ b/fact_program.go @@ -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 diff --git a/fact_program_test.go b/fact_program_test.go index 0654260ef..e491f6d06 100644 --- a/fact_program_test.go +++ b/fact_program_test.go @@ -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") diff --git a/outline.go b/outline.go index 99a5cbf47..0fa4442b1 100644 --- a/outline.go +++ b/outline.go @@ -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 diff --git a/outline_api_test.go b/outline_api_test.go index ba745092c..faeef80e4 100644 --- a/outline_api_test.go +++ b/outline_api_test.go @@ -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.