Skip to content

Commit d517192

Browse files
VEN-1 | Parse single file for function declarations (#943)
* VEN-1 | Parse single file for function declarations
1 parent 6f279fb commit d517192

File tree

7 files changed

+196
-17
lines changed

7 files changed

+196
-17
lines changed

test-crawler/collector/collector.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ type Function struct {
2828
IsTesting bool `json:"-"`
2929
}
3030

31+
type FunctionAnnotation struct {
32+
Name string `yaml:"name"`
33+
InputParams string `yaml:"inputParams"`
34+
ReturnValues string `yaml:"returnValues"`
35+
Description string
36+
Public bool
37+
}
38+
3139
func GetTestFiles(root string, ignore []string) (files []TestFile, err error) {
3240
fileArray, err := listTestFiles(root, ignore)
3341
if err != nil {

test-crawler/crawler_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestExtractPublicMethodsFromFile(t *testing.T) {
11+
fnsAnn, err := extractPublicMethodsFromFile(context.Background(), "./mocks/event.go")
12+
if err != nil {
13+
t.Errorf("got error: %v", err.Error())
14+
}
15+
16+
if len(fnsAnn) != 2 {
17+
t.Errorf("got %q, expected %q methods", len(fnsAnn), 2)
18+
}
19+
20+
assert.Equal(t, "HelloEvent", fnsAnn[0].Name)
21+
assert.Equal(t, "()", fnsAnn[0].InputParams) // input param
22+
assert.Equal(t, "string", fnsAnn[0].ReturnValues) // return param
23+
24+
assert.Equal(t, "HelloEventWithParameter", fnsAnn[1].Name)
25+
assert.Equal(t, "(param string)", fnsAnn[1].InputParams)
26+
assert.Equal(t, "(string, error)", fnsAnn[1].ReturnValues)
27+
}

test-crawler/extractor/extractor.go

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"strings"
11+
"unicode"
1112

1213
a "testsuites/annotations"
1314
c "testsuites/collector"
@@ -26,6 +27,7 @@ const (
2627
BLOCK NodeType = "block"
2728
CALL_EXPRESSION NodeType = "call_expression"
2829
IDENTIFIER NodeType = "identifier"
30+
METHOD_DECLARATION NodeType = "method_declaration"
2931
)
3032

3133
type Metadata struct {
@@ -39,6 +41,11 @@ type FileData struct {
3941
Functions []c.Function
4042
}
4143

44+
type FunctionAnnotationNode struct {
45+
Node *sitter.Node
46+
Function c.FunctionAnnotation
47+
}
48+
4249
type hasNoTests bool
4350

4451
func ExtractInfo(file c.TestFile, ctx context.Context, fileID c.FileID) (*FileData, hasNoTests, error) {
@@ -81,6 +88,29 @@ func ExtractInfo(file c.TestFile, ctx context.Context, fileID c.FileID) (*FileDa
8188
return fileData, hasNoTests(!checkForExistanceOfTests(fData)), nil
8289
}
8390

91+
func GetExportedFunctions(ctx context.Context, filePath string) ([]c.FunctionAnnotation, error) {
92+
content, err := getFileContent(filePath)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
parser := sitter.NewParser()
98+
parser.SetLanguage(golang.GetLanguage())
99+
100+
tree, err := parser.ParseCtx(ctx, nil, []byte(content))
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
cursor := sitter.NewTreeCursor(tree.RootNode())
106+
fnsAnno, err := parseContentForFunctions(content, cursor)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
return fnsAnno, nil
112+
}
113+
84114
func getFileContent(filePath string) (content string, err error) {
85115

86116
file, err := os.Open(filePath)
@@ -103,17 +133,17 @@ func parseContent(content string, treeCursor *sitter.TreeCursor, filePath string
103133
var annotationParser a.Parser
104134

105135
fileData.Metadata = getMetadata(content, treeCursor, &annotationParser)
106-
functions := getFunctionNodes(content, treeCursor, &annotationParser)
136+
funcNodes := getFunctionNodes(content, treeCursor, &annotationParser)
107137

108-
for _, function := range functions {
138+
for _, funcNode := range funcNodes {
109139

110-
behaviors := findBehaviorsFromNode(content, function.Node)
140+
behaviors := findBehaviorsFromNode(content, funcNode.Node)
111141
var callExpressions []string = nil
112142
if len(behaviors) > 0 {
113-
callExpressions = findCallExprFromNode(content, function.Node)
143+
callExpressions = findCallExprFromNode(content, funcNode.Node)
114144
}
115145

116-
fileData.Functions = append(fileData.Functions, makeCollectorScenario(filePath, function.Name, behaviors, callExpressions))
146+
fileData.Functions = append(fileData.Functions, makeCollectorScenario(filePath, funcNode.Function.Name, behaviors, callExpressions))
117147

118148
}
119149

@@ -163,10 +193,11 @@ func getMetadata(content string, treeCursor *sitter.TreeCursor, parser *a.Parser
163193
return &meta
164194
}
165195

166-
func getFunctionNodes(content string, treeCursor *sitter.TreeCursor, parser *a.Parser) (funcAnnoPair []struct {
167-
Node *sitter.Node
168-
Name string
169-
}) {
196+
// getFunctionNodes method extracts functions and methods.
197+
// Function can have 2 types: function_declaration (for example contructor)
198+
// and method_declaration (can be exported and unexported).
199+
// Return value is a slice of FunctionAnnotationNode, where each node holds function's annotation.
200+
func getFunctionNodes(content string, treeCursor *sitter.TreeCursor, parser *a.Parser) (funcAnnoPair []FunctionAnnotationNode) {
170201

171202
numChildsRootNode := treeCursor.CurrentNode().ChildCount()
172203
node := &sitter.Node{}
@@ -203,18 +234,48 @@ func getFunctionNodes(content string, treeCursor *sitter.TreeCursor, parser *a.P
203234
continue
204235
}
205236

206-
funcAnnoPair = append(funcAnnoPair, struct {
207-
Node *sitter.Node
208-
Name string
209-
}{
237+
funcAnnoPair = append(funcAnnoPair, FunctionAnnotationNode{
210238
Node: node,
211-
Name: funcName,
239+
Function: c.FunctionAnnotation{
240+
Name: funcName,
241+
},
212242
})
213243

214244
node = nil
215245
funcName = ""
216246
isIgnored = false
217247
}
248+
/*
249+
Take a look at this function for example:
250+
*********
251+
func (e *ProofEventStream) ListenProofEvent(
252+
ctx context.Context,
253+
policy *types2.ProofRegisterPolicy)
254+
(<-chan *types2.RequestEvent, error){...}
255+
*********
256+
child.Child(1) is a first part of function declaration (e *ProofEventStream)
257+
child.Child(2): field_identifier 'ListenProofEvent'
258+
child.Child(3): parameter_list: (ctx context.Context, policy *types2.ProofRegisterPolicy)
259+
child.Child(4): parameter_list: (<-chan *types2.RequestEvent, error)
260+
*/
261+
if child.Type() == string(METHOD_DECLARATION) {
262+
funcName = content[child.Child(2).StartByte():child.Child(2).EndByte()]
263+
params := ""
264+
returnValues := ""
265+
if child.Child(1).Type() == string(PARAMETER_LIST) {
266+
params = content[child.Child(3).StartByte():child.Child(3).EndByte()]
267+
returnValues = content[child.Child(4).StartByte():child.Child(4).EndByte()]
268+
}
269+
funcAnnoPair = append(funcAnnoPair, FunctionAnnotationNode{
270+
Node: child,
271+
Function: c.FunctionAnnotation{
272+
Name: funcName,
273+
Public: isPublic(funcName),
274+
InputParams: params,
275+
ReturnValues: returnValues,
276+
},
277+
})
278+
}
218279
prevNode = child
219280
}
220281
}
@@ -301,3 +362,21 @@ func makeID(filePath string, funcName string, behavior string) string {
301362
hash := md5.Sum([]byte(fmt.Sprintf("%s_%s_%s", filePath, funcName, behavior)))
302363
return string(hex.EncodeToString(hash[:]))
303364
}
365+
366+
// parseContentForFunctions accepts a content which represents whole golang file as a string,
367+
// parses it and returns a slice of function annotations (including exported and unexported ones).
368+
func parseContentForFunctions(content string, cursor *sitter.TreeCursor) ([]c.FunctionAnnotation, error) {
369+
var annotationParser a.Parser
370+
371+
var fnsAnno []c.FunctionAnnotation
372+
funcNodes := getFunctionNodes(content, cursor, &annotationParser)
373+
for _, funcNode := range funcNodes {
374+
fnsAnno = append(fnsAnno, funcNode.Function)
375+
}
376+
377+
return fnsAnno, nil
378+
}
379+
380+
func isPublic(funcName string) bool {
381+
return unicode.IsUpper(rune(funcName[0]))
382+
}

test-crawler/go.mod

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ module testsuites
22

33
go 1.17
44

5-
require github.com/smacker/go-tree-sitter v0.0.0-20211116060328-db7fde9b5e82
5+
require (
6+
github.com/smacker/go-tree-sitter v0.0.0-20211116060328-db7fde9b5e82
7+
github.com/stretchr/testify v1.4.0
8+
)
69

7-
require gopkg.in/yaml.v2 v2.4.0 // indirect
10+
require (
11+
github.com/davecgh/go-spew v1.1.0 // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v2 v2.4.0
14+
)

test-crawler/go.sum

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ github.com/smacker/go-tree-sitter v0.0.0-20211116060328-db7fde9b5e82/go.mod h1:E
77
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
88
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
99
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1011
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11-
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
1212
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1313
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
1414
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

test-crawler/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func main() {
2626

2727
config := NewConfig()
2828

29+
crawlRepoBehaviorsAndSaveToJSON(config)
30+
}
31+
32+
func crawlRepoBehaviorsAndSaveToJSON(config Config) {
2933
allFiles := make(map[c.FileID]*c.TestFile)
3034
var fileFunctions []c.Function
3135

@@ -73,6 +77,29 @@ func main() {
7377
Save(result, config.OutputMode, config.OutputDir, config.IndentJSON)
7478
}
7579

80+
// crawlSingleFileForMethods accepts path of single go file,
81+
// and prints extracted methods out of it.
82+
func crawlSingleFileForMethods(path string) {
83+
fns, err := extractPublicMethodsFromFile(context.Background(), path)
84+
if err != nil {
85+
fmt.Print(err)
86+
os.Exit(1)
87+
}
88+
for _, fn := range fns {
89+
fmt.Printf(
90+
"name %s : public %v : params: %s : return values : %s\n",
91+
fn.Name,
92+
fn.Public,
93+
fn.InputParams,
94+
fn.ReturnValues,
95+
)
96+
}
97+
}
98+
99+
func extractPublicMethodsFromFile(ctx context.Context, filePath string) ([]c.FunctionAnnotation, error) {
100+
return ex.GetExportedFunctions(ctx, filePath)
101+
}
102+
76103
func linkFiles(flist []c.Function) (links [][]FnLink) {
77104

78105
functions := make(map[string]c.Function)

test-crawler/mocks/event.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package mocks
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"time"
7+
)
8+
9+
type Event struct {
10+
ID string `json:"id"`
11+
}
12+
13+
func NewEvent() *Event {
14+
return &Event{
15+
ID: time.Now().String(),
16+
}
17+
}
18+
19+
// HelloEvent simple method that just formats message.
20+
func (e *Event) HelloEvent() string {
21+
return fmt.Sprintf("Simple HelloEvent")
22+
}
23+
24+
// HelloEventWithParameter accepts one param that got formated in message.
25+
func (e *Event) HelloEventWithParameter(param string) (string, error) {
26+
if param == "" {
27+
return "", errors.New("no param provided")
28+
}
29+
30+
return fmt.Sprintf("HelloEventWithParameter: %v", param), nil
31+
}

0 commit comments

Comments
 (0)