Skip to content
Merged
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
50 changes: 38 additions & 12 deletions internal/lints/default_golangci.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lints

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -48,21 +49,17 @@ func (golangciLintRule) CheckPackage(ctx context.Context, pctx *rule.PackageCont
}

cmd := exec.CommandContext(ctx, "golangci-lint", "run", "--config=./.golangci.yml", "--out-format=json", target)
output, _ := cmd.CombinedOutput()
var stderr bytes.Buffer
cmd.Stderr = &stderr
stdout, _ := cmd.Output()

var golangciResult golangciOutput

// golangci-lint occasionally prints non-JSON output for files that
// contain gno package imports (p/demo, r/demo, std). When the
// output fails to decode we surface the error to the engine so it
// logs Warn via WithLogger; partial Issues are not returned because
// any successful decode would mean no error.
if err := json.Unmarshal(output, &golangciResult); err != nil {
return nil, fmt.Errorf("decode golangci-lint output for %s: %w", target, err)
result, err := decodeGolangciOutput(stdout)
if err != nil {
return nil, fmt.Errorf("decode golangci-lint output for %s: %w (stderr: %s)", target, err, snippet(stderr.Bytes(), 200))
}

issues := make([]tt.Issue, 0, len(golangciResult.Issues))
for _, gi := range golangciResult.Issues {
issues := make([]tt.Issue, 0, len(result.Issues))
for _, gi := range result.Issues {
if !pctx.InScope(gi.Pos.Filename) {
continue
}
Expand All @@ -79,3 +76,32 @@ func (golangciLintRule) CheckPackage(ctx context.Context, pctx *rule.PackageCont

return issues, nil
}

// decodeGolangciOutput parses golangci-lint stdout, treating empty
// input as "no issues". golangci-lint exits without writing JSON
// when go/packages fails to load the target — common for .gno-only
// directories whose temp .go file imports gno.land/... paths the
// stock Go loader cannot resolve — and erroring there would log a
// warn per file across the entire package.
func decodeGolangciOutput(stdout []byte) (golangciOutput, error) {
stdout = bytes.TrimSpace(stdout)
var result golangciOutput
if len(stdout) == 0 {
return result, nil
}
if err := json.Unmarshal(stdout, &result); err != nil {
return result, err
}
return result, nil
}

func snippet(b []byte, n int) string {
b = bytes.TrimSpace(b)
if len(b) == 0 {
return ""
}
if len(b) > n {
b = b[:n]
}
return string(b)
}
69 changes: 69 additions & 0 deletions internal/lints/default_golangci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package lints

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDecodeGolangciOutput(t *testing.T) {
t.Parallel()

t.Run("empty input yields no issues", func(t *testing.T) {
t.Parallel()
for _, in := range [][]byte{nil, []byte(""), []byte(" \n\t ")} {
got, err := decodeGolangciOutput(in)
require.NoError(t, err)
assert.Empty(t, got.Issues)
}
})

t.Run("valid JSON populates issues", func(t *testing.T) {
t.Parallel()
in := []byte(`{"Issues":[{"FromLinter":"errcheck","Text":"unchecked error","Pos":{"Filename":"a.go","Line":12,"Column":3}}]}`)

got, err := decodeGolangciOutput(in)
require.NoError(t, err)
require.Len(t, got.Issues, 1)
assert.Equal(t, "errcheck", got.Issues[0].FromLinter)
assert.Equal(t, "unchecked error", got.Issues[0].Text)
assert.Equal(t, "a.go", got.Issues[0].Pos.Filename)
assert.Equal(t, 12, got.Issues[0].Pos.Line)
assert.Equal(t, 3, got.Issues[0].Pos.Column)
})

t.Run("malformed input returns decode error", func(t *testing.T) {
t.Parallel()
got, err := decodeGolangciOutput([]byte("not json"))
require.Error(t, err)
assert.Empty(t, got.Issues)
})
}

func TestSnippet(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in []byte
n int
want string
}{
{"empty", nil, 10, ""},
{"whitespace only", []byte(" \n\t"), 10, ""},
{"under limit", []byte("hello"), 10, "hello"},
{"trimmed under limit", []byte(" hello\n"), 10, "hello"},
{"over limit truncates after trim", []byte(" abcdefghij"), 5, "abcde"},
{"exactly at limit", []byte("abcde"), 5, "abcde"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := snippet(tc.in, tc.n)
assert.Equal(t, tc.want, got)
assert.LessOrEqual(t, len(got), tc.n)
assert.Equal(t, strings.TrimSpace(got), got)
})
}
}
Loading