Skip to content

Commit

Permalink
lsp: Implement code actions for new fixes (#661)
Browse files Browse the repository at this point in the history
* lsp: Implement code actions for new fixes

#653 added fixes for
no-whitespace-comment and use-assignment-operator. This PR adds code
actions for these in the regal lsp.

Signed-off-by: Charlie Egan <charlie@styra.com>

* Move commands under a fix namespace

Signed-off-by: Charlie Egan <charlie@styra.com>

---------

Signed-off-by: Charlie Egan <charlie@styra.com>
  • Loading branch information
charlieegan3 authored Apr 18, 2024
1 parent 5a8c5c1 commit 8ae24da
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 147 deletions.
22 changes: 20 additions & 2 deletions internal/lsp/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func toAnySlice(a []string) []any {
func FmtCommand(args []string) types.Command {
return types.Command{
Title: "Format using opa-fmt",
Command: "regal.fmt",
Command: "regal.fix.opa-fmt",
Tooltip: "Format using opa-fmt",
Arguments: toAnySlice(args),
}
Expand All @@ -23,8 +23,26 @@ func FmtCommand(args []string) types.Command {
func FmtV1Command(args []string) types.Command {
return types.Command{
Title: "Format for Rego v1 using opa-fmt",
Command: "regal.fmt.v1",
Command: "regal.fix.use-rego-v1",
Tooltip: "Format for Rego v1 using opa-fmt",
Arguments: toAnySlice(args),
}
}

func UseAssignmentOperatorCommand(args []string) types.Command {
return types.Command{
Title: "Replace = with := in assignment",
Command: "regal.fix.use-assignment-operator",
Tooltip: "Replace = with := in assignment",
Arguments: toAnySlice(args),
}
}

func NoWhiteSpaceCommentCommand(args []string) types.Command {
return types.Command{
Title: "Format comment to have leading whitespace",
Command: "regal.fix.no-whitespace-comment",
Tooltip: "Format comment to have leading whitespace",
Arguments: toAnySlice(args),
}
}
88 changes: 88 additions & 0 deletions internal/lsp/commands/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package commands

import (
"errors"
"fmt"
"strconv"

"github.com/open-policy-agent/opa/ast"

"github.com/styrainc/regal/internal/lsp/types"
)

type ParseOptions struct {
TargetArgIndex int
RowArgIndex int
ColArgIndex int
}

type ParseResult struct {
Target string
Location *ast.Location
}

// Parse is responsible for extracting the target and location from the given params command params sent from the client
// after acting on a Code Action.
func Parse(params types.ExecuteCommandParams, opts ParseOptions) (*ParseResult, error) {
if len(params.Arguments) == 0 {
return nil, errors.New("no args supplied")
}

target := ""

if opts.TargetArgIndex < len(params.Arguments) {
target = fmt.Sprintf("%s", params.Arguments[opts.TargetArgIndex])
}

// we can't extract a location from the same location as the target, so location arg positions
// must not have been set in the opts.
if opts.RowArgIndex == opts.TargetArgIndex {
return &ParseResult{
Target: target,
}, nil
}

var loc *ast.Location

if opts.RowArgIndex < len(params.Arguments) && opts.ColArgIndex < len(params.Arguments) {
var row, col int

switch v := params.Arguments[opts.RowArgIndex].(type) {
case int:
row = v
case string:
var err error

row, err = strconv.Atoi(v)
if err != nil {
return nil, fmt.Errorf("failed to parse row: %w", err)
}
default:
return nil, fmt.Errorf("unexpected type for row: %T", params.Arguments[opts.RowArgIndex])
}

switch v := params.Arguments[opts.ColArgIndex].(type) {
case int:
col = v
case string:
var err error

col, err = strconv.Atoi(v)
if err != nil {
return nil, fmt.Errorf("failed to parse col: %w", err)
}
default:
return nil, fmt.Errorf("unexpected type for col: %T", params.Arguments[opts.ColArgIndex])
}

loc = &ast.Location{
Row: row,
Col: col,
}
}

return &ParseResult{
Target: target,
Location: loc,
}, nil
}
110 changes: 110 additions & 0 deletions internal/lsp/commands/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package commands

import (
"testing"

"github.com/open-policy-agent/opa/ast"

"github.com/styrainc/regal/internal/lsp/types"
)

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

testCases := map[string]struct {
ExecuteCommandParams types.ExecuteCommandParams
ParseOptions ParseOptions
ExpectedTarget string
ExpectedLocation *ast.Location
}{
"extract target only": {
ExecuteCommandParams: types.ExecuteCommandParams{
Command: "example",
Arguments: []interface{}{"target"},
},
ParseOptions: ParseOptions{TargetArgIndex: 0},
ExpectedTarget: "target",
ExpectedLocation: nil,
},
"extract target and location": {
ExecuteCommandParams: types.ExecuteCommandParams{
Command: "example",
Arguments: []interface{}{"target", "1", 2}, // different types for testing, but should be strings
},
ParseOptions: ParseOptions{TargetArgIndex: 0, RowArgIndex: 1, ColArgIndex: 2},
ExpectedTarget: "target",
ExpectedLocation: &ast.Location{Row: 1, Col: 2},
},
}

for name, tc := range testCases {
tc := tc

t.Run(name, func(t *testing.T) {
t.Parallel()

result, err := Parse(tc.ExecuteCommandParams, tc.ParseOptions)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if result.Target != tc.ExpectedTarget {
t.Fatalf("expected target %q, got %q", tc.ExpectedTarget, result.Target)
}

if tc.ExpectedLocation == nil && result.Location != nil {
t.Fatalf("expected location to be nil, got %v", result.Location)
}

if tc.ExpectedLocation != nil {
if result.Location == nil {
t.Fatalf("expected location to be %v, got nil", tc.ExpectedLocation)
}

if result.Location.Row != tc.ExpectedLocation.Row {
t.Fatalf("expected row %d, got %d", tc.ExpectedLocation.Row, result.Location.Row)
}

if result.Location.Col != tc.ExpectedLocation.Col {
t.Fatalf("expected col %d, got %d", tc.ExpectedLocation.Col, result.Location.Col)
}
}
})
}
}

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

testCases := map[string]struct {
ExecuteCommandParams types.ExecuteCommandParams
ParseOptions ParseOptions
ExpectedError string
}{
"error extracting target": {
ExecuteCommandParams: types.ExecuteCommandParams{
Command: "example",
Arguments: []interface{}{}, // empty and so nothing can be extracted
},
ParseOptions: ParseOptions{TargetArgIndex: 0},
ExpectedError: "no args supplied",
},
}

for name, tc := range testCases {
tc := tc

t.Run(name, func(t *testing.T) {
t.Parallel()

_, err := Parse(tc.ExecuteCommandParams, tc.ParseOptions)
if err == nil {
t.Fatalf("expected error %q, got nil", tc.ExpectedError)
}

if err.Error() != tc.ExpectedError {
t.Fatalf("expected error %q, got %q", tc.ExpectedError, err.Error())
}
})
}
}
1 change: 0 additions & 1 deletion internal/lsp/messages.go

This file was deleted.

Loading

0 comments on commit 8ae24da

Please sign in to comment.