Skip to content

Commit

Permalink
feat: implement diff parser (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
twelvelabs committed Feb 25, 2023
1 parent d7aa56d commit 1cdf391
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .stylist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ processors:
- preset: cspell
- preset: gitleaks
- preset: golangci-lint
- preset: gofmt
- preset: markdownlint
# - preset: hadolint
# - preset: shellcheck
# - preset: shfmt
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/imdario/mergo v0.3.13
github.com/owenrumney/go-sarif v1.1.1
github.com/sirupsen/logrus v1.9.0
github.com/sourcegraph/go-diff v0.7.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
Expand Down Expand Up @@ -101,8 +102,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
Expand Down Expand Up @@ -192,6 +197,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
2 changes: 1 addition & 1 deletion internal/stylist/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type OutputType string

// OutputFormat represents how to parse command output.
//
// ENUM(json, none, regexp, sarif).
// ENUM(diff, json, none, regexp, sarif).
type OutputFormat string

// ResultLevel represents the severity level of the result.
Expand Down
4 changes: 4 additions & 0 deletions internal/stylist/enums_enum.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions internal/stylist/output_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"io"
"regexp"
"strings"

"github.com/owenrumney/go-sarif/sarif"
"github.com/sourcegraph/go-diff/diff"
"github.com/tidwall/gjson"

"github.com/twelvelabs/stylist/internal/fsutils"
Expand All @@ -29,6 +31,8 @@ type OutputParser interface {
// NewOutputParser returns the appropriate parser for the given output type.
func NewOutputParser(format OutputFormat) OutputParser { //nolint:ireturn
switch format {
case OutputFormatDiff:
return &DiffOutputParser{}
case OutputFormatJson:
return &JSONOutputParser{}
case OutputFormatNone:
Expand All @@ -42,6 +46,76 @@ func NewOutputParser(format OutputFormat) OutputParser { //nolint:ireturn
}
}

/*
* DiffOutputParser
**/

// DiffOutputParser parses unified diffs.
type DiffOutputParser struct {
}

// Parse parses command output into a slice of results.
func (p *DiffOutputParser) Parse(output CommandOutput, mapping ResultMapping) ([]*Result, error) {
// Read the content.
buf, err := io.ReadAll(output.Content)
if err != nil {
return nil, err
}
content := ansiRegexp.ReplaceAll(buf, []byte(""))
if len(content) == 0 {
return nil, nil // nothing to parse
}

// Parse.
diffs, err := diff.ParseMultiFileDiff(content)
if err != nil {
return nil, fmt.Errorf("invalid diff: %w", err)
}

// Map the diffs to a slice of `Result` structs.
results := []*Result{}
for _, d := range diffs {
var startLine int
var contextLines []string

if len(d.Hunks) > 0 {
// Hunks often start w/ a few preceding context lines.
// Calculate the actual start of the changeset.
changeStart := 0
bodyLines := bytes.Split(d.Hunks[0].Body, []byte{'\n'})
for _, line := range bodyLines {
if len(line) > 0 && (line[0] == '+' || line[0] == '-') {
break
}
changeStart++
}
startLine = int(d.Hunks[0].OrigStartLine) + changeStart

// Printing just the hunks (vs full diff) so we don't have
// redundant file names at the top of the context.
hunks, _ := diff.PrintHunks(d.Hunks)
contextLines = strings.Split(strings.TrimSuffix(string(hunks), "\n"), "\n")
}

result := &Result{
Level: ResultLevelError,
Location: ResultLocation{
Path: d.NewName,
StartLine: startLine,
},
Rule: ResultRule{
ID: "diff",
Name: "diff",
Description: "Formatting error",
},
ContextLines: contextLines,
}
results = append(results, result)
}

return results, nil
}

/*
* JSONOutputParser
**/
Expand Down
99 changes: 99 additions & 0 deletions internal/stylist/output_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,105 @@ func TestNewOutputParser(t *testing.T) {
})
}

func TestDiffOutputParser_Parse(t *testing.T) {
tests := []struct {
desc string
content io.Reader
expected []*Result
err string
}{
{
desc: "returns an empty slice when no content",
content: bytes.NewBufferString(""),
expected: nil,
err: "",
},
{
desc: "returns an empty slice when not a valid diff",
content: bytes.NewBufferString("not a diff"),
expected: []*Result{},
err: "",
},
{
desc: "parses diffs",
content: mustOpenFile("testdata/output/shfmt.diff"),
expected: []*Result{
{
Level: ResultLevelError,
Location: ResultLocation{
Path: "bin/command.sh",
StartLine: 4,
},
Rule: ResultRule{
ID: "diff",
Name: "diff",
Description: "Formatting error",
},
ContextLines: []string{
"@@ -1,10 +1,10 @@",
" #!/usr/bin/env bash",
" set -o errexit -o errtrace -o nounset -o pipefail",
"",
"-",
"-if [",
"+if",
"+ [",
" $foo == \"bar\"",
"-]",
"+ ]",
" then",
" echo \"lol\"",
" fi",
},
},
{
Level: ResultLevelError,
Location: ResultLocation{
Path: "bin/entrypoint.sh",
StartLine: 19,
},
Rule: ResultRule{
ID: "diff",
Name: "diff",
Description: "Formatting error",
},
ContextLines: []string{
"@@ -16,8 +16,8 @@",
" # fix permissions",
" sudo chown -R app:app \\",
" /app \\",
"- /home/app \\",
"- /run/host-services/ssh-auth.sock",
"+ /home/app \\",
"+ /run/host-services/ssh-auth.sock",
" fi",
"",
},
},
},
err: "",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
actual, err := (&DiffOutputParser{}).Parse(
CommandOutput{
Content: tt.content,
},
ResultMapping{},
)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.err)
}

assert.Equal(t, tt.expected, actual)
})
}
}

func TestJSONOutputParser_Parse(t *testing.T) {
file, err := os.Open("testdata/output/shellcheck.json")
assert.NoError(t, err)
Expand Down
34 changes: 34 additions & 0 deletions internal/stylist/presets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ golangci-lint:
output: stdout
format: none

gofmt:
name: gofmt
preset: gofmt
tags: []
includes:
- "**/*.go"
check:
command: "gofmt -d"
input: variadic
output: stdout
format: diff
fix:
command: "gofmt -w"
input: variadic
output: stdout
format: none

hadolint:
name: hadolint
preset: hadolint
Expand Down Expand Up @@ -141,3 +158,20 @@ shellcheck:
rule_name: "SC{{ .code }}"
rule_description: "{{ .message }}"
rule_uri: "https://www.shellcheck.net/wiki/SC{{ .code }}"

shfmt:
name: shfmt
preset: shfmt
tags: []
includes:
- "**/*.{bash,sh,shell}"
check:
command: "shfmt --diff"
input: variadic
output: stdout
format: diff
fix:
command: "shfmt --write"
input: variadic
output: stdout
format: none
28 changes: 28 additions & 0 deletions internal/stylist/testdata/output/shfmt.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--- bin/command.sh.orig
+++ bin/command.sh
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
set -o errexit -o errtrace -o nounset -o pipefail

-
-if [
+if
+ [
$foo == "bar"
-]
+ ]
then
echo "lol"
fi
--- bin/entrypoint.sh.orig
+++ bin/entrypoint.sh
@@ -16,8 +16,8 @@
# fix permissions
sudo chown -R app:app \
/app \
- /home/app \
- /run/host-services/ssh-auth.sock
+ /home/app \
+ /run/host-services/ssh-auth.sock
fi

0 comments on commit 1cdf391

Please sign in to comment.