Skip to content

Commit

Permalink
lsp: show correct opa error hint links (#728)
Browse files Browse the repository at this point in the history
When regal encounters a parse error, we can sometimes workout the
correct opa errors page to send the user to. This PR makes use of some
existing code that matches errors to help pages.

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 authored May 21, 2024
1 parent 1aa0f50 commit 9d7530a
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 2 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.6.0
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/open-policy-agent/opa v0.64.1
github.com/owenrumney/go-sarif/v2 v2.3.1
Expand All @@ -18,6 +19,7 @@ require (
github.com/sourcegraph/jsonrpc2 v0.2.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRR
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/open-policy-agent/opa v0.64.1 h1:n8IJTYlFWzqiOYx+JiawbErVxiqAyXohovcZxYbskxQ=
Expand Down Expand Up @@ -188,6 +190,8 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
Expand Down
15 changes: 13 additions & 2 deletions internal/lsp/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/styrainc/regal/internal/lsp/types"
rparse "github.com/styrainc/regal/internal/parse"
"github.com/styrainc/regal/pkg/config"
"github.com/styrainc/regal/pkg/hints"
"github.com/styrainc/regal/pkg/linter"
"github.com/styrainc/regal/pkg/rules"
)
Expand Down Expand Up @@ -77,6 +78,16 @@ func updateParse(cache *cache.Cache, uri string) (bool, error) {
line = 0
}

key := "regal/parse"
link := "https://docs.styra.com/opa/category/rego-parse-error"

hints, _ := hints.GetForError(err)
if len(hints) > 0 {
// there should only be one hint, so take the first
key = hints[0]
link = "https://docs.styra.com/opa/errors/" + hints[0]
}

diags = append(diags, types.Diagnostic{
Severity: 1, // parse errors are the only error Diagnostic the server sends
Range: types.Range{
Expand All @@ -91,10 +102,10 @@ func updateParse(cache *cache.Cache, uri string) (bool, error) {
},
},
Message: astError.Message,
Source: "regal/parse",
Source: key,
Code: strings.ReplaceAll(astError.Code, "_", "-"),
CodeDescription: types.CodeDescription{
Href: "https://docs.styra.com/opa/category/rego-parse-error",
Href: link,
},
})
}
Expand Down
101 changes: 101 additions & 0 deletions pkg/hints/hints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//nolint:lll
package hints

import (
"errors"
"fmt"
"regexp"

"github.com/mitchellh/mapstructure"
)

// GetForError will return any matched, Styra-documented, errors for a given Go error.
func GetForError(e error) ([]string, error) {
e0 := unwrapToFP(e)

msgs, err := extractMessages(e0)
if err != nil {
return []string{}, fmt.Errorf("failed to extract messages: %w", err)
}

if len(msgs) < 1 {
return []string{}, errors.New("no messages found")
}

msg := msgs[0]

var hintKeys []string

for u, r := range patterns {
if r.MatchString(msg) {
hintKeys = append(hintKeys, u)
}
}

return hintKeys, nil
}

//nolint:gochecknoglobals
var patterns = map[string]*regexp.Regexp{
`eval-conflict-error/complete-rules-must-not-produce-multiple-outputs`: regexp.MustCompile(`^eval_conflict_error: complete rules must not produce multiple outputs$`),
`eval-conflict-error/object-keys-must-be-unique`: regexp.MustCompile(`^object insert conflict$|^eval_conflict_error: object keys must be unique$`),
`rego-unsafe-var-error/var-name-is-unsafe`: regexp.MustCompile(`^rego_unsafe_var_error: var .* is unsafe$`),
`rego-recursion-error/rule-name-is-recursive`: regexp.MustCompile(`^rego_recursion_error: rule .* is recursive:`),
`rego-parse-error/var-cannot-be-used-for-rule-name`: regexp.MustCompile(`^rego_parse_error: var cannot be used for rule name$`),
`rego-type-error/conflicting-rules-name-found`: regexp.MustCompile(`^rego_type_error: conflicting rules .* found$`),
`rego-type-error/match-error`: regexp.MustCompile(`^rego_type_error: match error`),
`rego-type-error/arity-mismatch`: regexp.MustCompile(`^rego_type_error: .*: arity mismatch`),
`rego-type-error/function-has-arity-got-argument`: regexp.MustCompile(`^rego_type_error: function .* has arity [0-9]+, got [0-9]+ arguments?$`),
`rego-compile-error/assigned-var-name-unused`: regexp.MustCompile(`^rego_compile_error: assigned var .* unused$`),
`rego-parse-error/unexpected-assign-token`: regexp.MustCompile(`^rego_parse_error: unexpected assign token:`),
`rego-parse-error/unexpected-identifier-token`: regexp.MustCompile(`^rego_parse_error: unexpected identifier token:`),
`rego-parse-error/unexpected-left-curly-token`: regexp.MustCompile(`^rego_parse_error: unexpected { token:`),
`rego-parse-error/unexpected-right-curly-token`: regexp.MustCompile(`^rego_parse_error: unexpected } token`),
`rego-parse-error/unexpected-name-keyword`: regexp.MustCompile(`^rego_parse_error: unexpected .* keyword:`),
`rego-parse-error/unexpected-string-token`: regexp.MustCompile(`^rego_parse_error: unexpected string token:`),
`rego-type-error/multiple-default-rules-name-found`: regexp.MustCompile(`^rego_type_error: multiple default rules .* found$`),
}

func unwrapToFP(e error) error {
if w := errors.Unwrap(e); w != nil {
return unwrapToFP(w)
}

return e
}

type message struct {
Message string `json:"message"`
Code string `json:"code"`
}

func extractMessages(e error) ([]string, error) {
msgs := []message{}

decoder, err := mapstructure.NewDecoder(
&mapstructure.DecoderConfig{
TagName: "json",
Result: &msgs,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create decoder: %w", err)
}

if err := decoder.Decode(e); err != nil {
return nil, fmt.Errorf("failed to decode error: %w", err)
}

m := make([]string, len(msgs))

for i := range msgs {
m[i] = msgs[i].Code
if m[i] != "" {
m[i] += ": "
}

m[i] += msgs[i].Message
}

return m, nil
}
31 changes: 31 additions & 0 deletions pkg/hints/hints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hints

import (
"slices"
"testing"

"github.com/styrainc/regal/internal/parse"
)

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

mod := `package foo
incomplete`

_, err := parse.Module("test.rego", mod)
if err == nil {
t.Fatal("expected error")
}

hints, err := GetForError(err)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

expectedHints := []string{"rego-parse-error/var-cannot-be-used-for-rule-name"}
if !slices.Equal(hints, expectedHints) {
t.Fatalf("expected\n%v but got\n%v", expectedHints, hints)
}
}

0 comments on commit 9d7530a

Please sign in to comment.