Skip to content
This repository has been archived by the owner on Mar 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1 from manucorporat/add-lib
Browse files Browse the repository at this point in the history
Adds client API + simple cli
  • Loading branch information
abeaumont authored Aug 24, 2017
2 parents 6089e18 + ce04323 commit 452bc73
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 2 deletions.
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: go
sudo: false
go:
- 1.6.x
- 1.7.x
- 1.8.x
- master

install:
- go get .

script:
- go test ./...
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
# client-go
Babelfish Go client
# go-client for babelfish

Go-client allows to easily comunicate with the Babelfish server to parse the any source code.
It's also integrated with libuast so it is possible to run xpath queries agains the UAST in an idiomatic way.


## Installation

### Dependencies

- libuast ( https://github.com/bblfsh/libuast )

### Go library

```
go get github.com/bblfsh/client-go
```
65 changes: 65 additions & 0 deletions bindings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#ifndef _CLIENT_GO_BINDINGS_H_
#define _CLIENT_GO_BINDINGS_H_

#include <libuast/uast.h>

extern char* goGetInternalType(uintptr_t);
extern int goGetPropertiesSize(uintptr_t);
extern char* goGetToken(uintptr_t);
extern int goGetChildrenSize(uintptr_t);
extern uintptr_t goGetChild(uintptr_t, int);
extern int goGetRolesSize(uintptr_t);
extern uint16_t goGetRole(uintptr_t, int);

static const char *get_internal_type(const void *node) {
return goGetInternalType((uintptr_t)node);
}

static const char *get_token(const void *node) {
return goGetToken((uintptr_t)node);
}

static int get_children_size(const void *node) {
return goGetChildrenSize((uintptr_t)node);
}

static void *get_child(const void *data, int index) {
return (void*)goGetChild((uintptr_t)data, index);
}

static int get_roles_size(const void *node) {
return goGetRolesSize((uintptr_t)node);
}

static uint16_t get_role(const void *node, int index) {
return goGetRole((uintptr_t)node, index);
}

static node_api *api;
static find_ctx *ctx;

static void create_go_node_api() {
api = new_node_api((node_iface){
.internal_type = get_internal_type,
.token = get_token,
.children_size = get_children_size,
.children = get_child,
.roles_size = get_roles_size,
.roles = get_role,
});
ctx = new_find_ctx();
}

static int _api_find(uintptr_t node_ptr, const char *query) {
return node_api_find(api, ctx, (void*)node_ptr, query);
}

static int _api_get_len() {
return find_ctx_get_len(ctx);
}

static uintptr_t _api_get_result(unsigned int i) {
return (uintptr_t)find_ctx_get(ctx, i);
}

#endif // _CLIENT_GO_BINDINGS_H_
40 changes: 40 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"flag"
"fmt"

"github.com/bblfsh/client-go"
)

var endpoint = flag.String("e", "localhost:9432", "endpoint of the babelfish server")
var filename = flag.String("f", "", "file to parse")
var query = flag.String("q", "", "xpath expression")

func main() {
flag.Parse()
if *filename == "" {
fmt.Println("filename was not provided. Use the -f flag")
return
}

client, err := bblfsh.NewBblfshClient(*endpoint)
if err != nil {
panic(err)
}

res, err := client.NewParseRequest().ReadFile(*filename).Do()
if err != nil {
panic(err)
}

if *query == "" {
fmt.Println(res.UAST)
} else {
results, _ := bblfsh.Find(res.UAST, *query)
for i, node := range results {
fmt.Println("-", i+1, "----------------------")
fmt.Println(node.String())
}
}
}
28 changes: 28 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package bblfsh

import (
"time"

"github.com/bblfsh/sdk/protocol"
"google.golang.org/grpc"
)

// BblfshClient holds the public client API to interact with the babelfish server.
type BblfshClient struct {
client protocol.ProtocolServiceClient
}

// NewBblfshClient returns a new babelfish client given a server endpoint
func NewBblfshClient(endpoint string) (*BblfshClient, error) {
conn, err := grpc.Dial(endpoint, grpc.WithTimeout(time.Second*2), grpc.WithInsecure())
if err != nil {
return nil, err
}
return &BblfshClient{
client: protocol.NewProtocolServiceClient(conn),
}, nil
}

func (c *BblfshClient) NewParseRequest() *ParseRequest {
return &ParseRequest{client: c}
}
22 changes: 22 additions & 0 deletions cstring_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package bblfsh

// #include <stdlib.h>
import "C"
import "unsafe"

type cstringPool struct {
pointers []unsafe.Pointer
}

func (pool *cstringPool) getCstring(str string) *C.char {
ptr := C.CString(str)
pool.pointers = append(pool.pointers, unsafe.Pointer(ptr))
return ptr
}

func (*cstringPool) release() {
for _, ptr := range pool.pointers {
C.free(ptr)
}
pool.pointers = pool.pointers[0:0]
}
93 changes: 93 additions & 0 deletions libuast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package bblfsh

import (
"errors"
"runtime/debug"
"sync"
"unsafe"

"github.com/bblfsh/sdk/uast"
)

// #cgo CFLAGS: -I/usr/local/include -I/usr/local/include/libxml2 -I/usr/include -I/usr/include/libxml2
// #cgo LDFLAGS: -luast -lxml2
// #include "bindings.h"
import "C"

var findMutex sync.Mutex
var pool cstringPool

func init() {
C.create_go_node_api()
}

func nodeToPtr(node *uast.Node) C.uintptr_t {
return C.uintptr_t(uintptr(unsafe.Pointer(node)))
}

func ptrToNode(ptr C.uintptr_t) *uast.Node {
return (*uast.Node)(unsafe.Pointer(uintptr(ptr)))
}

// Find takes a `*uast.Node` and a xpath query and filters the tree,
// returning the list of nodes that satisfy the given query.
// Find is thread-safe but not current by an internal global lock.
func Find(node *uast.Node, xpath string) ([]*uast.Node, error) {
// Find is not thread-safe bacuase of the underlining C API
findMutex.Lock()
defer findMutex.Unlock()

// convert xpath string to a NULL-terminated c string
cquery := pool.getCstring(xpath)

// Make sure we release the pool of strings
defer pool.release()

// stop GC
gcpercent := debug.SetGCPercent(-1)
defer debug.SetGCPercent(gcpercent)

ptr := nodeToPtr(node)
if C._api_find(ptr, cquery) != 0 {
return nil, errors.New("error: node_api_find() failed")
}

nu := int(C._api_get_len())
results := make([]*uast.Node, nu)
for i := 0; i < nu; i++ {
results[i] = ptrToNode(C._api_get_result(C.uint(i)))
}
return results, nil
}

//export goGetInternalType
func goGetInternalType(ptr C.uintptr_t) *C.char {
return pool.getCstring(ptrToNode(ptr).InternalType)
}

//export goGetToken
func goGetToken(ptr C.uintptr_t) *C.char {
return pool.getCstring(ptrToNode(ptr).Token)
}

//export goGetChildrenSize
func goGetChildrenSize(ptr C.uintptr_t) C.int {
return C.int(len(ptrToNode(ptr).Children))
}

//export goGetChild
func goGetChild(ptr C.uintptr_t, index C.int) C.uintptr_t {
child := ptrToNode(ptr).Children[int(index)]
return nodeToPtr(child)
}

//export goGetRolesSize
func goGetRolesSize(ptr C.uintptr_t) C.int {
return C.int(len(ptrToNode(ptr).Roles))
}

//export goGetRole
func goGetRole(ptr C.uintptr_t, index C.int) C.uint16_t {
role := ptrToNode(ptr).Roles[int(index)]
return C.uint16_t(role)
}
71 changes: 71 additions & 0 deletions parse_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package bblfsh

import (
"context"
"errors"
"io/ioutil"
"path"

"github.com/bblfsh/sdk/protocol"
)

// ParseRequest is a placeholder for the parse requests performed by the library
type ParseRequest struct {
internal protocol.ParseRequest
client *BblfshClient
err error
}

// Language sets the language of the given source file to parse.
func (req *ParseRequest) Language(lan string) *ParseRequest {
req.internal.Language = lan
return req
}

// ReadFile loads a file given a local path and sets the content and the filename of the request.
func (req *ParseRequest) ReadFile(filepath string) *ParseRequest {
data, err := ioutil.ReadFile(filepath)
if err != nil {
req.err = err
} else {
req.internal.Content = string(data)
req.internal.Filename = path.Base(filepath)
}
return req
}

// Content sets the content of the parse request. It should be the source code that wants to be parsed.
func (req *ParseRequest) Content(content string) *ParseRequest {
req.internal.Content = content
return req
}

// Filename sets the filename of the content.
func (req *ParseRequest) Filename(filename string) *ParseRequest {
req.internal.Filename = filename
return req
}

// Encoding sets the text encoding of the content.
func (req *ParseRequest) Encoding(encoding protocol.Encoding) *ParseRequest {
req.internal.Encoding = encoding
return req
}

// Do performs the actual parsing by serializaing the request,
// sending it to the babelfish and waiting for the response (UAST tree).
func (req *ParseRequest) Do() (*protocol.ParseResponse, error) {
return req.DoWithContext(context.Background())
}

// DoWithContext does the same as Do(), but sopporting cancellation by the use of
// Go contexts.
func (req *ParseRequest) DoWithContext(ctx context.Context) (*protocol.ParseResponse, error) {
if req.err != nil {
return nil, req.err
}
if req.client == nil {
return nil, errors.New("request is clientless")
}
return req.client.client.Parse(ctx, &req.internal)
}
27 changes: 27 additions & 0 deletions parse_request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package bblfsh

import "testing"
import "github.com/bblfsh/sdk/protocol"
import "github.com/stretchr/testify/require"

func TestParseRequestConfiguration(t *testing.T) {
req := ParseRequest{}
req.Filename("file.py").Language("python").Encoding(protocol.UTF8).Content("a=b+c")

require.Equal(t, "file.py", req.internal.Filename)
require.Equal(t, "python", req.internal.Language)
require.Equal(t, protocol.UTF8, req.internal.Encoding)
require.Equal(t, "a=b+c", req.internal.Content)
}

func TestParseRequestClientlessError(t *testing.T) {
req := ParseRequest{}
_, err := req.Content("a=b+c").Language("python").Do()
require.Errorf(t, err, "request is clientless")
}

func TestParseRequestReadFileError(t *testing.T) {
req := ParseRequest{}
_, err := req.ReadFile("NO_EXISTS").Do()
require.Errorf(t, err, "open NO_EXISTS: no such file or directory")
}

0 comments on commit 452bc73

Please sign in to comment.