diff --git a/.golangci.yaml b/.golangci.yaml index bda034a9..6ceb4b2d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -18,6 +18,7 @@ linters: - rowserrcheck - wastedassign # annoying + - nestif - gocognit - goerr113 - varnamelen diff --git a/internal/lsp/documentsymbol.go b/internal/lsp/documentsymbol.go index e3ac428b..d8453bd7 100644 --- a/internal/lsp/documentsymbol.go +++ b/internal/lsp/documentsymbol.go @@ -11,7 +11,6 @@ import ( "github.com/styrainc/regal/internal/lsp/types/symbols" ) -//nolint:nestif func documentSymbols( contents string, module *ast.Module, @@ -173,7 +172,6 @@ func refToString(ref ast.Ref) string { return sb.String() } -//nolint:nestif func getRuleDetail(rule *ast.Rule) string { if rule.Head.Args != nil { return "function" + rule.Head.Args.String() diff --git a/internal/lsp/documentsymbol_test.go b/internal/lsp/documentsymbol_test.go index 1c263753..c773d815 100644 --- a/internal/lsp/documentsymbol_test.go +++ b/internal/lsp/documentsymbol_test.go @@ -83,7 +83,6 @@ func TestRefToString(t *testing.T) { } } -//nolint:nestif func TestDocumentSymbols(t *testing.T) { t.Parallel() diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 1762bf54..6c1de638 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -27,6 +27,7 @@ import ( rparse "github.com/styrainc/regal/internal/parse" "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/fixer/fixes" + "github.com/styrainc/regal/pkg/linter" ) const ( @@ -114,6 +115,8 @@ func (l *LanguageServer) Handle( return l.handleTextDocumentDidOpen(ctx, conn, req) case "textDocument/didClose": return struct{}{}, nil + case "textDocument/didSave": + return l.handleTextDocumentDidSave(ctx, conn, req) case "textDocument/documentSymbol": return l.handleTextDocumentDocumentSymbol(ctx, conn, req) case "textDocument/didChange": @@ -800,6 +803,56 @@ func (l *LanguageServer) handleTextDocumentDidChange( return struct{}{}, nil } +func (l *LanguageServer) handleTextDocumentDidSave( + ctx context.Context, + _ *jsonrpc2.Conn, + req *jsonrpc2.Request, +) (result any, err error) { + var params types.TextDocumentDidSaveParams + if err := json.Unmarshal(*req.Params, ¶ms); err != nil { + return nil, fmt.Errorf("failed to unmarshal params: %w", err) + } + + if params.Text != nil && l.loadedConfig != nil { + if !strings.Contains(*params.Text, "\r\n") { + return struct{}{}, nil + } + + enabled, err := linter.NewLinter().WithUserConfig(*l.loadedConfig).DetermineEnabledRules(ctx) + if err != nil { + l.logError(fmt.Errorf("failed to determine enabled rules: %w", err)) + + return struct{}{}, nil + } + + formattingEnabled := false + + for _, rule := range enabled { + if rule == "opa-fmt" || rule == "use-rego-v1" { + formattingEnabled = true + + break + } + } + + if formattingEnabled { + resp := types.ShowMessageParams{ + Type: 2, // warning + Message: "CRLF line ending detected. Please change editor setting to use LF for line endings.", + } + + err := l.conn.Notify(ctx, "window/showMessage", resp) + if err != nil { + l.logError(fmt.Errorf("failed to notify: %w", err)) + + return struct{}{}, nil + } + } + } + + return struct{}{}, nil +} + func (l *LanguageServer) handleTextDocumentDocumentSymbol( _ context.Context, _ *jsonrpc2.Conn, @@ -1037,6 +1090,9 @@ func (l *LanguageServer) handleInitialize( TextDocumentSyncOptions: types.TextDocumentSyncOptions{ OpenClose: true, Change: 1, // TODO: write logic to use 2, for incremental updates + Save: types.TextDocumentSaveOptions{ + IncludeText: true, + }, }, DiagnosticProvider: types.DiagnosticOptions{ Identifier: "rego", diff --git a/internal/lsp/types/types.go b/internal/lsp/types/types.go index c5638b4f..d633e950 100644 --- a/internal/lsp/types/types.go +++ b/internal/lsp/types/types.go @@ -69,6 +69,11 @@ type GeneralClientCapabilities struct { StaleRequestSupport StaleRequestSupportClientCapabilities `json:"staleRequestSupport"` } +type ShowMessageParams struct { + Type uint `json:"type"` + Message string `json:"message"` +} + type StaleRequestSupportClientCapabilities struct { Cancel bool `json:"cancel"` RetryOnContentModifieds []string `json:"retryOnContentModified"` @@ -247,9 +252,19 @@ type TextDocumentInlayHintParams struct { Range Range `json:"range"` } +type TextDocumentSaveOptions struct { + IncludeText bool `json:"includeText"` +} + +type TextDocumentDidSaveParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Text *string `json:"text,omitempty"` +} + type TextDocumentSyncOptions struct { - OpenClose bool `json:"openClose"` - Change uint `json:"change"` + OpenClose bool `json:"openClose"` + Change uint `json:"change"` + Save TextDocumentSaveOptions `json:"save"` } type TextDocumentIdentifier struct { diff --git a/pkg/reporter/reporter.go b/pkg/reporter/reporter.go index b9a76a2f..bf0d2cce 100644 --- a/pkg/reporter/reporter.go +++ b/pkg/reporter/reporter.go @@ -94,7 +94,7 @@ func (tr PrettyReporter) Publish(_ context.Context, r report.Report) error { footer := fmt.Sprintf("%d file%s linted.", r.Summary.FilesScanned, pluralScanned) - if r.Summary.NumViolations == 0 { //nolint:nestif + if r.Summary.NumViolations == 0 { footer += " No violations found." } else { pluralViolations := "" @@ -256,8 +256,6 @@ func (tr JSONReporter) Publish(_ context.Context, r report.Report) error { // Publish first prints the pretty formatted report to console for easy access in the logs. It then goes on // to print the GitHub Actions annotations for each violation. Finally, it prints a summary of the report suitable // for the GitHub Actions UI. -// -//nolint:nestif func (tr GitHubReporter) Publish(ctx context.Context, r report.Report) error { err := NewPrettyReporter(tr.out).Publish(ctx, r) if err != nil { @@ -302,7 +300,7 @@ func (tr GitHubReporter) Publish(ctx context.Context, r report.Report) error { fmt.Fprintf(summaryFile, "%d file%s linted.", r.Summary.FilesScanned, pluralScanned) - if r.Summary.NumViolations == 0 { //nolint:nestif + if r.Summary.NumViolations == 0 { fmt.Fprintf(summaryFile, " No violations found") } else { pluralViolations := ""