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

feat(*): add SDK core #1

Merged
merged 26 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e095afe
feat(*): add SDK core
burntcarrot Jun 6, 2022
d768047
tests(*): integration tests using tree-sitter
burntcarrot Jun 7, 2022
5ad1b10
ci: add golangci-lint configuration
burntcarrot Jun 7, 2022
ceef642
fix(sdk/utils): sanitize markdown
burntcarrot Jun 7, 2022
bd4a0fe
refactor(*): refactor SDK and add tests
burntcarrot Jun 11, 2022
318ccaa
tests(*): test TOML generation
burntcarrot Jun 12, 2022
a0928fa
fix(utils): check return value while creating directory
burntcarrot Jun 12, 2022
da20033
refactor(*): refactor SDK and add tests
burntcarrot Jun 12, 2022
7ce40b7
docs(*): add guide for writing an analyzer
burntcarrot Jun 12, 2022
3a0c99a
chore: add README
burntcarrot Jun 12, 2022
f7d3b7e
refactor(*): refactor SDK and cleanup
burntcarrot Jun 13, 2022
6bd2540
fix(analyzers): use REPO_ROOT and TOOLBOX_PATH environment variables
burntcarrot Jun 14, 2022
a0ce3b1
chore: add gitignore
burntcarrot Jun 14, 2022
7fc2940
docs(*): update guide
burntcarrot Jun 14, 2022
adfb69d
docs(*): add guide for writing CSS analyzer
burntcarrot Jun 14, 2022
a101b86
tests(analysistest): add integration test for CSS
burntcarrot Jun 14, 2022
bb2135c
chore: fix DeepSource issues
burntcarrot Jun 14, 2022
180769b
refactor(*): refactor tests and add guides
burntcarrot Jun 15, 2022
e5a7554
docs(guides): fix TOML fields for issues.toml
burntcarrot Jun 15, 2022
a74d7ce
ci: install csslint
burntcarrot Jun 15, 2022
e9e4724
feat(analysistest): add all tree-sitter languages
burntcarrot Jun 15, 2022
f7d38a1
Apply suggestions from code review
sourya-deepsource Jun 15, 2022
0513907
tests(*): add unit tests
burntcarrot Jun 17, 2022
1034696
chore: fix merge conflicts
burntcarrot Jun 17, 2022
1112721
refactor(*): allow issue parsing via structs
burntcarrot Jun 17, 2022
ad43322
refactor(*): pass env vars through params
burntcarrot Jun 20, 2022
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
18 changes: 18 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Lint
on: [push, pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.17.x]
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.43
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
run:
burntcarrot marked this conversation as resolved.
Show resolved Hide resolved
skip-dirs:
- sdk/triggers
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/deepsourcelabs/deepsource-go

go 1.17

require (
github.com/BurntSushi/toml v1.1.0
github.com/microcosm-cc/bluemonday v1.0.18
github.com/smacker/go-tree-sitter v0.0.0-20220421092837-ec55f7cfeaf4
github.com/yuin/goldmark v1.4.12
)

require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
)
29 changes: 29 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smacker/go-tree-sitter v0.0.0-20220421092837-ec55f7cfeaf4 h1:UFOHRX5nrxNCVORhicjy31nzSVt9rEjf/YRcx2Dc3MM=
github.com/smacker/go-tree-sitter v0.0.0-20220421092837-ec55f7cfeaf4/go.mod h1:EiUuVMUfLQj8Sul+S8aKWJwQy7FRYnJCO2EWzf8F5hk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
90 changes: 90 additions & 0 deletions sdk/sdk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package sdk

import (
"bytes"
"log"
"os/exec"

"github.com/deepsourcelabs/deepsource-go/sdk/types"
"github.com/deepsourcelabs/deepsource-go/sdk/utils"
"github.com/deepsourcelabs/deepsource-go/sdk/utils/processors"
)

// The main analyzer interface. Analyzers must implement Run and Processor.
type Analyzer interface {
Run() error
Processor(result interface{}) (types.AnalysisReport, error)
}

// CLIAnalyzer is used for creating an analyzer.
type CLIAnalyzer struct {
Name string
Command string
Args []string
ExportOpts ExportOpts
}

type ExportOpts struct {
Path string
Type string
}

// Run executes the analyzer and streams the output to the processor.
func (a *CLIAnalyzer) Run() error {
cmd := exec.Command(a.Command, a.Args...)

// store the process's standard output in a buffer
var out bytes.Buffer
cmd.Stdout = &out

// TODO: handle exit status 1
_ = cmd.Run()

// fetch report from processor
report, err := a.Processor(out.String())
if err != nil {
return err
}

// save report to file
err = utils.SaveReport(report, a.ExportOpts.Path, a.ExportOpts.Type)
if err != nil {
return err
}

return nil
}

// Processor takes the analyzer output and generates a report.
func (a *CLIAnalyzer) Processor(result interface{}) (types.AnalysisReport, error) {
var report types.AnalysisReport
var err error

// use custom processors for each major linter/analyzer
switch a.Name {
case "staticcheck":
report, err = processors.StaticCheck(result)
default:
// if a match is not found, the user needs to implement a processor
log.Printf("custom processor needs to be implemented for %s.\n", a.Name)
}

return report, err
}

// GenerateTOML helps in generating TOML files for each issue from a JSON file.
func (a *CLIAnalyzer) GenerateTOML(filename string, rootDir string) error {
// fetch parsed issues
issues, err := utils.ParseIssues(filename)
if err != nil {
return err
}

// generate TOML files
err = utils.BuildTOML(issues, rootDir)
if err != nil {
return err
}

return nil
}
53 changes: 53 additions & 0 deletions sdk/sdk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package sdk

import (
"encoding/json"
"os"
"testing"

"github.com/deepsourcelabs/deepsource-go/sdk/triggers"
"github.com/deepsourcelabs/deepsource-go/sdk/types"
)

func TestStaticCheck(t *testing.T) {
t.Run("verify staticcheck", func(t *testing.T) {
a := CLIAnalyzer{
Name: "staticcheck",
Command: "staticcheck",
Args: []string{"-f", "json", "./triggers/staticcheck/..."},
ExportOpts: ExportOpts{
Path: "triggers/staticcheck/issues.json",
Type: "json",
},
}

err := a.Run()
if err != nil {
t.Error(err)
}

// read the generated report
reportContent, err := os.ReadFile("triggers/staticcheck/issues.json")
if err != nil {
t.Error(err)
}

var report types.AnalysisReport
err = json.Unmarshal(reportContent, &report)
if err != nil {
t.Error(err)
}

// do a verification check for the generated report
err = triggers.Verify(report, "triggers/staticcheck/staticcheck.go")
if err != nil {
t.Error(err)
}

// cleanup after test
err = os.Remove("triggers/staticcheck/issues.json")
if err != nil {
t.Error(err)
}
})
}
119 changes: 119 additions & 0 deletions sdk/triggers/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package triggers

import (
"context"
"errors"
"os"
"path/filepath"
"regexp"
"strings"

sitter "github.com/smacker/go-tree-sitter"
"github.com/smacker/go-tree-sitter/golang"

"github.com/deepsourcelabs/deepsource-go/sdk/types"
)

// ParsedIssue represents an issue parsed using tree-sitter.
type ParsedIssue struct {
IssueCode string
Line int
}

// Verify compares the generated report and parsed issues using tree-sitter.
func Verify(report types.AnalysisReport, filename string) error {
parser := sitter.NewParser()

// get language
lang, err := getLanguage(filename)
if err != nil {
return err
}
parser.SetLanguage(lang)

// read report
content, err := os.ReadFile(filename)
if err != nil {
return err
}

// generate tree
ctx := context.Background()
tree, err := parser.ParseCtx(ctx, nil, content)
if err != nil {
return err
}

// create a query for fetching comments
queryStr := "(comment) @comment"
query, err := sitter.NewQuery([]byte(queryStr), lang)
if err != nil {
return err
}

// execute query on root node
qc := sitter.NewQueryCursor()
n := tree.RootNode()
qc.Exec(query, n)
defer qc.Close()

var parsedIssues []ParsedIssue

// iterate over matches
for {
m, ok := qc.NextMatch()
if !ok {
break
}

for _, c := range m.Captures {
node := c.Node
nodeContent := node.Content(content)

// check if the comment contains raise annotation
if strings.Contains(nodeContent, "raise") {
// find match using expression
exp := regexp.MustCompile(`.+ raise: `)
submatches := exp.FindStringSubmatch(nodeContent)

if len(submatches) != 0 {
substrings := exp.Split(nodeContent, -1)
if len(substrings) > 1 {
issueCodes := strings.Split(substrings[1], ",")
// add issue to parsedIssues
for _, issueCode := range issueCodes {
parsedIssue := ParsedIssue{IssueCode: strings.TrimSpace(issueCode), Line: int(node.StartPoint().Row) + 1}
parsedIssues = append(parsedIssues, parsedIssue)
}
}
}
}
}
}

// if number of issues don't match, exit early.
if len(parsedIssues) != len(report.Issues) {
return errors.New("mismatch between the number of reported issues and parsed issues")
}

// compare the report's issues and parsed issues
for i, issue := range report.Issues {
if (parsedIssues[i].Line != issue.Location.Position.Begin.Line) && (parsedIssues[i].IssueCode != issue.IssueCode) {
return errors.New("mismatch between parsed issue and report issue")
}
}

return nil
}

// getLanguage is a helper for fetching tree-sitter language based on the file's extension.
func getLanguage(filename string) (*sitter.Language, error) {
extension := filepath.Ext(filename)

switch extension {
case ".go":
return golang.GetLanguage(), nil
default:
return nil, errors.New("language not supported")
}
}
7 changes: 7 additions & 0 deletions sdk/triggers/staticcheck/staticcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pkg

import "fmt"

func trigger() { // raise: U1000
fmt.Sprint("trigger") // raise: SA4017, S1039
}
48 changes: 48 additions & 0 deletions sdk/types/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package types

type Coordinate struct {
Line int `json:"line"`
Column int `json:"column"`
}

type Position struct {
Begin Coordinate `json:"begin"`
End Coordinate `json:"end"`
}

type Location struct {
Path string `json:"path"`
Position Position `json:"position"`
}

type SourceCode struct {
Rendered []byte `json:"rendered"`
}

type ProcessedData struct {
SourceCode SourceCode `json:"source_code,omitempty"`
}

type Issue struct {
IssueCode string `json:"issue_code"`
IssueText string `json:"issue_text"`
Location Location `json:"location"`
ProcessedData ProcessedData `json:"processed_data,omitempty"`
}

// Location of an issue
type IssueLocation struct {
Path string `json:"path"`
Position Position `json:"position"`
}

type AnalysisError struct {
HMessage string `json:"hmessage"`
Level int `json:"level"`
}

type AnalysisReport struct {
Issues []Issue `json:"issues"`
Errors []AnalysisError `json:"errors"`
ExtraData interface{} `json:"extra_data"`
}
Loading