Skip to content
Open
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ ID, bare name, or qualified name; `Callers`/`Callees` return one-hop call edges;
`Affected(seeds, opts)` returns reverse-reachable evidence paths from a set of
sinks; `Path(from, to, opts)` returns the shortest forward call chain;
`JSON(w)` writes sorted output so repeated builds of unchanged input are
byte-identical. Go and Python include cross-file call resolution. Ruby includes
byte-identical. Anonymous Go functions, Python lambdas, and Ruby blocks have
their own function nodes and retain their body calls. Containment or passing a
callback does not add a call edge to its body. Directly invoked Go/Python
literals have call edges to their anonymous nodes; variable-held callables and
callback execution by another function remain unresolved.

Go and Python include cross-file call resolution. Ruby includes
direct calls within the same class or module and follows literal
`require_relative` chains to singleton and `module_function` bodies in loaded
files. It retains distinct instance and singleton method names, unresolved
Expand Down
3 changes: 3 additions & 0 deletions analyse.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type decl struct {
Method bool
ModuleFunction bool
Owner string
Anonymous bool
BodyStart uint32
}

func (d decl) symID(path string) string {
Expand Down Expand Up @@ -87,6 +89,7 @@ func extractDecls(src []byte, l *lang, root *ts.Node, matches []ts.QueryMatch) [
for _, m := range matches {
raw = append(raw, declsFromMatch(src, l, m)...)
}
raw = append(raw, anonymousDecls(src, l, root)...)
if exported, merge, ok := explicitExports(l, root, src); ok {
for i := range raw {
if merge {
Expand Down
82 changes: 82 additions & 0 deletions anonymous.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package outline

import (
"strconv"

ts "github.com/odvcencio/gotreesitter"
)

func anonymousName(kind string, start uint32) string {
return "<" + kind + "@" + strconv.FormatUint(uint64(start), 10) + ">"
}

func anonymousDecls(src []byte, l *lang, root *ts.Node) []decl {
if l.name != "go" && l.name != "python" && l.name != "ruby" {
return nil
}
var out []decl
walkNamed(root, func(node *ts.Node) {
kind := ""
definition := node
switch l.name {
case "go":
if node.Type(l.language) == "func_literal" {
kind = "func"
}
case "python":
if node.Type(l.language) == "lambda" {
kind = "lambda"
}
case "ruby":
if node.Type(l.language) == "block" || node.Type(l.language) == "do_block" {
kind = "block"
if parent := node.Parent(); parent != nil && parent.Type(l.language) == "lambda" {
definition = parent
kind = "lambda"
}
}
}
if kind == "" {
return
}
bodyStart := node.EndByte()
if body := node.ChildByFieldName("body", l.language); body != nil {
bodyStart = body.StartByte()
}
callStart := bodyStart
if l.name == "ruby" {
if params := definition.ChildByFieldName("parameters", l.language); params != nil {
callStart = params.StartByte()
}
}
out = append(out, decl{
Name: anonymousName(kind, definition.StartByte()), Kind: KindFunc,
Line: sourceLine(definition), NameAt: definition.StartByte(),
Start: definition.StartByte(), End: definition.EndByte(), SigEnd: bodyStart,
Parent: -1, Params: extractParams(src, l, definition),
Anonymous: true, BodyStart: callStart,
})
})
return out
}

func callEnclosing(decls []decl, pos uint32) int {
best := -1
for i, d := range decls {
if d.Start > pos {
break
}
// Lambda defaults execute outside their anonymous body.
if d.End > pos && (!d.Anonymous || pos >= d.BodyStart) {
best = i
}
}
return best
}

func callFunction(node *ts.Node, language *ts.Language) *ts.Node {
for node != nil && node.Type(language) == "parenthesized_expression" && node.NamedChildCount() == 1 {
node = node.NamedChild(0)
}
return node
}
2 changes: 1 addition & 1 deletion binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestBuildGoLocalBindings(t *testing.T) {
{"short declaration", "helper := func(){}; helper()", 0, 1},
{"var declaration", "var helper = func(){}; helper()", 0, 1},
{"initializer", "helper := helper; helper()", 0, 1},
{"initializer call", "helper := func(){ helper() }; helper()", 1, 1},
{"initializer call", "helper := func(){ helper() }; helper()", 0, 1},
{"before declaration", "helper(); helper := func(){}; helper()", 1, 1},
{"sibling block", "{ helper := func(){}; helper() }; helper()", 1, 1},
{"if initializer", "if helper := func(){}; true { helper() }; helper()", 1, 1},
Expand Down
6 changes: 6 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ func emitFileNodes(g *Graph, files []fileAnalysis, methodOwners map[string]strin
}

func qualified(lang string, decls []decl, i int) string {
if d := decls[i]; d.Anonymous {
if d.Parent >= 0 {
return qualified(lang, decls, d.Parent) + "." + d.Name
}
return d.Name
}
if lang == "ruby" {
return rubyQualified(decls, i)
}
Expand Down
12 changes: 9 additions & 3 deletions call.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func callsFor(src []byte, l *lang, root *ts.Node, decls []decl) ([]Call, bool) {
return nil, false
}
for i := range calls {
calls[i].In = enclosing(decls, calls[i].Start)
calls[i].In = callEnclosing(decls, calls[i].Start)
}
return calls, true
}
Expand All @@ -67,7 +67,7 @@ func goCalls(src []byte, language *ts.Language, root *ts.Node) []Call {
if node.Type(language) != "call_expression" {
return
}
fn := node.ChildByFieldName("function", language)
fn := callFunction(node.ChildByFieldName("function", language), language)
if fn == nil {
return
}
Expand All @@ -76,6 +76,9 @@ func goCalls(src []byte, language *ts.Language, root *ts.Node) []Call {
case "identifier":
c.Name = fn.Text(src)
c.ReceiverKind = ReceiverBare
case "func_literal":
c.Name = anonymousName("func", fn.StartByte())
c.ReceiverKind = ReceiverExpression
case "selector_expression":
recv := fn.ChildByFieldName("operand", language)
member := fn.ChildByFieldName("field", language)
Expand Down Expand Up @@ -104,7 +107,7 @@ func pythonCalls(src []byte, language *ts.Language, root *ts.Node) []Call {
if node.Type(language) != "call" {
return
}
fn := node.ChildByFieldName("function", language)
fn := callFunction(node.ChildByFieldName("function", language), language)
if fn == nil {
return
}
Expand All @@ -113,6 +116,9 @@ func pythonCalls(src []byte, language *ts.Language, root *ts.Node) []Call {
case "identifier":
c.Name = fn.Text(src)
c.ReceiverKind = ReceiverBare
case "lambda":
c.Name = anonymousName("lambda", fn.StartByte())
c.ReceiverKind = ReceiverExpression
case "attribute":
recv := fn.ChildByFieldName("object", language)
member := fn.ChildByFieldName("attribute", language)
Expand Down
Loading
Loading