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
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,11 @@ ID, bare name, or qualified name; `Callers`/`Callees` return one-hop call edges;
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
direct calls, same-file calls within the same class or module, distinct instance
and singleton method names, unresolved dynamic receivers, subshells, and local
`require_relative` load edges. Other supported languages contain file, symbol,
module, and import structure with call edges omitted.
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
dynamic receivers, subshells, and local load edges. Other supported languages
contain file, symbol, module, and import structure with call edges omitted.

`Tree(paths []string) string` renders a box-drawing directory tree from a flat
path list.
Expand Down Expand Up @@ -148,11 +149,7 @@ $ outline affected -inferred -dir testdata/cli-go ext:go:os/exec:Command
NODE Handler func main.go:9 exported=true sig=func Handler(name string) error
NODE os/exec.Command external exported=false sig=
NODE main func main.go:17 exported=false sig=func main()
EDGE Handler --calls[inferred]--> Run at main.go:14
NODE Run external exported=false sig=
EDGE Handler --calls[inferred]--> os/exec.Command at main.go:14
EDGE Handler --calls[inferred]--> Load at main.go:10
NODE Load func store/store.go:7 exported=true sig=func Load(name string) (Record, error)
EDGE main --calls[extracted]--> Handler at main.go:18
```

Expand Down
91 changes: 62 additions & 29 deletions analyse.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ import (
// retains the full definition span and nesting so callers can compute node
// identities and containment before the tree is released.
type decl struct {
Name string
Kind string
Line int
Exported bool
NameAt uint32
Start uint32
End uint32
SigEnd uint32
Parent int
Params []string
Singleton bool
Owner string
Name string
Kind string
Line int
Exported bool
NameAt uint32
Start uint32
End uint32
SigEnd uint32
Parent int
Params []string
Singleton bool
Method bool
ModuleFunction bool
Conditional bool
Owner string
}

func (d decl) symID(path string) string {
Expand All @@ -30,10 +33,16 @@ func (d decl) symID(path string) string {

// analysis is the per-file fact set that graph resolution consumes.
type analysis struct {
Lang string
Decls []decl
Imports []Import
Calls []Call
Lang string
Package string
Decls []decl
Imports []Import
Calls []Call
Bindings []binding
SyntaxErrors bool
PyImports []pythonImport
PyExports *pythonExports
RubyLoads []Import
}

// analyse parses src once and returns every fact the graph builder needs
Expand All @@ -49,10 +58,28 @@ func analyse(src []byte, filename string) (*analysis, bool) {
root := tree.RootNode()
matches := l.query.Execute(tree)

a := &analysis{Lang: l.name}
a := &analysis{Lang: l.name, SyntaxErrors: root.HasError()}
if l.name == "go" {
for i := range root.NamedChildCount() {
node := root.NamedChild(i)
if node.Type(l.language) == "package_clause" && node.NamedChildCount() > 0 {
a.Package = node.NamedChild(0).Text(src)
break
}
}
}
a.Decls = extractDecls(src, l, root, matches)
a.Imports, _ = importsFor(src, l, root)
a.Calls, _ = callsFor(src, l, root, a.Decls)
a.Bindings = bindingsFor(src, l, root, a.Decls)
if l.name == "python" {
a.PyImports = pythonImportFacts(src, l, root, a.Decls)
a.PyExports = pythonExportFacts(src, l, root)
}
if l.name == "ruby" {
a.RubyLoads = rubyLoadFacts(src, l, root)
rubyModuleFunctions(a, src, l, root)
}
return a, true
}

Expand Down Expand Up @@ -113,7 +140,11 @@ func declsFromMatch(src []byte, l *lang, m ts.QueryMatch) []decl {
}
params := extractParams(src, l, definition)
singleton := l.name == "ruby" && definition.Type(l.language) == "singleton_method"
method := l.name == "go" && definition.Type(l.language) == "method_declaration"
owner := ""
if method {
owner = goReceiverOwner(src, l, definition)
}
if singleton {
if object := definition.ChildByFieldName("object", l.language); object != nil {
owner = object.Text(src)
Expand All @@ -129,18 +160,20 @@ func declsFromMatch(src []byte, l *lang, m ts.QueryMatch) []decl {
continue
}
out = append(out, decl{
Name: name,
Kind: normalizeSymbolKind(l.name, kind, name, definition, src, l.language),
Line: int(n.StartPoint().Row) + 1,
Exported: exported || symbolExported(l.name, name, definition, src, l.language),
NameAt: n.StartByte(),
Start: start,
End: end,
SigEnd: sigEnd,
Parent: -1,
Params: params,
Singleton: singleton,
Owner: owner,
Name: name,
Kind: normalizeSymbolKind(l.name, kind, name, definition, src, l.language),
Line: int(n.StartPoint().Row) + 1,
Exported: exported || symbolExported(l.name, name, definition, src, l.language),
NameAt: n.StartByte(),
Start: start,
End: end,
SigEnd: sigEnd,
Parent: -1,
Params: params,
Singleton: singleton,
Method: method,
Owner: owner,
Conditional: l.name == "python" && pythonConditional(definition, l.language),
})
}
return out
Expand Down
134 changes: 134 additions & 0 deletions binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package outline

import ts "github.com/odvcencio/gotreesitter"

const forStatement = "for_statement"

type binding struct {
Name string
In int
Start uint32
End uint32
At uint32
Decl int
}

func bindingsFor(src []byte, l *lang, root *ts.Node, decls []decl) []binding {
if l.name != "go" && l.name != "python" {
return nil
}
var out []binding
walkNamed(root, func(node *ts.Node) {
names, scope, start := bindingNode(src, l, node)
if scope == nil || len(names) == 0 {
return
}
in := enclosing(decls, scope.StartByte())
target := bindingDeclaration(l, node, decls)
at := node.EndByte()
if l.name == "python" && node.Type(l.language) == forStatement {
at = node.StartByte()
}
for _, name := range names {
if l.name != "go" || name != "_" {
out = append(out, binding{Name: name, In: in, Start: start, End: scope.EndByte(), At: at, Decl: target})
}
}
})
return out
}

func bindingNode(src []byte, l *lang, node *ts.Node) ([]string, *ts.Node, uint32) {
switch l.name {
case "go":
names := goBindingNames(src, l.language, node)
if len(names) > 0 {
return names, goBindingScope(node, l.language), node.EndByte()
}
case "python":
switch node.Type(l.language) {
case "assignment", "augmented_assignment", forStatement:
names := bindingNames(src, l.language, node.ChildByFieldName("left", l.language))
if len(names) > 0 {
if scope := pythonBindingScope(node, l.language); scope != nil {
return names, scope, scope.StartByte()
}
}
}
}
return nil, nil, 0
}

func bindingDeclaration(l *lang, node *ts.Node, decls []decl) int {
if l.name == "go" {
switch node.Type(l.language) {
case "type_spec", "type_alias":
return enclosing(decls, node.StartByte())
}
}
return -1
}

func bindingNames(src []byte, language *ts.Language, node *ts.Node) []string {
if node == nil {
return nil
}
switch node.Type(language) {
case "identifier", "type_identifier":
return []string{node.Text(src)}
case "expression_list", "pattern_list", "tuple_pattern", "list_pattern", "tuple", "list", "list_splat_pattern":
var out []string
for i := range node.NamedChildCount() {
out = append(out, bindingNames(src, language, node.NamedChild(i))...)
}
return out
}
return nil
}

func goBindingNames(src []byte, language *ts.Language, node *ts.Node) []string {
switch node.Type(language) {
case "short_var_declaration":
return bindingNames(src, language, node.ChildByFieldName("left", language))
case "type_spec", "type_alias":
return bindingNames(src, language, node.ChildByFieldName("name", language))
case "range_clause":
for i := range node.ChildCount() {
if node.Child(i).Type(language) == ":=" {
return bindingNames(src, language, node.ChildByFieldName("left", language))
}
}
case "var_spec", "const_spec":
var out []string
for i := range node.NamedChildCount() {
child := node.NamedChild(i)
if child.Type(language) == "identifier" {
out = append(out, child.Text(src))
}
}
return out
}
return nil
}

func goBindingScope(node *ts.Node, language *ts.Language) *ts.Node {
for parent := node.Parent(); parent != nil; parent = parent.Parent() {
switch parent.Type(language) {
case "block", "if_statement", forStatement, "expression_switch_statement", "type_switch_statement", "communication_case", "expression_case", "type_case":
return parent
}
}
return nil
}

func pythonBindingScope(node *ts.Node, language *ts.Language) *ts.Node {
for parent := node.Parent(); parent != nil; parent = parent.Parent() {
switch parent.Type(language) {
case "function_definition":
return parent
case "class_definition":
return nil
}
}
return nil
}
Loading
Loading