Skip to content

Commit 7bd66d9

Browse files
authoredAug 5, 2024··
Allow using regal fix as a formatter (#960)
Also: - Removed the code that would send a response back for formatter options that `opa fmt` can't handle. This was a good idea, but since there isn't any obvious way for users to fix that, it hurts more than it helps - Removed the fmt fix from the default fixer options as formatting is done anyway by the rego-v1 fixer Fixes #839 Signed-off-by: Anders Eknert <anders@styra.com>
1 parent b3a79c0 commit 7bd66d9

File tree

8 files changed

+115
-88
lines changed

8 files changed

+115
-88
lines changed
 

‎docs/fixing.md

+5
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ Currently, the following rules are automatically fixable:
2222
Need to fix individual violations? Checkout the editors Regal supports
2323
[here](/regal/editor-support).
2424
:::
25+
26+
## Community
27+
28+
If you'd like to discuss Regal development or just talk about Regal in general, please join us in the `#regal`
29+
channel in the Styra Community [Slack](https://communityinviter.com/apps/styracommunity/signup)!

‎docs/language-server.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,18 @@ user-defined functions too.
8888

8989
### Formatting
9090

91-
Regal uses the `opa fmt` formatter for formatting Rego. This is made available as a command in editors, but also via
92-
a [code action](#code-actions) when unformatted files are encountered.
91+
By default, Regal uses the `opa fmt` formatter for formatting Rego. This is made available as a command in editors,
92+
but also via a [code action](#code-actions) when unformatted files are encountered.
9393

9494
<img
9595
src={require('./assets/lsp/format.png').default}
9696
alt="Screenshot of diagnostics as displayed in Zed"/>
9797

98+
Two other formatters are also available — `opa fmt --rego-v1` and `regal fix`. See the docs on
99+
[Fixing Violations](fixing.md) for more information about the `fix` command. Which formatter to use
100+
can be set via the `formatter` configuration option, which can be passed to Regal via the client (see
101+
the documentation for your client for how to do that).
102+
98103
### Code completions
99104

100105
Code completions, or suggestions, is likely one of the most useful features of the Regal language server. And best of

‎internal/lsp/format.go

-28
Original file line numberDiff line numberDiff line change
@@ -60,31 +60,3 @@ func ComputeEdits(before, after string) []types.TextEdit {
6060

6161
return edits
6262
}
63-
64-
// The opa fmt command does not allow configuration options, so we will have
65-
// to ignore them if provided by the client. We can however log the warnings
66-
// so that the caller has some chance to be made aware of why their options
67-
// aren't applied.
68-
func validateFormattingOptions(opts types.FormattingOptions) []string {
69-
warnings := make([]string, 0)
70-
71-
if opts.InsertSpaces {
72-
warnings = append(warnings, "opa fmt: only tabs supported for indentation")
73-
}
74-
75-
if !opts.TrimTrailingWhitespace {
76-
warnings = append(warnings, "opa fmt: trailing whitespace always trimmed")
77-
}
78-
79-
if !opts.InsertFinalNewline {
80-
warnings = append(warnings, "opa fmt: final newline always inserted")
81-
}
82-
83-
if !opts.TrimFinalNewlines {
84-
warnings = append(warnings, "opa fmt: final newlines always trimmed")
85-
}
86-
87-
// opts.TabSize ignored as we don't support using spaces
88-
89-
return warnings
90-
}

‎internal/lsp/server.go

+77-37
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ type LanguageServer struct {
8888
workspaceRootURI string
8989
clientIdentifier clients.Identifier
9090

91+
clientInitializationOptions types.InitializationOptions
92+
9193
cache *cache.Cache
9294
regoStore storage.Store
9395

@@ -352,7 +354,7 @@ func (l *LanguageServer) StartConfigWorker(ctx context.Context) {
352354
}
353355
}
354356

355-
// when a file is 'unignored', we move it's contents to the
357+
// when a file is 'unignored', we move its contents to the
356358
// standard file list if missing
357359
for k, v := range l.cache.GetAllIgnoredFiles() {
358360
if l.ignoreURI(k) {
@@ -364,7 +366,7 @@ func (l *LanguageServer) StartConfigWorker(ctx context.Context) {
364366
if !ok {
365367
l.cache.SetFileContents(k, v)
366368

367-
// updating the parse here will enable things like go-to defn
369+
// updating the parse here will enable things like go-to definition
368370
// to start working right away without the need for a file content
369371
// update to run updateParse.
370372
_, err = updateParse(ctx, l.cache, l.regoStore, k)
@@ -529,9 +531,9 @@ func (l *LanguageServer) StartWorkspaceStateWorker(ctx context.Context) {
529531
continue
530532
}
531533

532-
for _, uri := range changedOrNewURIs {
534+
for _, cnURI := range changedOrNewURIs {
533535
l.diagnosticRequestFile <- fileUpdateEvent{
534-
URI: uri,
536+
URI: cnURI,
535537
Reason: "internal/workspaceStateWorker/changedOrNewFile",
536538
}
537539
}
@@ -1296,10 +1298,6 @@ func (l *LanguageServer) handleTextDocumentFormatting(
12961298
return nil, fmt.Errorf("failed to unmarshal params: %w", err)
12971299
}
12981300

1299-
if warnings := validateFormattingOptions(params.Options); len(warnings) > 0 {
1300-
l.logError(fmt.Errorf("formatting params validation warnings: %v", warnings))
1301-
}
1302-
13031301
var oldContent string
13041302

13051303
var ok bool
@@ -1315,43 +1313,81 @@ func (l *LanguageServer) handleTextDocumentFormatting(
13151313
return nil, fmt.Errorf("failed to get file contents for uri %q", params.TextDocument.URI)
13161314
}
13171315

1318-
// set up an in-memory file provider to pass to the fixer for this one file
1319-
memfp := fileprovider.NewInMemoryFileProvider(map[string][]byte{
1320-
params.TextDocument.URI: []byte(oldContent),
1321-
})
1316+
formatter := "opa-fmt"
13221317

1323-
input, err := memfp.ToInput()
1324-
if err != nil {
1325-
return nil, fmt.Errorf("failed to create fixer input: %w", err)
1318+
if l.clientInitializationOptions.Formatter != nil {
1319+
formatter = *l.clientInitializationOptions.Formatter
13261320
}
13271321

1328-
li := linter.NewLinter().
1329-
WithUserConfig(*l.loadedConfig).
1330-
WithInputModules(&input)
1322+
var newContent []byte
1323+
1324+
switch formatter {
1325+
case "opa-fmt", "opa-fmt-rego-v1":
1326+
opts := format.Opts{}
1327+
if formatter == "opa-fmt-rego-v1" {
1328+
opts.RegoVersion = ast.RegoV0CompatV1
1329+
}
1330+
1331+
f := &fixes.Fmt{OPAFmtOpts: opts}
1332+
p := uri.ToPath(l.clientIdentifier, params.TextDocument.URI)
1333+
1334+
fixResults, err := f.Fix(&fixes.FixCandidate{Filename: filepath.Base(p), Contents: []byte(oldContent)}, nil)
1335+
if err != nil {
1336+
l.logError(fmt.Errorf("failed to format file: %w", err))
1337+
1338+
// return "null" as per the spec
1339+
return nil, nil
1340+
}
1341+
1342+
if len(fixResults) == 0 {
1343+
return []types.TextEdit{}, nil
1344+
}
1345+
1346+
newContent = fixResults[0].Contents
1347+
case "regal-fix":
1348+
// set up an in-memory file provider to pass to the fixer for this one file
1349+
memfp := fileprovider.NewInMemoryFileProvider(map[string][]byte{
1350+
params.TextDocument.URI: []byte(oldContent),
1351+
})
1352+
1353+
input, err := memfp.ToInput()
1354+
if err != nil {
1355+
return nil, fmt.Errorf("failed to create fixer input: %w", err)
1356+
}
13311357

1332-
f := fixer.NewFixer()
1333-
f.RegisterFixes(fixes.NewDefaultFixes()...)
1334-
f.RegisterMandatoryFixes(
1335-
&fixes.Fmt{
1336-
NameOverride: "use-rego-v1",
1337-
OPAFmtOpts: format.Opts{
1338-
RegoVersion: ast.RegoV0CompatV1,
1358+
f := fixer.NewFixer()
1359+
f.RegisterFixes(fixes.NewDefaultFixes()...)
1360+
f.RegisterMandatoryFixes(
1361+
&fixes.Fmt{
1362+
NameOverride: "use-rego-v1",
1363+
OPAFmtOpts: format.Opts{
1364+
RegoVersion: ast.RegoV0CompatV1,
1365+
},
13391366
},
1340-
},
1341-
)
1367+
)
13421368

1343-
fixReport, err := f.Fix(ctx, &li, memfp)
1344-
if err != nil {
1345-
return nil, fmt.Errorf("failed to format: %w", err)
1346-
}
1369+
li := linter.NewLinter().
1370+
WithInputModules(&input)
13471371

1348-
if fixReport.TotalFixes() == 0 {
1349-
return []types.TextEdit{}, nil
1350-
}
1372+
if l.loadedConfig != nil {
1373+
li = li.WithUserConfig(*l.loadedConfig)
1374+
}
13511375

1352-
newContent, err := memfp.GetFile(params.TextDocument.URI)
1353-
if err != nil {
1354-
return nil, fmt.Errorf("failed to get formatted contents: %w", err)
1376+
fixReport, err := f.Fix(ctx, &li, memfp)
1377+
if err != nil {
1378+
return nil, fmt.Errorf("failed to format: %w", err)
1379+
}
1380+
1381+
if fixReport.TotalFixes() == 0 {
1382+
return []types.TextEdit{}, nil
1383+
}
1384+
1385+
newContent, err = memfp.GetFile(params.TextDocument.URI)
1386+
if err != nil {
1387+
return nil, fmt.Errorf("failed to get formatted contents: %w", err)
1388+
}
1389+
default:
1390+
return nil, fmt.Errorf("unrecognized formatter %q", formatter)
13551391
}
13561392

13571393
return ComputeEdits(oldContent, string(newContent)), nil
@@ -1507,6 +1543,10 @@ func (l *LanguageServer) handleInitialize(
15071543
)
15081544
}
15091545

1546+
if params.InitializationOptions != nil {
1547+
l.clientInitializationOptions = *params.InitializationOptions
1548+
}
1549+
15101550
regoFilter := types.FileOperationFilter{
15111551
Scheme: "file",
15121552
Pattern: types.FileOperationPattern{

‎internal/lsp/types/types.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@ type FileEvent struct {
1919
URI string `json:"uri"`
2020
}
2121

22+
type InitializationOptions struct {
23+
Formatter *string `json:"formatter,omitempty"`
24+
}
25+
2226
type InitializeParams struct {
23-
ProcessID int `json:"processId"`
24-
ClientInfo Client `json:"clientInfo"`
25-
Locale string `json:"locale"`
26-
RootPath string `json:"rootPath"`
27-
RootURI string `json:"rootUri"`
28-
Capabilities ClientCapabilities `json:"capabilities"`
29-
Trace string `json:"trace"`
30-
WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders"`
27+
ProcessID int `json:"processId"`
28+
ClientInfo Client `json:"clientInfo"`
29+
Locale string `json:"locale"`
30+
RootPath string `json:"rootPath"`
31+
RootURI string `json:"rootUri"`
32+
Capabilities ClientCapabilities `json:"capabilities"`
33+
Trace string `json:"trace"`
34+
WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders"`
35+
InitializationOptions *InitializationOptions `json:"initializationOptions,omitempty"`
3136
}
3237

3338
type WorkspaceFolder struct {

‎pkg/fixer/fixer.go

+11-10
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ import (
1212
"github.com/styrainc/regal/pkg/linter"
1313
)
1414

15+
// NewFixer instantiates a Fixer.
1516
func NewFixer() *Fixer {
16-
return &Fixer{}
17+
return &Fixer{
18+
registeredFixes: make(map[string]any),
19+
registeredMandatoryFixes: make(map[string]any),
20+
}
1721
}
1822

23+
// Fixer must be instantiated via NewFixer.
1924
type Fixer struct {
2025
registeredFixes map[string]any
2126
registeredMandatoryFixes map[string]any
@@ -24,25 +29,21 @@ type Fixer struct {
2429
// RegisterFixes sets the fixes that will be fixed if there are related linter
2530
// violations that can be fixed by fixes.
2631
func (f *Fixer) RegisterFixes(fixes ...fixes.Fix) {
27-
if f.registeredFixes == nil {
28-
f.registeredFixes = make(map[string]any)
29-
}
30-
3132
for _, fix := range fixes {
32-
f.registeredFixes[fix.Name()] = fix
33+
if _, mandatory := f.registeredMandatoryFixes[fix.Name()]; !mandatory {
34+
f.registeredFixes[fix.Name()] = fix
35+
}
3336
}
3437
}
3538

3639
// RegisterMandatoryFixes sets fixes which will be run before other registered
3740
// fixes, against all files which are not ignored, regardless of linter
3841
// violations.
3942
func (f *Fixer) RegisterMandatoryFixes(fixes ...fixes.Fix) {
40-
if f.registeredMandatoryFixes == nil {
41-
f.registeredMandatoryFixes = make(map[string]any)
42-
}
43-
4443
for _, fix := range fixes {
4544
f.registeredMandatoryFixes[fix.Name()] = fix
45+
46+
delete(f.registeredFixes, fix.Name())
4647
}
4748
}
4849

‎pkg/fixer/fixer_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ deny = true
4949

5050
expectedFileFixedViolations := map[string][]string{
5151
// use-assignment-operator is not expected since use-rego-v1 also addresses this in this example
52-
"main.rego": {"no-whitespace-comment", "opa-fmt", "use-rego-v1"},
52+
"main.rego": {"no-whitespace-comment", "use-rego-v1"},
5353
}
5454
expectedFileContents := map[string][]byte{
5555
"main.rego": []byte(`package test
@@ -64,7 +64,7 @@ deny := true
6464
`),
6565
}
6666

67-
if got, exp := fixReport.TotalFixes(), 3; got != exp {
67+
if got, exp := fixReport.TotalFixes(), 2; got != exp {
6868
t.Fatalf("expected %d fixed files, got %d", exp, got)
6969
}
7070

‎pkg/fixer/fixes/fixes.go

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
// When a new fix is added, it should be added to this list.
1010
func NewDefaultFixes() []Fix {
1111
return []Fix{
12-
&Fmt{},
1312
&Fmt{
1413
NameOverride: "use-rego-v1",
1514
OPAFmtOpts: format.Opts{

0 commit comments

Comments
 (0)
Please sign in to comment.