diff --git a/bundle/regal/lsp/clients/clients.rego b/bundle/regal/lsp/clients/clients.rego index 04340b0a..45968414 100644 --- a/bundle/regal/lsp/clients/clients.rego +++ b/bundle/regal/lsp/clients/clients.rego @@ -1,3 +1,5 @@ +# METADATA +# description: Client identifiers known by the Regal LSP server package regal.lsp.clients # METADATA diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename.rego b/bundle/regal/lsp/completion/providers/packagename/packagename.rego index 02126c6a..332e19d8 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename.rego @@ -16,7 +16,7 @@ items contains item if { startswith(line, "package ") position.character > 7 - ps := input.regal.context.path_separator + ps := input.regal.environment.path_separator abs_dir := _base(input.regal.file.name) rel_dir := trim_prefix(abs_dir, input.regal.context.workspace_root) diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego index 97b9b8ed..83a0cb21 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego @@ -10,13 +10,13 @@ test_package_name_completion_on_typing if { "lines": split(policy, "\n"), }, "context": { - "path_separator": "/", "workspace_root": "/Users/joe/policy", "location": { "row": 1, "col": 10, }, }, + "environment": {"path_separator": "/"}, }} items := provider.items with input as provider_input items == {{ @@ -41,13 +41,13 @@ test_package_name_completion_on_typing_multiple_suggestions if { "lines": split(policy, "\n"), }, "context": { - "path_separator": "/", "workspace_root": "/Users/joe/policy", "location": { "row": 1, "col": 10, }, }, + "environment": {"path_separator": "/"}, }} items := provider.items with input as provider_input items == { @@ -86,13 +86,13 @@ test_package_name_completion_on_typing_multiple_suggestions_when_invoked if { "lines": split(policy, "\n"), }, "context": { - "path_separator": "/", "workspace_root": "/Users/joe/policy", "location": { "row": 1, "col": 9, }, }, + "environment": {"path_separator": "/"}, }} items := provider.items with input as provider_input items == { diff --git a/bundle/regal/lsp/completion/providers/regov1/regov1.rego b/bundle/regal/lsp/completion/providers/regov1/regov1.rego index 082c447d..a2595d86 100644 --- a/bundle/regal/lsp/completion/providers/regov1/regov1.rego +++ b/bundle/regal/lsp/completion/providers/regov1/regov1.rego @@ -8,9 +8,9 @@ import data.regal.lsp.completion.kind import data.regal.lsp.completion.location # METADATA -# description: completion suggestion for rego.v1 +# description: completion suggestion for rego.v1 import items contains item if { - input.regal.context.rego_version != 3 # the rego.v1 import is not used in v1 rego (3) + input.regal.file.rego_version != "v1" # the rego.v1 import is not used in v1 Rego not strings.any_prefix_match(input.regal.file.lines, "import rego.v1") position := location.to_position(input.regal.context.location) diff --git a/bundle/regal/lsp/completion/providers/regov1/regov1_test.rego b/bundle/regal/lsp/completion/providers/regov1/regov1_test.rego index a9069fba..88d262ab 100644 --- a/bundle/regal/lsp/completion/providers/regov1/regov1_test.rego +++ b/bundle/regal/lsp/completion/providers/regov1/regov1_test.rego @@ -8,7 +8,7 @@ test_regov1_completion_on_typing if { policy := `package policy import r` - items := provider.items with input as util.input_with_location_and_version(policy, {"row": 3, "col": 9}, 0) + items := provider.items with input as util.input_with_location_and_version(policy, {"row": 3, "col": 9}, "v0") items == {{ "label": "rego.v1", "kind": 9, @@ -33,7 +33,7 @@ test_regov1_completion_on_invoked if { policy := `package policy import ` - items := provider.items with input as util.input_with_location_and_version(policy, {"row": 3, "col": 8}, 0) + items := provider.items with input as util.input_with_location_and_version(policy, {"row": 3, "col": 8}, "v0") items == {{ "label": "rego.v1", "kind": 9, @@ -60,7 +60,7 @@ test_no_regov1_completion_if_already_imported if { import rego.v1 import r` - items := provider.items with input as util.input_with_location_and_version(policy, {"row": 5, "col": 9}, 0) + items := provider.items with input as util.input_with_location_and_version(policy, {"row": 5, "col": 9}, "v0") items == set() } @@ -71,7 +71,7 @@ import r` items := provider.items with input as util.input_with_location_and_version( policy, {"row": 3, "col": 9}, - 3, # RegoV1 + "v1", # RegoV1 ) items == set() } diff --git a/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego b/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego index d0b9c712..ed1e8e11 100644 --- a/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego +++ b/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego @@ -35,9 +35,7 @@ input_with_location_and_version(policy, location, rego_version) := {"regal": { "file": { "name": "p.rego", "lines": split(policy, "\n"), - }, - "context": { - "location": location, "rego_version": rego_version, }, + "context": {"location": location}, }} diff --git a/cmd/test.go b/cmd/test.go index 352d8270..0d29d058 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -397,7 +397,7 @@ func Runtime() *ast.Term { for _, s := range os.Environ() { parts := strings.SplitN(s, "=", 2) if len(parts) == 1 { - env.Insert(ast.StringTerm(parts[0]), ast.NullTerm()) + env.Insert(ast.StringTerm(parts[0]), ast.InternedNullTerm) } else if len(parts) > 1 { env.Insert(ast.StringTerm(parts[0]), ast.StringTerm(parts[1])) } diff --git a/go.mod b/go.mod index 98d47b3b..584d1ac0 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/sourcegraph/jsonrpc2 v0.2.1 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - github.com/styrainc/roast v0.12.0 + github.com/styrainc/roast v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 7382ac65..9505421d 100644 --- a/go.sum +++ b/go.sum @@ -305,8 +305,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/styrainc/roast v0.12.0 h1:TCxazAd1PodsmzMlWgaomDxsx48QdFqJWGldxC7YEpg= -github.com/styrainc/roast v0.12.0/go.mod h1:cKELz96vKP1jeA/0HeyMW6gOqvDI6eo3wmq5/a43TYA= +github.com/styrainc/roast v0.14.0 h1:UewC6U8A2MvUUnXRQggHFvpr94jSHzFEvcyYVfbLutc= +github.com/styrainc/roast v0.14.0/go.mod h1:cKELz96vKP1jeA/0HeyMW6gOqvDI6eo3wmq5/a43TYA= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= diff --git a/internal/lsp/clients/clients.go b/internal/lsp/clients/clients.go index 22a0cbf4..39255d4b 100644 --- a/internal/lsp/clients/clients.go +++ b/internal/lsp/clients/clients.go @@ -2,7 +2,7 @@ package clients // Identifier represent different supported clients and can be used to toggle or change // server behavior based on the client. -type Identifier int +type Identifier uint8 const ( IdentifierGeneric Identifier = iota diff --git a/internal/lsp/completions/providers/policy.go b/internal/lsp/completions/providers/policy.go index 0ba65b0b..8fde233d 100644 --- a/internal/lsp/completions/providers/policy.go +++ b/internal/lsp/completions/providers/policy.go @@ -18,6 +18,7 @@ import ( "github.com/styrainc/regal/pkg/builtins" "github.com/styrainc/roast/pkg/encoding" + "github.com/styrainc/roast/pkg/transform" ) // Policy provides suggestions that have been determined by Rego policy. @@ -58,40 +59,42 @@ func (p *Policy) Run( return nil, fmt.Errorf("could not get file contents for: %s", params.TextDocument.URI) } + // input.regal.context location := rego2.LocationFromPosition(params.Position) - inputContext := make(map[string]any) - inputContext["location"] = map[string]any{ - "row": location.Row, - "col": location.Col, - } - inputContext["client_identifier"] = opts.ClientIdentifier - inputContext["workspace_root"] = uri.ToPath(opts.ClientIdentifier, opts.RootURI) - inputContext["path_separator"] = rio.PathSeparator - inputContext["rego_version"] = opts.RegoVersion - - workspacePath := uri.ToPath(opts.ClientIdentifier, opts.RootURI) - - inputDotJSONPath, inputDotJSONContent := rio.FindInput( - uri.ToPath(opts.ClientIdentifier, params.TextDocument.URI), - workspacePath, + regalContext := ast.NewObject( + ast.Item(ast.InternedStringTerm("location"), ast.ObjectTerm( + ast.Item(ast.InternedStringTerm("row"), ast.InternedIntNumberTerm(location.Row)), + ast.Item(ast.InternedStringTerm("col"), ast.InternedIntNumberTerm(location.Col)), + )), + ast.Item(ast.InternedStringTerm("client_identifier"), ast.InternedIntNumberTerm(int(opts.ClientIdentifier))), + ast.Item(ast.InternedStringTerm("workspace_root"), ast.InternedStringTerm(opts.RootURI)), ) + path := uri.ToPath(opts.ClientIdentifier, params.TextDocument.URI) + + // TODO: Avoid the intermediate map[string]any step and unmarshal directly into ast.Value. + inputDotJSONPath, inputDotJSONContent := rio.FindInput(path, uri.ToPath(opts.ClientIdentifier, opts.RootURI)) if inputDotJSONPath != "" && inputDotJSONContent != nil { - inputContext["input_dot_json_path"] = inputDotJSONPath - inputContext["input_dot_json"] = inputDotJSONContent - } + inputDotJSONValue, err := transform.ToOPAInputValue(inputDotJSONContent) + if err != nil { + return nil, fmt.Errorf("failed converting input dot JSON content to value: %w", err) + } - input, err := rego2.ToInput( - params.TextDocument.URI, - opts.ClientIdentifier, - content, - inputContext, - ) - if err != nil { - // parser error could be due to work in progress, so just return an empty list here - return []types.CompletionItem{}, nil //nolint: nilerr + regalContext.Insert(ast.InternedStringTerm("input_dot_json_path"), ast.InternedStringTerm(inputDotJSONPath)) + regalContext.Insert(ast.InternedStringTerm("input_dot_json"), ast.NewTerm(inputDotJSONValue)) } + // input.regal + regalObj := transform.RegalContext(path, content, opts.RegoVersion.String()) + regalObj.Insert(ast.InternedStringTerm("context"), ast.NewTerm(regalContext)) + + fileRef := ast.Ref{ast.InternedStringTerm("file")} + fileObj, _ := regalObj.Find(fileRef) + //nolint:forcetypeassert + fileObj.(ast.Object).Insert(ast.InternedStringTerm("uri"), ast.InternedStringTerm(params.TextDocument.URI)) + + input := ast.NewObject(ast.Item(ast.InternedStringTerm("regal"), ast.NewTerm(regalObj))) + result, err := rego2.QueryRegalBundle(ctx, input, p.pq) if err != nil { return nil, fmt.Errorf("failed querying regal bundle: %w", err) diff --git a/internal/lsp/completions/providers/policy_test.go b/internal/lsp/completions/providers/policy_test.go index 6fb89cac..b9b98e9d 100644 --- a/internal/lsp/completions/providers/policy_test.go +++ b/internal/lsp/completions/providers/policy_test.go @@ -44,28 +44,18 @@ allow if { }, inmem.OptRoundTripOnWrite(false)) locals := NewPolicy(t.Context(), store) - params := types.CompletionParams{ - TextDocument: types.TextDocumentIdentifier{ - URI: testCaseFileURI, - }, - Position: types.Position{ - Line: 5, - Character: 11, - }, + TextDocument: types.TextDocumentIdentifier{URI: testCaseFileURI}, + Position: types.Position{Line: 5, Character: 11}, } + opts := &Options{ClientIdentifier: clients.IdentifierGeneric} - result, err := locals.Run( - t.Context(), - c, - params, - &Options{ClientIdentifier: clients.IdentifierGeneric}, - ) + result, err := locals.Run(t.Context(), c, params, opts) if err != nil { t.Fatalf("unexpected error: %v", err) } - labels := []string{} + labels := make([]string, 0, len(result)) for _, item := range result { labels = append(labels, item.Label) } diff --git a/internal/lsp/completions/providers/utils.go b/internal/lsp/completions/providers/utils.go index 19e99ee5..58d32ca1 100644 --- a/internal/lsp/completions/providers/utils.go +++ b/internal/lsp/completions/providers/utils.go @@ -2,9 +2,10 @@ package providers import ( "regexp" - "slices" "strings" + "github.com/open-policy-agent/opa/v1/util" + "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/types" ) @@ -47,15 +48,8 @@ func groupKeyedRefsByDepth(refs map[string]types.Ref) ([]int, map[int]map[string byDepth[depth][key] = item } - depths := make([]int, 0) - for k := range byDepth { - depths = append(depths, k) - } - // items from higher depths should be shown first - slices.Sort(depths) - - return depths, byDepth + return util.KeysSorted(byDepth), byDepth } // inRuleBody is a best-effort helper to determine if the current line is in a rule body. diff --git a/internal/lsp/completions/refs/used.go b/internal/lsp/completions/refs/used.go index bf21412f..14a8429f 100644 --- a/internal/lsp/completions/refs/used.go +++ b/internal/lsp/completions/refs/used.go @@ -14,11 +14,17 @@ import ( "github.com/styrainc/regal/pkg/builtins" "github.com/styrainc/regal/pkg/config" + "github.com/styrainc/roast/pkg/rast" "github.com/styrainc/roast/pkg/transform" _ "embed" ) +var ( + refNamesQuery = rast.RefStringToBody(`data.regal.lsp.completion.ref_names`) + pqOnce = sync.OnceValues(prepareQuery) +) + // initialize prepares the rego query for finding ref names used in a module. // This is run and the resulting prepared query stored for performance reasons. // This function is only used by language server code paths and so init() is not @@ -41,7 +47,7 @@ func prepareQuery() (*rego.PreparedEvalQuery, error) { regoArgs := append([]func(*rego.Rego){ rego.ParsedBundle("regal", &rbundle.LoadedBundle), rego.ParsedBundle("internal", &dataBundle), - rego.Query(`data.regal.lsp.completion.ref_names`), + rego.ParsedQuery(refNamesQuery), }, builtins.RegalBuiltinRegoFuncs...) preparedQuery, err := rego.New(regoArgs...).PrepareForEval(context.Background()) @@ -52,14 +58,12 @@ func prepareQuery() (*rego.PreparedEvalQuery, error) { return &preparedQuery, nil } -var pqOnce = sync.OnceValues(prepareQuery) - // UsedInModule returns a list of ref names suitable for completion that are // used in the module's code. // See the rego above for more details on what's included and excluded. // This function is run when the parse completes for a module. func UsedInModule(ctx context.Context, module *ast.Module) ([]string, error) { - inputValue, err := transform.ToOPAInputValue(module) + inputValue, err := transform.ModuleToValue(module) if err != nil { return nil, fmt.Errorf("failed converting input to value: %w", err) } diff --git a/internal/lsp/handle_text_document_code_action_test.go b/internal/lsp/handle_text_document_code_action_test.go index 441ddc09..34534d3a 100644 --- a/internal/lsp/handle_text_document_code_action_test.go +++ b/internal/lsp/handle_text_document_code_action_test.go @@ -37,6 +37,10 @@ func TestHandleTextDocumentCodeAction(t *testing.T) { params := types.CodeActionParams{ TextDocument: types.TextDocumentIdentifier{URI: uri}, Context: types.CodeActionContext{Diagnostics: []types.Diagnostic{diag}}, + Range: types.Range{ + Start: types.Position{Line: 2, Character: 4}, + End: types.Position{Line: 2, Character: 10}, + }, } expectedAction := types.CodeAction{ @@ -115,8 +119,10 @@ func TestHandleTextDocumentCodeAction(t *testing.T) { } } -// 0.06 milliseconds per operation, not bad at all! -// 63243 ns/op 59576 B/op 1110 allocs/op +// 63243 ns/op 59576 B/op 1110 allocs/op - the OPA JSON roundtrip method +// 42402 ns/op 37822 B/op 738 allocs/op - build input Value by hand +// 45049 ns/op 39731 B/op 790 allocs/op - build input Value using reflection +// 44024 ns/op 38040 B/op 749 allocs/op - build input Value using reflection + interning // ... // "real world" usage shows a number somewhere between 0.1 - 0.5 ms // of which most of the cost is in JSON marshaling and unmarshaling. diff --git a/internal/lsp/hover/hover.go b/internal/lsp/hover/hover.go index c07efc7e..74baf224 100644 --- a/internal/lsp/hover/hover.go +++ b/internal/lsp/hover/hover.go @@ -165,14 +165,9 @@ func UpdateBuiltinPositions(cache *cache.Cache, uri string, builtins map[string] } func UpdateKeywordLocations(ctx context.Context, cache *cache.Cache, uri string) error { - module, ok := cache.GetModule(uri) - if !ok { - return fmt.Errorf("failed to update builtin positions: no parsed module for uri %q", uri) - } - - fileContents, ok := cache.GetFileContents(uri) + fileContents, module, ok := cache.GetContentAndModule(uri) if !ok { - return fmt.Errorf("failed to determine keyword locations: no file contents for uri %q", uri) + return fmt.Errorf("failed to determine keyword locations: missing file contents for uri %q", uri) } keywords, err := rego.AllKeywords(ctx, filepath.Base(uri), fileContents, module) diff --git a/internal/lsp/lint.go b/internal/lsp/lint.go index 1f1c5023..24156646 100644 --- a/internal/lsp/lint.go +++ b/internal/lsp/lint.go @@ -14,6 +14,7 @@ import ( "github.com/styrainc/regal/internal/lsp/completions/refs" "github.com/styrainc/regal/internal/lsp/types" rparse "github.com/styrainc/regal/internal/parse" + "github.com/styrainc/regal/internal/util" "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/hints" "github.com/styrainc/regal/pkg/linter" @@ -123,7 +124,7 @@ func updateParse( //nolint:gosec diags = append(diags, types.Diagnostic{ - Severity: 1, // parse errors are the only error Diagnostic the server sends + Severity: util.Pointer(uint(1)), // parse errors are the only error Diagnostic the server sends Range: types.Range{ Start: types.Position{ Line: uint(line), @@ -136,7 +137,7 @@ func updateParse( }, }, Message: astError.Message, - Source: key, + Source: &key, Code: strings.ReplaceAll(astError.Code, "_", "-"), CodeDescription: &types.CodeDescription{ Href: link, @@ -299,12 +300,13 @@ func convertReportToDiagnostics(rpt *report.Report, workspaceRootURI string) map } file := cmp.Or(item.Location.File, workspaceRootURI) + source := "regal/" + item.Category fileDiags[file] = append(fileDiags[file], types.Diagnostic{ - Severity: severity, + Severity: &severity, Range: getRangeForViolation(item), Message: item.Description, - Source: "regal/" + item.Category, + Source: &source, Code: item.Title, CodeDescription: &types.CodeDescription{ Href: fmt.Sprintf( diff --git a/internal/lsp/lint_test.go b/internal/lsp/lint_test.go index 4066b84e..db0b8410 100644 --- a/internal/lsp/lint_test.go +++ b/internal/lsp/lint_test.go @@ -9,6 +9,7 @@ import ( "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/types" "github.com/styrainc/regal/internal/parse" + "github.com/styrainc/regal/internal/util" "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/report" ) @@ -168,10 +169,10 @@ func TestConvertReportToDiagnostics(t *testing.T) { expectedFileDiags := map[string][]types.Diagnostic{ "file1": { { - Severity: 2, + Severity: util.Pointer(uint(2)), Range: getRangeForViolation(violation1), Message: "Mock Error", - Source: "regal/mock_category", + Source: util.Pointer("regal/mock_category"), Code: "mock_title", CodeDescription: &types.CodeDescription{ Href: "https://docs.styra.com/regal/rules/mock_category/mock_title", @@ -180,10 +181,10 @@ func TestConvertReportToDiagnostics(t *testing.T) { }, "workspaceRootURI": { { - Severity: 3, + Severity: util.Pointer(uint(3)), Range: getRangeForViolation(violation2), Message: "Mock Warning", - Source: "regal/mock_category", + Source: util.Pointer("regal/mock_category"), Code: "mock_title", CodeDescription: &types.CodeDescription{ Href: "https://docs.styra.com/regal/rules/mock_category/mock_title", diff --git a/internal/lsp/rego/rego.go b/internal/lsp/rego/rego.go index e900fa5e..7dae8cb1 100644 --- a/internal/lsp/rego/rego.go +++ b/internal/lsp/rego/rego.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "sync" "github.com/open-policy-agent/opa/v1/ast" @@ -13,11 +12,10 @@ import ( rbundle "github.com/styrainc/regal/bundle" "github.com/styrainc/regal/internal/lsp/clients" "github.com/styrainc/regal/internal/lsp/types" - "github.com/styrainc/regal/internal/lsp/uri" - "github.com/styrainc/regal/internal/parse" "github.com/styrainc/regal/pkg/builtins" "github.com/styrainc/roast/pkg/encoding" + "github.com/styrainc/roast/pkg/rast" "github.com/styrainc/roast/pkg/transform" ) @@ -29,6 +27,15 @@ var ( errExcpectedOneExpr = errors.New("expected exactly one expression in result") ) +func init() { + ast.InternStringTerm( + // All keys from Code Actions + "identifier", "workspace_root_uri", "web_server_base_uri", "client", "params", "start", "end", + "textDocument", "context", "range", "uri", "diagnostics", "only", "triggerKind", "codeDescription", + "message", "severity", "source", "code", "data", "title", "command", "kind", "isPreferred", + ) +} + type BuiltInCall struct { Builtin *ast.Builtin Location *ast.Location @@ -47,18 +54,31 @@ type KeywordUseLocation struct { Col uint `json:"col"` } -type CodeActionContext struct { - client clients.Identifier - webServerBaseURI string - workspaceRootURI string +type Client struct { + Identifier clients.Identifier `json:"identifier"` + InitializationOptions *types.InitializationOptions `json:"init_options,omitempty"` } -func NewCodeActionContext(client clients.Identifier, webServerBaseURI, workspaceRootURI string) CodeActionContext { - return CodeActionContext{ - client: client, - webServerBaseURI: webServerBaseURI, - workspaceRootURI: workspaceRootURI, - } +type Environment struct { + WorkspaceRootURI string `json:"workspace_root_uri"` + WebServerBaseURI string `json:"web_server_base_uri"` +} + +type RegalContext struct { + Client Client `json:"client"` + WebServerBaseURI string `json:"web_server_base_uri"` + WorkspaceRootURI string `json:"workspace_root_uri"` +} + +type CodeActionInput struct { + Regal RegalContext `json:"regal"` + Params types.CodeActionParams `json:"params"` +} + +type CodeActionContext struct { + Client clients.Identifier `json:"client"` + WebServerBaseURI string `json:"web_server_base_uri"` + WorkspaceRootURI string `json:"workspace_root_uri"` } func PositionFromLocation(loc *ast.Location) types.Position { @@ -187,21 +207,12 @@ func CodeLenses(ctx context.Context, uri, contents string, module *ast.Module) ( // CodeActions returns all code actions in the module. // Note that at least as of now, no code actions depend on the data in the module, so // it is not passed as part of the input. This could change in the future. -func CodeActions( - ctx context.Context, - context CodeActionContext, - params types.CodeActionParams, -) ([]types.CodeAction, error) { +func CodeActions(ctx context.Context, input CodeActionInput) ([]types.CodeAction, error) { preparedQueriesInitOnce.Do(initialize) var codeActions []types.CodeAction - input, err := prepareCodeActionInput(context, params) - if err != nil { - return nil, fmt.Errorf("failed preparing code action input: %w", err) - } - - value, err := queryToValueWithInput(ctx, codeActionPreparedQuery, input, codeActions) + value, err := queryToValueWithParsedInput(ctx, codeActionPreparedQuery, rast.StructToValue(input), codeActions) if err != nil { return nil, fmt.Errorf("failed querying code lenses: %w", err) } @@ -210,26 +221,21 @@ func CodeActions( } func queryToValue[T any](ctx context.Context, pq *rego.PreparedEvalQuery, policy policy, toValue T) (T, error) { - input, err := parse.PrepareAST(policy.fileName, policy.contents, policy.module) + input, err := transform.ToAST(policy.fileName, policy.contents, policy.module, false) if err != nil { return toValue, fmt.Errorf("failed to prepare input: %w", err) } - return queryToValueWithInput(ctx, pq, input, toValue) + return queryToValueWithParsedInput(ctx, pq, input, toValue) } -func queryToValueWithInput[T any]( +func queryToValueWithParsedInput[T any]( ctx context.Context, pq *rego.PreparedEvalQuery, - input map[string]any, + input ast.Value, toValue T, ) (T, error) { - inputValue, err := transform.ToOPAInputValue(input) - if err != nil { - return toValue, fmt.Errorf("failed converting input to value: %w", err) - } - - result, err := toValidResult(pq.Eval(ctx, rego.EvalParsedInput(inputValue))) + result, err := toValidResult(pq.Eval(ctx, rego.EvalParsedInput(input))) if err != nil { return toValue, err } @@ -256,52 +262,8 @@ func toValidResult(rs rego.ResultSet, err error) (rego.Result, error) { return rs[0], nil } -// ToInput prepares a module with Regal additions to be used as input for evaluation. -func ToInput( - fileURI string, - cid clients.Identifier, - content string, - context map[string]any, -) (map[string]any, error) { - path := uri.ToPath(cid, fileURI) - - input := map[string]any{ - "regal": map[string]any{ - "file": map[string]any{ - "name": path, - "uri": fileURI, - "lines": strings.Split(content, "\n"), - }, - "context": context, - }, - } - - if regal, ok := input["regal"].(map[string]any); ok { - if f, ok := regal["file"].(map[string]any); ok { - f["uri"] = fileURI - } - - regal["client_id"] = cid - } - - return SetInputContext(input, context), nil -} - -func SetInputContext(input map[string]any, context map[string]any) map[string]any { - if regal, ok := input["regal"].(map[string]any); ok { - regal["context"] = context - } - - return input -} - -func QueryRegalBundle(ctx context.Context, input map[string]any, pq rego.PreparedEvalQuery) (map[string]any, error) { - inputValue, err := transform.ToOPAInputValue(input) - if err != nil { - return nil, fmt.Errorf("failed converting input map to value: %w", err) - } - - result, err := pq.Eval(ctx, rego.EvalParsedInput(inputValue)) +func QueryRegalBundle(ctx context.Context, input ast.Value, pq rego.PreparedEvalQuery) (map[string]any, error) { + result, err := pq.Eval(ctx, rego.EvalParsedInput(input)) if err != nil { return nil, fmt.Errorf("failed evaluating query: %w", err) } @@ -313,27 +275,6 @@ func QueryRegalBundle(ctx context.Context, input map[string]any, pq rego.Prepare return result[0].Bindings, nil } -func prepareCodeActionInput(context CodeActionContext, params types.CodeActionParams) (map[string]any, error) { - var preparedParams map[string]any - - if err := encoding.JSONRoundTrip(params, &preparedParams); err != nil { - return nil, fmt.Errorf("JSON rountrip failed for code action params: %w", err) - } - - return map[string]any{ - "params": preparedParams, - "regal": map[string]any{ - "client": map[string]any{ - "identifier": int(context.client), - }, - "environment": map[string]any{ - "web_server_base_uri": context.webServerBaseURI, - "workspace_root_uri": context.workspaceRootURI, - }, - }, - }, nil -} - func createArgs(args ...func(*rego.Rego)) []func(*rego.Rego) { always := append([]func(*rego.Rego){ rego.ParsedBundle("regal", &rbundle.LoadedBundle), @@ -344,7 +285,9 @@ func createArgs(args ...func(*rego.Rego)) []func(*rego.Rego) { } func createPreparedQuery(query string) *rego.PreparedEvalQuery { - pq, err := rego.New(createArgs(rego.Query(query))...).PrepareForEval(context.Background()) + args := createArgs(rego.ParsedQuery(rast.RefStringToBody(query))) + + pq, err := rego.New(args...).PrepareForEval(context.Background()) if err != nil { panic(fmt.Sprintf("failed to prepare query %s: %v", query, err)) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f8120c2f..1b5a8642 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1429,8 +1429,13 @@ func (l *LanguageServer) handleTextDocumentHover(params types.TextDocumentHoverP // this is an approximation, if there are multiple violations on the same line // where hover loc is in their range, then they all just share a range as a // single range is needed in the hover response. + source := "" + if v.Source != nil { + source = *v.Source + } + sharedRange = v.Range - docSnippets = append(docSnippets, fmt.Sprintf("[%s/%s](%s)", v.Source, v.Code, v.CodeDescription.Href)) + docSnippets = append(docSnippets, fmt.Sprintf("[%s/%s](%s)", source, v.Code, v.CodeDescription.Href)) } } @@ -1521,9 +1526,17 @@ func (l *LanguageServer) handleTextDocumentCodeAction(ctx context.Context, param return noCodeActions, nil } - codeActionContext := rego.NewCodeActionContext(l.clientIdentifier, l.webServer.GetBaseURL(), l.workspaceRootURI) - - return rego.CodeActions(ctx, codeActionContext, params) //nolint:wrapcheck + return rego.CodeActions(ctx, rego.CodeActionInput{ //nolint:wrapcheck + Regal: rego.RegalContext{ + Client: rego.Client{ + Identifier: l.clientIdentifier, + InitializationOptions: &l.clientInitializationOptions, + }, + WebServerBaseURI: l.webServer.GetBaseURL(), + WorkspaceRootURI: l.workspaceRootURI, + }, + Params: params, + }) } func (l *LanguageServer) handleWorkspaceExecuteCommand(params types.ExecuteCommandParams) (any, error) { @@ -1557,8 +1570,6 @@ func (l *LanguageServer) handleTextDocumentInlayHint(params types.TextDocumentIn return partialInlayHints(parseErrors, contents, params.TextDocument.URI, bis), nil } - // TODO: use GetContentAndModule here, or do we need to handle the cases separately? - // file is blank, nothing to do if contents, ok := l.cache.GetFileContents(params.TextDocument.URI); ok && contents == "" { return []types.InlayHint{}, nil } @@ -1570,9 +1581,7 @@ func (l *LanguageServer) handleTextDocumentInlayHint(params types.TextDocumentIn return []types.InlayHint{}, nil } - inlayHints := getInlayHints(module, bis) - - return inlayHints, nil + return getInlayHints(module, bis), nil } func (l *LanguageServer) handleTextDocumentCodeLens(ctx context.Context, params types.CodeLensParams) (any, error) { @@ -1609,12 +1618,11 @@ func (l *LanguageServer) handleTextDocumentCodeLens(ctx context.Context, params return nil, fmt.Errorf("failed to get code lenses: %w", err) } - if l.clientInitializationOptions.EnableDebugCodelens != nil && - *l.clientInitializationOptions.EnableDebugCodelens { + if l.clientInitializationOptions.EnableDebugCodelens != nil && *l.clientInitializationOptions.EnableDebugCodelens { return lenses, nil } - // filter out `regal.debug` codelens + // remove `regal.debug` codelens, as it's not enabled here filteredLenses := make([]types.CodeLens, 0, len(lenses)) for _, lens := range lenses { @@ -2104,10 +2112,7 @@ func (l *LanguageServer) handleWorkspaceDidRenameFiles( l.cache.SetFileContents(renameOp.NewURI, content) - job := lintFileJob{ - Reason: "textDocument/didRename", - URI: renameOp.NewURI, - } + job := lintFileJob{Reason: "textDocument/didRename", URI: renameOp.NewURI} l.lintFileJobs <- job l.builtinsPositionJobs <- job @@ -2201,21 +2206,15 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init l.clientInitializationOptions = *params.InitializationOptions } - regoFilter := types.FileOperationFilter{ - Scheme: "file", - Pattern: types.FileOperationPattern{ - Glob: "**/*.rego", - }, - } + regoFilter := types.FileOperationFilter{Scheme: "file", Pattern: types.FileOperationPattern{Glob: "**/*.rego"}} + fileOpOpts := types.FileOperationRegistrationOptions{Filters: []types.FileOperationFilter{regoFilter}} initializeResult := types.InitializeResult{ Capabilities: types.ServerCapabilities{ TextDocumentSyncOptions: types.TextDocumentSyncOptions{ OpenClose: true, Change: 1, // TODO: write logic to use 2, for incremental updates - Save: types.TextDocumentSaveOptions{ - IncludeText: true, - }, + Save: types.TextDocumentSaveOptions{IncludeText: true}, }, DiagnosticProvider: types.DiagnosticOptions{ Identifier: "rego", @@ -2224,15 +2223,9 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init }, Workspace: types.WorkspaceOptions{ FileOperations: types.FileOperationsServerCapabilities{ - DidCreate: types.FileOperationRegistrationOptions{ - Filters: []types.FileOperationFilter{regoFilter}, - }, - DidRename: types.FileOperationRegistrationOptions{ - Filters: []types.FileOperationFilter{regoFilter}, - }, - DidDelete: types.FileOperationRegistrationOptions{ - Filters: []types.FileOperationFilter{regoFilter}, - }, + DidCreate: fileOpOpts, + DidRename: fileOpOpts, + DidDelete: fileOpOpts, }, WorkspaceFolders: types.WorkspaceFoldersServerCapabilities{ // NOTE(anders): The language server protocol doesn't go into detail about what this is meant to @@ -2247,16 +2240,9 @@ func (l *LanguageServer) handleInitialize(ctx context.Context, params types.Init Supported: true, }, }, - InlayHintProvider: types.InlayHintOptions{ - ResolveProvider: false, - }, - HoverProvider: true, - CodeActionProvider: types.CodeActionOptions{ - CodeActionKinds: []string{ - "quickfix", - "source.explore", - }, - }, + InlayHintProvider: types.InlayHintOptions{ResolveProvider: false}, + HoverProvider: true, + CodeActionProvider: types.CodeActionOptions{CodeActionKinds: []string{"quickfix", "source.explore"}}, ExecuteCommandProvider: types.ExecuteCommandOptions{ Commands: []string{ "regal.debug", diff --git a/internal/lsp/types/types.go b/internal/lsp/types/types.go index e083cf1f..066bb4f7 100644 --- a/internal/lsp/types/types.go +++ b/internal/lsp/types/types.go @@ -435,10 +435,10 @@ type TextDocumentContentChangeEvent struct { type Diagnostic struct { CodeDescription *CodeDescription `json:"codeDescription,omitempty"` Message string `json:"message"` - Source string `json:"source"` - Code string `json:"code"` + Source *string `json:"source,omitempty"` + Code string `json:"code"` // spec says optional integer or string Range Range `json:"range"` - Severity uint `json:"severity"` + Severity *uint `json:"severity,omitempty"` } type CodeDescription struct { diff --git a/internal/parse/parse.go b/internal/parse/parse.go index e181e547..006698a5 100644 --- a/internal/parse/parse.go +++ b/internal/parse/parse.go @@ -32,25 +32,18 @@ var attemptVersionOrder = [2]ast.RegoVersion{ast.RegoV1, ast.RegoV0} // ModuleWithOpts parses a module with the given options. If the Rego version is unknown, the function // may attempt to run several parser versions to determine the correct version. Setting the Rego version // in the parser options will skip this step, and is recommended whenever possible. -func ModuleWithOpts(path, policy string, opts ast.ParserOptions) (*ast.Module, error) { - var ( - module *ast.Module - err error - ) - +func ModuleWithOpts(path, policy string, opts ast.ParserOptions) (module *ast.Module, err error) { if opts.RegoVersion == ast.RegoUndefined && strings.HasSuffix(path, "_v0.rego") { opts.RegoVersion = ast.RegoV0 } if opts.RegoVersion != ast.RegoUndefined { - module, err = ast.ParseModuleWithOpts(path, policy, opts) - if err != nil { + if module, err = ast.ParseModuleWithOpts(path, policy, opts); err != nil { return nil, err //nolint:wrapcheck } } else { // We are parsing for an unknown Rego version - module, err = ModuleUnknownVersionWithOpts(path, policy, opts) - if err != nil { + if module, err = ModuleUnknownVersionWithOpts(path, policy, opts); err != nil { return nil, err } } @@ -63,11 +56,7 @@ func ModuleWithOpts(path, policy string, opts ast.ParserOptions) (*ast.Module, e // which parser was successful. Note that this is not 100% accurate, and the conditions for determining the // version may change over time. If the version is known beforehand, use ModuleWithOpts instead, and provide // the target Rego version in the parser options. -func ModuleUnknownVersionWithOpts( - filename string, - policy string, - opts ast.ParserOptions, -) (*ast.Module, error) { +func ModuleUnknownVersionWithOpts(filename, policy string, opts ast.ParserOptions) (*ast.Module, error) { var ( err error mod *ast.Module @@ -124,6 +113,8 @@ func Module(filename, policy string) (*ast.Module, error) { } // PrepareAST prepares the AST to be used as linter input. +// Deprecated: New code should use the `transform` package from roast, as this avoids an +// expensive intermediate step in module -> ast.Value conversions. func PrepareAST(name string, content string, module *ast.Module) (map[string]any, error) { var preparedAST map[string]any diff --git a/internal/util/ast.go b/internal/util/ast.go deleted file mode 100644 index f1f63454..00000000 --- a/internal/util/ast.go +++ /dev/null @@ -1,18 +0,0 @@ -package util - -import ( - "strings" - - "github.com/open-policy-agent/opa/v1/ast" -) - -// UnquotedPath returns a slice of strings from a path without quotes. -// e.g. data.foo["bar"] -> ["foo", "bar"], note that the data is not included. -func UnquotedPath(path ast.Ref) []string { - ret := make([]string, 0, len(path)-1) - for _, ref := range path[1:] { - ret = append(ret, strings.Trim(ref.String(), `"`)) - } - - return ret -} diff --git a/internal/util/util.go b/internal/util/util.go index a997d21e..468effc0 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -260,3 +260,7 @@ func FreePort(preferred ...int) (port int, err error) { return 0, fmt.Errorf("failed to find free port: %w", err) } + +func Pointer[T any](v T) *T { + return &v +} diff --git a/pkg/builtins/builtins.go b/pkg/builtins/builtins.go index 10ac1bca..09020fd0 100644 --- a/pkg/builtins/builtins.go +++ b/pkg/builtins/builtins.go @@ -16,7 +16,7 @@ import ( "github.com/styrainc/regal/internal/parse" - "github.com/styrainc/roast/pkg/encoding" + "github.com/styrainc/roast/pkg/transform" ) // RegalParseModuleMeta metadata for regal.parse_module. @@ -86,27 +86,17 @@ func RegalParseModule(_ rego.BuiltinContext, filename *ast.Term, policy *ast.Ter opts.RegoVersion = ast.RegoV0 } - module, err := ast.ParseModuleWithOpts(filenameStr, policyStr, opts) + mod, err := ast.ParseModuleWithOpts(filenameStr, policyStr, opts) if err != nil { return nil, err } - enhancedAST, err := parse.PrepareAST(filenameStr, policyStr, module) + roast, err := transform.ToAST(filenameStr, policyStr, mod, false) if err != nil { return nil, err } - roast, err := encoding.JSON().MarshalToString(enhancedAST) - if err != nil { - return nil, err - } - - term, err := ast.ParseTerm(roast) - if err != nil { - return nil, err - } - - return term, nil + return ast.NewTerm(roast), nil } // RegalLast regal.last returns the last element of an array. diff --git a/pkg/linter/linter.go b/pkg/linter/linter.go index 5bf8f4dd..77153ad0 100644 --- a/pkg/linter/linter.go +++ b/pkg/linter/linter.go @@ -35,6 +35,7 @@ import ( "github.com/styrainc/regal/pkg/rules" "github.com/styrainc/roast/pkg/encoding" + "github.com/styrainc/roast/pkg/rast" "github.com/styrainc/roast/pkg/transform" rutil "github.com/styrainc/roast/pkg/util" ) @@ -618,7 +619,7 @@ func (l Linter) validate(conf *config.Config) error { // Add all built-in rules for _, b := range l.ruleBundles { for _, module := range b.Modules { - parts := util.UnquotedPath(module.Parsed.Package.Path) + parts := rast.UnquotedPath(module.Parsed.Package.Path) // 1 2 3 4 // regal.rules.cat.rule if len(parts) != 4 { @@ -632,7 +633,7 @@ func (l Linter) validate(conf *config.Config) error { // Add any custom rules for _, module := range l.customRuleModules { - parts := util.UnquotedPath(module.Package.Path) + parts := rast.UnquotedPath(module.Package.Path) // 1 2 3 4 5 // custom.regal.rules.cat.rule if len(parts) != 5 { @@ -746,10 +747,7 @@ func (l Linter) prepareRegoArgs(query ast.Body) ([]func(*rego.Rego), error) { } if l.printHook != nil { - regoArgs = append(regoArgs, - rego.EnablePrintStatements(true), - rego.PrintHook(l.printHook), - ) + regoArgs = append(regoArgs, rego.EnablePrintStatements(true), rego.PrintHook(l.printHook)) } if l.instrumentation { @@ -826,9 +824,7 @@ func (l Linter) lintWithRegoRules( return } - evalArgs := []rego.EvalOption{ - rego.EvalParsedInput(inputValue), - } + evalArgs := []rego.EvalOption{rego.EvalParsedInput(inputValue)} if l.baseCache != nil { evalArgs = append(evalArgs, rego.EvalBaseCache(l.baseCache)) @@ -838,16 +834,16 @@ func (l Linter) lintWithRegoRules( evalArgs = append(evalArgs, rego.EvalMetrics(l.metrics)) } + if l.instrumentation { + evalArgs = append(evalArgs, rego.EvalInstrument(true)) + } + var prof *profiler.Profiler if l.profiling { prof = profiler.New() evalArgs = append(evalArgs, rego.EvalQueryTracer(prof)) } - if l.instrumentation { - evalArgs = append(evalArgs, rego.EvalInstrument(true)) - } - resultSet, err := l.preparedQuery.Eval(ctx, evalArgs...) if err != nil { errCh <- fmt.Errorf("error encountered in query evaluation %w", err) diff --git a/pkg/linter/linter_test.go b/pkg/linter/linter_test.go index 8b61d65a..48104fea 100644 --- a/pkg/linter/linter_test.go +++ b/pkg/linter/linter_test.go @@ -728,7 +728,9 @@ import data.unresolved`, } // 930767688 ns/op 2765064504 B/op 50859905 allocs/op OPA v1.5.0 -// 948058583 ns/op 2826178208 B/op 51937635 allocs/op“ OPA v1.5.1 +// 948058583 ns/op 2826178208 B/op 51937635 allocs/op OPA v1.5.1 +// 952606688 ns/op 2808314460 B/op 51658499 allocs/op +// 892354312 ns/op 2669512068 B/op 48780541 allocs/op // ... func BenchmarkRegalLintingItself(b *testing.B) { conf, err := config.FromPath(filepath.Join("..", "..", ".regal", "config.yaml")) @@ -785,6 +787,7 @@ func BenchmarkRegalLintingItselfPrepareOnce(b *testing.B) { } // 6 168875708 ns/op 455586606 B/op 8537889 allocs/op +// 8 139592615 ns/op 326694376 B/op 5996314 allocs/op // ... func BenchmarkRegalNoEnabledRules(b *testing.B) { linter := NewLinter(). @@ -836,8 +839,6 @@ func BenchmarkRegalNoEnabledRulesPrepareOnce(b *testing.B) { // Runs a separate benchmark for each rule in the bundle. Note that this will take *several* minutes to run, // meaning you do NOT want to do this more than occasionally. You may however find it helpful to use this with // a single, or handful of rules to get a better idea of how long they take to run, and relative to each other. -// The reason why this is currently so slow is that we parse + compile everything for each rule, which is really -// something we should fix: https://github.com/StyraInc/regal/issues/1394 func BenchmarkEachRule(b *testing.B) { conf, err := config.LoadConfigWithDefaultsFromBundle(&bundle.LoadedBundle, nil) if err != nil { diff --git a/pkg/version/version.go b/pkg/version/version.go index 4351d47e..8a2780ab 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,34 +1,29 @@ package version import ( + "cmp" "runtime" "strings" ) -// Version stores the version of Regal and is injected at build time. -var Version = "" +const platform = runtime.GOOS + "/" + runtime.GOARCH -// Additional Regal metadata to be injected at build time. var ( + // Values injected at build time using -ldflags. + Version = "" Commit = "" Timestamp = "" Hostname = "" -) - -// goVersion is the version of Go this was built with. -var goVersion = runtime.Version() -// platform is the runtime OS and architecture of this OPA binary. -const platform = runtime.GOOS + "/" + runtime.GOARCH + // The version of Go Regal was built with. + goVersion = runtime.Version() +) // Info wraps the various version metadata values and provides a means of marshalling as JSON or pretty string. type Info struct { - Version string `json:"version"` - + Version string `json:"version"` GoVersion string `json:"go_version"` - - Platform string `json:"platform"` - + Platform string `json:"platform"` Commit string `json:"commit"` Timestamp string `json:"timestamp"` Hostname string `json:"hostname"` @@ -50,19 +45,11 @@ func (vi Info) String() string { func New() Info { return Info{ - Version: unknownString(Version), + Version: cmp.Or(Version, "unknown"), GoVersion: goVersion, Platform: platform, - Commit: unknownString(Commit), - Timestamp: unknownString(Timestamp), - Hostname: unknownString(Hostname), - } -} - -func unknownString(s string) string { - if s == "" { - return "unknown" + Commit: cmp.Or(Commit, "unknown"), + Timestamp: cmp.Or(Timestamp, "unknown"), + Hostname: cmp.Or(Hostname, "unknown"), } - - return s }