diff --git a/bundle/regal/lsp/completion/location/location_test.rego b/bundle/regal/lsp/completion/location/location_test.rego index 6c5ae894..5718fe8d 100644 --- a/bundle/regal/lsp/completion/location/location_test.rego +++ b/bundle/regal/lsp/completion/location/location_test.rego @@ -1,8 +1,6 @@ package regal.lsp.completion.location_test import data.regal.ast -import data.regal.capabilities -import data.regal.config import data.regal.lsp.completion.location @@ -70,9 +68,7 @@ another if { [{"row": 16, "col": 1}, {"x", "y", "z"}], } - r := location.find_locals(module.rules, loc) with input as module - with input.regal.file.lines as lines - with config.capabilities as capabilities.provided + r := location.find_locals(module.rules, loc) with input as module with input.regal.file.lines as lines r == want } diff --git a/bundle/regal/lsp/completion/main.rego b/bundle/regal/lsp/completion/main.rego index 61e5e44b..8b0791a7 100644 --- a/bundle/regal/lsp/completion/main.rego +++ b/bundle/regal/lsp/completion/main.rego @@ -3,16 +3,22 @@ # base package for completion suggestion provider policies, and acts # like a router that collects suggestions from all provider policies # under regal.lsp.completion.providers +# related_resources: +# - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion +# schemas: +# - input: schema.regal.lsp.common +# - input.params: schema.regal.lsp.completion +# scope: subpackages package regal.lsp.completion import data.regal.util -# regal ignore:pointless-import -import data.regal.lsp.completion.location - # METADATA # entrypoint: true -result["response"] := items +result["response"] := { + "items": items, + "isIncomplete": true, +} # METADATA # description: main entry point for completion suggestions @@ -32,14 +38,12 @@ items contains object.union(completion, {"_regal": {"provider": provider}}) if { # description: | # checks if the current position is inside a comment inside_comment if { - position := location.to_position(input.regal.context.location) - # avoid unmarshalling every comment location but only one that starts # with the line number of the current position - line := sprintf("%d:", [position.line + 1]) + line := sprintf("%d:", [input.params.position.line + 1]) - some comment in data.workspace.parsed[input.regal.file.uri].comments + some comment in data.workspace.parsed[input.params.textDocument.uri].comments startswith(comment.location, line) - util.to_location_no_text(comment.location).col <= position.character + 1 + util.to_location_no_text(comment.location).col <= input.params.position.character + 1 } diff --git a/bundle/regal/lsp/completion/main_test.rego b/bundle/regal/lsp/completion/main_test.rego index 2e35c27d..8140fbd9 100644 --- a/bundle/regal/lsp/completion/main_test.rego +++ b/bundle/regal/lsp/completion/main_test.rego @@ -9,26 +9,26 @@ test_completion_entrypoint if { } test_inside_comment if { - _data := {"file:///example.rego": {"comments": [ + _data := {"file:///p.rego": {"comments": [ {"location": "2:1:2:10"}, {"location": "4:1:4:10"}, ]}} - _input := {"regal": { - "context": {"location": {"row": 4, "col": 5}}, - "file": {"uri": "file:///example.rego"}, + _input := {"params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 4}, }} completion.inside_comment with input as _input with data.workspace.parsed as _data } test_not_inside_comment if { - _data := {"file:///example.rego": {"comments": [ + _data := {"file:///p.rego": {"comments": [ {"location": "2:1:2:10"}, {"location": "4:8:4:10"}, ]}} - _input := {"regal": { - "context": {"location": {"row": 4, "col": 5}}, - "file": {"uri": "file:///example.rego"}, + _input := {"params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 4}, }} not completion.inside_comment with input as _input with data.workspace.parsed as _data diff --git a/bundle/regal/lsp/completion/providers/booleans/booleans.rego b/bundle/regal/lsp/completion/providers/booleans/booleans.rego index 79e1ac81..7bf15f3c 100644 --- a/bundle/regal/lsp/completion/providers/booleans/booleans.rego +++ b/bundle/regal/lsp/completion/providers/booleans/booleans.rego @@ -8,9 +8,7 @@ import data.regal.lsp.completion.location # METADATA # description: completion suggestions for true/false items contains item if { - position := location.to_position(input.regal.context.location) - - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] line != "" words := regex.split(`\s+`, line) @@ -20,7 +18,7 @@ items contains item if { previous_word in {"==", ":="} - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) some b in ["true", "false"] @@ -31,7 +29,7 @@ items contains item if { "kind": kind.constant, "detail": "boolean value", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": b, }, } diff --git a/bundle/regal/lsp/completion/providers/booleans/booleans_test.rego b/bundle/regal/lsp/completion/providers/booleans/booleans_test.rego index 0abd0295..b5ed9215 100644 --- a/bundle/regal/lsp/completion/providers/booleans/booleans_test.rego +++ b/bundle/regal/lsp/completion/providers/booleans/booleans_test.rego @@ -8,16 +8,13 @@ test_suggested_in_head if { allow := f`} - regal_module := {"regal": { - "file": { - "name": "p.rego", - "lines": split(workspace["file:///p.rego"], "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 2, "character": 9}, }, - "context": {"location": { - "row": 3, - "col": 10, - }}, - }} + "regal": {"file": {"lines": split(workspace["file:///p.rego"], "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) @@ -35,16 +32,13 @@ allow if { foo := t }`} - regal_module := {"regal": { - "file": { - "name": "p.rego", - "lines": split(workspace["file:///p.rego"], "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 9}, }, - "context": {"location": { - "row": 4, - "col": 10, - }}, - }} + "regal": {"file": {"lines": split(workspace["file:///p.rego"], "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) @@ -62,16 +56,13 @@ allow if { foo == t }`} - regal_module := {"regal": { - "file": { - "name": "p.rego", - "lines": split(workspace["file:///p.rego"], "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 9}, }, - "context": {"location": { - "row": 4, - "col": 10, - }}, - }} + "regal": {"file": {"lines": split(workspace["file:///p.rego"], "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) @@ -89,16 +80,13 @@ allow if { t }`} - regal_module := {"regal": { - "file": { - "name": "p.rego", - "lines": split(workspace["file:///p.rego"], "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 2}, }, - "context": {"location": { - "row": 4, - "col": 3, - }}, - }} + "regal": {"file": {"lines": split(workspace["file:///p.rego"], "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) diff --git a/bundle/regal/lsp/completion/providers/builtins/builtins.rego b/bundle/regal/lsp/completion/providers/builtins/builtins.rego index 3804f243..95492bbd 100644 --- a/bundle/regal/lsp/completion/providers/builtins/builtins.rego +++ b/bundle/regal/lsp/completion/providers/builtins/builtins.rego @@ -9,14 +9,13 @@ import data.regal.lsp.template # METADATA # description: suggest built-in functions matching typed ref items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] line != "" not startswith(line, "default ") location.in_rule_body(line) - ref := location.ref_at(line, input.regal.context.location.col) + ref := location.ref_at(line, input.params.position.character + 1) some builtin in data.workspace.builtins @@ -29,7 +28,7 @@ items contains item if { "label": builtin.name, "kind": kind.function, "detail": "built-in function", - "textEdit": {"range": location.word_range(ref, position), "newText": builtin.name}, + "textEdit": {"range": location.word_range(ref, input.params.position), "newText": builtin.name}, "documentation": {"kind": "markdown", "value": template.render_for_builtin(builtin)}, } } diff --git a/bundle/regal/lsp/completion/providers/builtins/builtins_test.rego b/bundle/regal/lsp/completion/providers/builtins/builtins_test.rego index f0d4213a..9b322584 100644 --- a/bundle/regal/lsp/completion/providers/builtins/builtins_test.rego +++ b/bundle/regal/lsp/completion/providers/builtins/builtins_test.rego @@ -3,16 +3,19 @@ package regal.lsp.completion.providers.builtins_test import data.regal.lsp.completion.providers.builtins test_simple_builtin_completion if { - items := builtins.items with data.workspace.builtins as _builtins with input as {"regal": { - "context": {"location": {"row": 4, "col": 11}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as _builtins with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 10}, + }, + "regal": {"file": {"lines": [ "package p", "", "allow if {", " b := c", "}", - ]}, - }} + ]}}, + } items == { { @@ -51,16 +54,19 @@ test_simple_builtin_completion if { } test_simple_builtin_completion_single_match if { - items := builtins.items with data.workspace.builtins as _builtins with input as {"regal": { - "context": {"location": {"row": 4, "col": 12}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as _builtins with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 11}, + }, + "regal": {"file": {"lines": [ "package p", "", "allow if {", " b := co", "}", - ]}, - }} + ]}}, + } items == {{ "detail": "built-in function", @@ -81,16 +87,19 @@ test_simple_builtin_completion_single_match if { } test_simple_builtin_completion_single_match_longer_ref if { - items := builtins.items with data.workspace.builtins as _builtins with input as {"regal": { - "context": {"location": {"row": 4, "col": 18}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as _builtins with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 17}, + }, + "regal": {"file": {"lines": [ "package p", "", "allow if {", " b := crypto.h", "}", - ]}, - }} + ]}}, + } items == {{ "detail": "built-in function", @@ -112,45 +121,54 @@ test_simple_builtin_completion_single_match_longer_ref if { test_no_completion_of_deprecated_builtin if { builtins_deprecated := [object.union(_builtins[0], {"deprecated": true})] - items := builtins.items with data.workspace.builtins as builtins_deprecated with input as {"regal": { - "context": {"location": {"row": 4, "col": 11}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as builtins_deprecated with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 10}, + }, + "regal": {"file": {"lines": [ "package p", "", "allow if {", " b := c", "}", - ]}, - }} + ]}}, + } count(items) == 0 } test_no_completion_of_infix_builtin if { builtins_deprecated := [object.union(_builtins[0], {"infix": "🔄"})] - items := builtins.items with data.workspace.builtins as builtins_deprecated with input as {"regal": { - "context": {"location": {"row": 4, "col": 11}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as builtins_deprecated with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 3, "character": 10}, + }, + "regal": {"file": {"lines": [ "package p", "", "allow if {", " b := c", "}", - ]}, - }} + ]}}, + } count(items) == 0 } test_no_completion_in_default_rule if { - items := builtins.items with data.workspace.builtins as _builtins with input as {"regal": { - "context": {"location": {"row": 3, "col": 17}}, - "file": {"lines": [ + items := builtins.items with data.workspace.builtins as _builtins with input as { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 2, "character": 16}, + }, + "regal": {"file": {"lines": [ "package p", "", "default foo := c", - ]}, - }} + ]}}, + } count(items) == 0 } diff --git a/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego b/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego index 8415fe0f..9894dd81 100644 --- a/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego +++ b/bundle/regal/lsp/completion/providers/commonrule/commonrule.rego @@ -15,8 +15,7 @@ _suggested_names := { # METADATA # description: all completion suggestions for common rule names items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] some label in _suggested_names @@ -31,7 +30,7 @@ items contains item if { "value": sprintf("%q is a common rule name", [label]), }, "textEdit": { - "range": location.from_start_of_line_to_position(position), + "range": location.from_start_of_line_to_position(input.params.position), "newText": concat("", [label, " "]), }, } diff --git a/bundle/regal/lsp/completion/providers/default/default.rego b/bundle/regal/lsp/completion/providers/default/default.rego index c6c42b4d..28195590 100644 --- a/bundle/regal/lsp/completion/providers/default/default.rego +++ b/bundle/regal/lsp/completion/providers/default/default.rego @@ -11,8 +11,7 @@ import data.regal.lsp.completion.location # description: all completion suggestions for default keyword # scope: document items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith("default", line) @@ -21,15 +20,14 @@ items contains item if { "kind": kind.keyword, "detail": "default := ", "textEdit": { - "range": location.from_start_of_line_to_position(position), + "range": location.from_start_of_line_to_position(input.params.position), "newText": "default ", }, } } items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith("default", line) @@ -40,7 +38,7 @@ items contains item if { "kind": kind.keyword, "detail": sprintf("add default assignment for %s rule", [name]), "textEdit": { - "range": location.from_start_of_line_to_position(position), + "range": location.from_start_of_line_to_position(input.params.position), "newText": sprintf("default %s := ", [name]), }, } diff --git a/bundle/regal/lsp/completion/providers/import/import.rego b/bundle/regal/lsp/completion/providers/import/import.rego index 5e4a5d6c..6c6c4133 100644 --- a/bundle/regal/lsp/completion/providers/import/import.rego +++ b/bundle/regal/lsp/completion/providers/import/import.rego @@ -8,19 +8,18 @@ import data.regal.lsp.completion.location # METADATA # description: all completion suggestions for the import keyword items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith("import", line) - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) item := { "label": "import", "kind": kind.keyword, "detail": "import ", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": "import ", }, } diff --git a/bundle/regal/lsp/completion/providers/input/input.rego b/bundle/regal/lsp/completion/providers/input/input.rego index b7e69297..dc650fee 100644 --- a/bundle/regal/lsp/completion/providers/input/input.rego +++ b/bundle/regal/lsp/completion/providers/input/input.rego @@ -8,13 +8,12 @@ import data.regal.lsp.completion.location # METADATA # description: all completion suggestions for the input keyword items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] line != "" location.in_rule_body(line) - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) startswith("input", word.text) @@ -27,7 +26,7 @@ items contains item if { "kind": kind.keyword, "detail": "input document", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": "input", }, "documentation": { diff --git a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego index 6e3b1dec..12b54196 100644 --- a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego +++ b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson.rego @@ -16,13 +16,12 @@ import data.regal.lsp.completion.kind import data.regal.lsp.completion.location # METADATA -# description: items contains found suggestions from `input.json`` +# description: items contains found suggestions from `input.json` items contains item if { - input.regal.context.input_dot_json_path + input.regal.environment.input_dot_json_path - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] - word := location.ref_at(line, input.regal.context.location.col) + line := input.regal.file.lines[input.params.position.line] + word := location.ref_at(line, input.params.position.character + 1) some [suggestion, type] in _matching_input_suggestions @@ -32,23 +31,22 @@ items contains item if { "detail": type, "documentation": { "kind": "markdown", - "value": sprintf("(inferred from [`input.json`](%s))", [input.regal.context.input_dot_json_path]), + "value": sprintf("(inferred from [`input.json`](%s))", [input.regal.environment.input_dot_json_path]), }, "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": suggestion, }, } } _matching_input_suggestions contains [suggestion, type] if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] line != "" location.in_rule_body(line) - word := location.ref_at(line, input.regal.context.location.col) + word := location.ref_at(line, input.params.position.character + 1) some [suggestion, type] in _input_paths @@ -56,7 +54,7 @@ _matching_input_suggestions contains [suggestion, type] if { } _input_paths contains [input_path, input_type] if { - walk(input.regal.context.input_dot_json, [path, value]) + walk(input.regal.environment.input_dot_json, [path, value]) count(path) > 0 diff --git a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson_test.rego b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson_test.rego index dc721c39..21707834 100644 --- a/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson_test.rego +++ b/bundle/regal/lsp/completion/providers/inputdotjson/inputdotjson_test.rego @@ -57,43 +57,45 @@ test_matching_input_suggestions if { } test_not_matching_input_suggestions if { - input_obj_new_loc := object.union(input_obj, {"regal": {"context": {"location": { - "row": 1, - "col": 1, - }}}}) + input_obj_new_loc := object.union(input_obj, {"params": { + "textDocument": {"uri": "file:///example.rego"}, + "position": {"line": 0, "character": 0}, + }}) items := provider.items with input as input_obj_new_loc items == set() } -input_obj := {"regal": { - "context": { - "location": { - "row": 6, - "col": 12, - }, - "input_dot_json": { - "user": { - "name": { - "first": "John", - "last": "Doe", +input_obj := { + "params": { + "textDocument": {"uri": "file:///example.rego"}, + "position": {"line": 5, "character": 11}, + }, + "regal": { + "environment": { + "input_dot_json": { + "user": { + "name": { + "first": "John", + "last": "Doe", + }, + "email": "john@doe.com", + "roles": [{"name": "admin"}, {"name": "user"}], + }, + "request": { + "method": "GET", + "url": "https://example.com", }, - "email": "john@doe.com", - "roles": [{"name": "admin"}, {"name": "user"}], - }, - "request": { - "method": "GET", - "url": "https://example.com", }, + "input_dot_json_path": "/foo/bar/input.json", }, - "input_dot_json_path": "/foo/bar/input.json", + "file": {"lines": [ + "package p", + "", + "import rego.v1", + "", + "allow if {", + " f(input.r", + "}", + ]}, }, - "file": {"lines": [ - "package p", - "", - "import rego.v1", - "", - "allow if {", - " f(input.r", - "}", - ]}, -}} +} diff --git a/bundle/regal/lsp/completion/providers/locals/locals.rego b/bundle/regal/lsp/completion/providers/locals/locals.rego index cb133ec3..6d7c19be 100644 --- a/bundle/regal/lsp/completion/providers/locals/locals.rego +++ b/bundle/regal/lsp/completion/providers/locals/locals.rego @@ -8,20 +8,20 @@ import data.regal.lsp.completion.location # METADATA # description: completion suggestions for local symbols items contains item if { - position := location.to_position(input.regal.context.location) - - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] line != "" location.in_rule_body(line) + not _excluded(line, input.params.position) - not _excluded(line, position) - - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) not endswith(word.text_before, ".") - some local in location.find_locals(data.workspace.parsed[input.regal.file.uri].rules, input.regal.context.location) + some local in location.find_locals(data.workspace.parsed[input.params.textDocument.uri].rules, { + "row": input.params.position.line + 1, + "col": input.params.position.character + 1, + }) startswith(local, word.text) @@ -32,7 +32,7 @@ items contains item if { "kind": kind.variable, "detail": "local variable", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": local, }, } diff --git a/bundle/regal/lsp/completion/providers/locals/locals_test.rego b/bundle/regal/lsp/completion/providers/locals/locals_test.rego index 24067cbe..50219d1c 100644 --- a/bundle/regal/lsp/completion/providers/locals/locals_test.rego +++ b/bundle/regal/lsp/completion/providers/locals/locals_test.rego @@ -17,17 +17,17 @@ bar if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", - "lines": split(workspace["file:///p.rego"], "\n"), + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 7, "character": 8}, }, - "context": {"location": { - "row": 8, - "col": 9, + "regal": {"file": { + "uri": "file:///p.rego", + "lines": split(workspace["file:///p.rego"], "\n"), }}, - }} - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + } + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 0 } @@ -45,19 +45,18 @@ function(bar) if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 8, "character": 9}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(workspace["file:///p.rego"], "\n"), - }, - "context": {"location": { - "row": 9, - "col": 10, }}, - }} + } - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 2 _expect_item(items, "bar", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}}) @@ -77,19 +76,18 @@ function(bar) if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 8, "character": 24}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(workspace["file:///p.rego"], "\n"), - }, - "context": {"location": { - "row": 9, - "col": 25, }}, - }} + } - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 2 _expect_item(items, "bar", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}}) @@ -106,18 +104,17 @@ function(bar) := f if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 4, "character": 18}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(workspace["file:///p.rego"], "\n"), - }, - "context": {"location": { - "row": 5, - "col": 19, }}, - }} - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + } + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 1 _expect_item(items, "foo", {"end": {"character": 18, "line": 4}, "start": {"character": 17, "line": 4}}) @@ -133,18 +130,17 @@ function() if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 4, "character": 9}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(workspace["file:///p.rego"], "\n"), - }, - "context": {"location": { - "row": 5, - "col": 10, }}, - }} - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + } + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 0 } @@ -160,18 +156,17 @@ allow if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 6, "character": 18}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(workspace["file:///p.rego"], "\n"), - }, - "context": {"location": { - "row": 7, - "col": 19, }}, - }} - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + } + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) util.single_set_item(items).label == "xyz" } @@ -185,19 +180,18 @@ no_completion if { } `} - regal_module := {"regal": { - "file": { - "name": "p.rego", + _input := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": 4, "character": 7}, + }, + "regal": {"file": { "uri": "file:///p.rego", "lines": split(replace(workspace["file:///p.rego"], "input", "input."), "\n"), - }, - "context": {"location": { - "row": 5, - "col": 8, }}, - }} + } - items := provider.items with input as regal_module with data.workspace.parsed as utils.parsed_modules(workspace) + items := provider.items with input as _input with data.workspace.parsed as utils.parsed_modules(workspace) count(items) == 0 } diff --git a/bundle/regal/lsp/completion/providers/package/package.rego b/bundle/regal/lsp/completion/providers/package/package.rego index 26f98bdd..5f94c7d7 100644 --- a/bundle/regal/lsp/completion/providers/package/package.rego +++ b/bundle/regal/lsp/completion/providers/package/package.rego @@ -10,17 +10,14 @@ import data.regal.lsp.completion.location items contains item if { not strings.any_prefix_match(input.regal.file.lines, "package ") - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] - - startswith("package", line) + startswith("package", input.regal.file.lines[input.params.position.line]) item := { "label": "package", "kind": kind.keyword, "detail": "package ", "textEdit": { - "range": location.from_start_of_line_to_position(position), + "range": location.from_start_of_line_to_position(input.params.position), "newText": "package ", }, } diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename.rego b/bundle/regal/lsp/completion/providers/packagename/packagename.rego index ac559727..733ebb71 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename.rego @@ -10,19 +10,18 @@ import data.regal.lsp.completion.location # METADATA # description: set of suggested package names items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith(line, "package ") - position.character > 7 + input.params.position.character > 7 ps := input.regal.environment.path_separator - abs_dir := _base(input.regal.file.name) - rel_dir := trim_prefix(abs_dir, input.regal.context.workspace_root) + abs_dir := _base(input.params.textDocument.uri) + rel_dir := trim_prefix(abs_dir, input.regal.environment.workspace_root_path) fix_dir := replace(replace(trim_prefix(rel_dir, ps), ".", "_"), ps, ".") - word := location.ref_at(line, input.regal.context.location.col) + word := location.ref_at(line, input.params.position.character + 1) some suggestion in _suggestions(fix_dir, word.text) @@ -31,13 +30,16 @@ items contains item if { "kind": kind.folder, "detail": "suggested package name based on directory structure", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": concat("", [suggestion, "\n\n"]), }, } } -_base(path) := substring(path, 0, regal.last(indexof_n(path, "/"))) +_base(uri) := base if { + path := trim_prefix(uri, "file://") + base := substring(path, 0, regal.last(indexof_n(path, input.regal.environment.path_separator))) +} _suggestions(dir, text) := [path | parts := split(dir, ".") diff --git a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego index acbd35c2..66aa99f7 100644 --- a/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego +++ b/bundle/regal/lsp/completion/providers/packagename/packagename_test.rego @@ -4,21 +4,24 @@ import data.regal.lsp.completion.providers.packagename as provider test_package_name_completion_on_typing if { policy := `package f` - provider_input := {"regal": { - "file": { - "name": "/Users/joe/policy/foo/bar/baz/p.rego", - "lines": split(policy, "\n"), - }, - "context": { - "workspace_root": "/Users/joe/policy", - "location": { - "row": 1, - "col": 10, + provider_input := { + "params": { + "textDocument": {"uri": "file:///Users/joe/policy/foo/bar/baz/p.rego"}, + "position": {"line": 0, "character": 9}, + }, + "regal": { + "file": { + "lines": split(policy, "\n"), + "uri": "file:///Users/joe/policy/foo/bar/baz/p.rego", + }, + "environment": { + "path_separator": "/", + "workspace_root_path": "/Users/joe/policy", }, }, - "environment": {"path_separator": "/"}, - }} + } items := provider.items with input as provider_input + items == {{ "detail": "suggested package name based on directory structure", "kind": 19, @@ -35,21 +38,25 @@ test_package_name_completion_on_typing if { test_package_name_completion_on_typing_multiple_suggestions if { policy := `package b` - provider_input := {"regal": { - "file": { - "name": "/Users/joe/policy/foo/bar/baz/p.rego", - "lines": split(policy, "\n"), - }, - "context": { - "workspace_root": "/Users/joe/policy", - "location": { - "row": 1, - "col": 10, + provider_input := { + "params": { + "textDocument": {"uri": "file:///Users/joe/policy/foo/bar/baz/p.rego"}, + "position": {"line": 0, "character": 9}, + }, + "regal": { + "file": { + "lines": split(policy, "\n"), + "uri": "file:///Users/joe/policy/foo/bar/baz/p.rego", + }, + "environment": { + "path_separator": "/", + "workspace_root_path": "/Users/joe/policy", }, }, - "environment": {"path_separator": "/"}, - }} + } + items := provider.items with input as provider_input + items == { { "detail": "suggested package name based on directory structure", @@ -80,21 +87,24 @@ test_package_name_completion_on_typing_multiple_suggestions if { test_package_name_completion_on_typing_multiple_suggestions_when_invoked if { policy := `package ` - provider_input := {"regal": { - "file": { - "name": "/Users/joe/policy/foo/bar/baz/p.rego", - "lines": split(policy, "\n"), - }, - "context": { - "workspace_root": "/Users/joe/policy", - "location": { - "row": 1, - "col": 9, + provider_input := { + "params": { + "textDocument": {"uri": "file:///Users/joe/policy/foo/bar/baz/p.rego"}, + "position": {"line": 0, "character": 8}, + }, + "regal": { + "file": { + "lines": split(policy, "\n"), + "uri": "file:///Users/joe/policy/foo/bar/baz/p.rego", + }, + "environment": { + "path_separator": "/", + "workspace_root_path": "/Users/joe/policy", }, }, - "environment": {"path_separator": "/"}, - }} + } items := provider.items with input as provider_input + items == { { "detail": "suggested package name based on directory structure", @@ -137,22 +147,24 @@ test_package_name_completion_on_typing_multiple_suggestions_when_invoked if { test_package_name_quoted if { policy := `package f` - provider_input := {"regal": { - "file": { - "name": "/Users/joe/foo/bar/baz-are/foo/baz-are/foo/p.rego", - "lines": split(policy, "\n"), - }, - "context": { - "workspace_root": "/Users/joe/policy", - "location": { - "row": 1, - "col": 10, + provider_input := { + "params": { + "textDocument": {"uri": "file:////Users/joe/foo/bar/baz-are/foo/baz-are/foo/p.rego"}, + "position": {"line": 0, "character": 9}, + }, + "regal": { + "file": { + "lines": split(policy, "\n"), + "uri": "file:////Users/joe/foo/bar/baz-are/foo/baz-are/foo/p.rego", + }, + "environment": { + "path_separator": "/", + "workspace_root_path": "/Users/joe/policy", }, }, - "environment": {"path_separator": "/"}, - }} - + } items := provider.items with input as provider_input + items == { { "detail": "suggested package name based on directory structure", diff --git a/bundle/regal/lsp/completion/providers/packagerefs/packagerefs.rego b/bundle/regal/lsp/completion/providers/packagerefs/packagerefs.rego index f7bcf577..2b77df56 100644 --- a/bundle/regal/lsp/completion/providers/packagerefs/packagerefs.rego +++ b/bundle/regal/lsp/completion/providers/packagerefs/packagerefs.rego @@ -10,12 +10,11 @@ import data.regal.lsp.completion.location # METADATA # description: suggest packages matching typed import ref items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith(line, "import ") - ref := location.ref_at(line, input.regal.context.location.col) + ref := location.ref_at(line, input.params.position.character + 1) startswith(ref.text, "d") @@ -28,7 +27,7 @@ items contains item if { "kind": kind.module, "detail": "package", "textEdit": { - "range": location.word_range(ref, position), + "range": location.word_range(ref, input.params.position), "newText": path, }, # tell clients to sort paths first by the number of path components (shortest first), @@ -41,7 +40,7 @@ _package_paths contains str if { some uri path := data.workspace.parsed[uri].package.path - uri != input.regal.file.uri # don't suggest the package of the current file + uri != input.params.textDocument.uri # don't suggest the package of the current file not endswith(regal.last(path).value, "_test") # importing tests makes no sense str := ast.ref_to_string(path) diff --git a/bundle/regal/lsp/completion/providers/packagerefs/packagerefs_test.rego b/bundle/regal/lsp/completion/providers/packagerefs/packagerefs_test.rego index 8a3cac6f..122f6c24 100644 --- a/bundle/regal/lsp/completion/providers/packagerefs/packagerefs_test.rego +++ b/bundle/regal/lsp/completion/providers/packagerefs/packagerefs_test.rego @@ -3,17 +3,17 @@ package regal.lsp.completion.providers.packagerefs_test import data.regal.lsp.completion.providers.packagerefs test_all_package_refs_sugggested_for_import if { - items := packagerefs.items with data.workspace.parsed as _workspace_parsed with input as {"regal": { - "context": {"location": {"row": 3, "col": 9}}, - "file": { - "uri": "file:///example.rego", - "lines": [ - "package foo.bar", - "", - "import d", - ], + items := packagerefs.items with data.workspace.parsed as _workspace_parsed with input as { + "params": { + "textDocument": {"uri": "file:///example.rego"}, + "position": {"line": 2, "character": 8}, }, - }} + "regal": {"file": {"lines": [ + "package foo.bar", + "", + "import d", + ]}}, + } # 6 suggestions minus the current package and one test package # also note how the sortText attribute hints to the client to sort not by @@ -28,17 +28,20 @@ test_all_package_refs_sugggested_for_import if { } test_matching_package_refs_sugggested_for_import if { - items := packagerefs.items with data.workspace.parsed as _workspace_parsed with input as {"regal": { - "context": {"location": {"row": 3, "col": 14}}, - "file": { + items := packagerefs.items with data.workspace.parsed as _workspace_parsed with input as { + "params": { + "textDocument": {"uri": "file:///example.rego"}, + "position": {"line": 2, "character": 13}, + }, + "regal": {"file": { "uri": "file:///example.rego", "lines": [ "package foo.bar", "", "import data.f", ], - }, - }} + }}, + } items == { _suggestion("data.foo.baz", "002", [2, 7, 2, 13]), diff --git a/bundle/regal/lsp/completion/providers/regov1/regov1.rego b/bundle/regal/lsp/completion/providers/regov1/regov1.rego index a2595d86..80db5a32 100644 --- a/bundle/regal/lsp/completion/providers/regov1/regov1.rego +++ b/bundle/regal/lsp/completion/providers/regov1/regov1.rego @@ -13,12 +13,11 @@ items contains item if { 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) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith(line, "import ") - word := location.ref_at(line, input.regal.context.location.col) + word := location.ref_at(line, input.params.position.character + 1) startswith("rego.v1", word.text) @@ -27,7 +26,7 @@ items contains item if { "kind": kind.module, "detail": "use rego.v1", "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": "rego.v1\n\n", }, } diff --git a/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword.rego b/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword.rego index daa31e73..0f92ae0a 100644 --- a/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword.rego +++ b/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword.rego @@ -16,21 +16,20 @@ import data.regal.lsp.completion.location # METADATA # description: Set of suggested completion items items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] not regex.match(`^\s+`, line) not startswith(line, "package") not startswith(line, "import") - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) _word_matches(word.text) some obj in _suggestions(word.text, _words_no_space(word.text_before)) item := object.union(obj, {"textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": concat("", [obj.label, " "]), }}) } diff --git a/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword_test.rego b/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword_test.rego index 8f2d20ee..2cb66636 100644 --- a/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword_test.rego +++ b/bundle/regal/lsp/completion/providers/ruleheadkeyword/ruleheadkeyword_test.rego @@ -3,20 +3,13 @@ package regal.lsp.completion.providers.ruleheadkeyword_test import data.regal.lsp.completion.providers.ruleheadkeyword as provider test_keyword_completion_after_rule_name_no_prefix[label] if { - items := provider.items with input as {"regal": { - "file": { - "name": "/ws/p.rego", - "lines": split("package p\n\nrule ", "\n"), + items := provider.items with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": 5}, }, - "context": { - "workspace_root": "/ws", - "location": { - "row": 3, - "col": 6, - }, - }, - "environment": {"path_separator": "/"}, - }} + "regal": {"file": {"lines": split("package p\n\nrule ", "\n")}}, + } count(items) == 3 @@ -33,20 +26,13 @@ test_keyword_completion_after_rule_name_no_prefix[label] if { } test_keyword_completion_after_rule_name_i_prefix_suggests_only_if if { - items := provider.items with input as {"regal": { - "file": { - "name": "/ws/p.rego", - "lines": split("package p\n\nrule i", "\n"), - }, - "context": { - "workspace_root": "/ws", - "location": { - "row": 3, - "col": 7, - }, + items := provider.items with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": 6}, }, - "environment": {"path_separator": "/"}, - }} + "regal": {"file": {"lines": split("package p\n\nrule i", "\n")}}, + } items == {object.union(provider.completions["if"], {"textEdit": { "newText": "if ", @@ -58,20 +44,13 @@ test_keyword_completion_after_rule_name_i_prefix_suggests_only_if if { } test_completion_after_contains_only_has_if if { - items := provider.items with input as {"regal": { - "file": { - "name": "/ws/p.rego", - "lines": split("package p\n\nrule contains 100 ", "\n"), - }, - "context": { - "workspace_root": "/ws", - "location": { - "row": 3, - "col": 19, - }, + items := provider.items with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": 18}, }, - "environment": {"path_separator": "/"}, - }} + "regal": {"file": {"lines": split("package p\n\nrule contains 100 ", "\n")}}, + } expected := {{ "kind": 14, diff --git a/bundle/regal/lsp/completion/providers/rulename/rulename.rego b/bundle/regal/lsp/completion/providers/rulename/rulename.rego index bf660e46..35436c64 100644 --- a/bundle/regal/lsp/completion/providers/rulename/rulename.rego +++ b/bundle/regal/lsp/completion/providers/rulename/rulename.rego @@ -14,14 +14,13 @@ import data.regal.lsp.completion.location items contains item if { count(input.regal.file.lines) > 1 - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] - word := location.word_at(line, input.regal.context.location.col) + line := input.regal.file.lines[input.params.position.line] + word := location.word_at(line, input.params.position.character + 1) not regex.match(`\s`, word.text_before) rules := {[name, _rule_kind(rule)] | - some rule in data.workspace.parsed[input.regal.file.uri].rules + some rule in data.workspace.parsed[input.params.textDocument.uri].rules name := ast.ref_static_to_string(rule.head.ref) not startswith(name, "test_") } @@ -35,7 +34,7 @@ items contains item if { "kind": kind, "detail": _kind_detail[kind], "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": concat("", [name, " "]), }, } diff --git a/bundle/regal/lsp/completion/providers/rulename/rulename_test.rego b/bundle/regal/lsp/completion/providers/rulename/rulename_test.rego index 4a2f3210..76446306 100644 --- a/bundle/regal/lsp/completion/providers/rulename/rulename_test.rego +++ b/bundle/regal/lsp/completion/providers/rulename/rulename_test.rego @@ -15,16 +15,13 @@ test_rule_name_completion[title] if { ] title := sprintf("typing '%s' suggests: %s", [case.typed, concat(", ", case.expect)]) - items := provider.items with data.workspace.parsed as cache with input as {"regal": { - "file": { - "lines": split(concat("", [above, case.typed, below]), "\n"), - "uri": "file:///ws/p.rego", + items := provider.items with data.workspace.parsed as cache with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": count(case.typed)}, }, - "context": {"location": { - "row": 3, - "col": count(case.typed) + 1, - }}, - }} + "regal": {"file": {"lines": split(concat("", [above, case.typed, below]), "\n")}}, + } count(items) == count(case.expect) @@ -38,16 +35,13 @@ test_rule_name_completion_only_start_of_line if { below := "\n\nconstant := 5\n\nfunction(_) := true\n\nrule if 1 + 1 == 3\n\nrule if true\n" cache := {"file:///ws/p.rego": regal.parse_module("p.rego", concat("", [above, below]))} typed := "foo r" - items := provider.items with data.workspace.parsed as cache with input as {"regal": { - "file": { - "lines": split(concat("", [above, typed, below]), "\n"), - "uri": "file:///ws/p.rego", + items := provider.items with data.workspace.parsed as cache with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": count(typed)}, }, - "context": {"location": { - "row": 3, - "col": count(typed) + 1, - }}, - }} + "regal": {"file": {"lines": split(concat("", [above, typed, below]), "\n")}}, + } count(items) == 0 } @@ -57,16 +51,13 @@ test_rule_name_completion_no_tests if { below := "\n\ntest_foo if true\n\n" cache := {"file:///ws/p.rego": regal.parse_module("p.rego", concat("", [above, below]))} typed := "t" - items := provider.items with data.workspace.parsed as cache with input as {"regal": { - "file": { - "lines": split(concat("", [above, typed, below]), "\n"), - "uri": "file:///ws/p.rego", + items := provider.items with data.workspace.parsed as cache with input as { + "params": { + "textDocument": {"uri": "file:///ws/p.rego"}, + "position": {"line": 2, "character": count(typed)}, }, - "context": {"location": { - "row": 3, - "col": count(typed) + 1, - }}, - }} + "regal": {"file": {"lines": split(concat("", [above, typed, below]), "\n")}}, + } count(items) == 0 } diff --git a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego index 6a8fd1ad..4bfa6e13 100644 --- a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego +++ b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego @@ -9,18 +9,16 @@ import data.regal.lsp.completion.location _ref_is_internal(ref) if contains(ref, "._") -_position := location.to_position(input.regal.context.location) +_line := input.regal.file.lines[input.params.position.line] -_line := input.regal.file.lines[_position.line] - -_word := location.ref_at(_line, input.regal.context.location.col) +_word := location.ref_at(_line, input.params.position.character + 1) _workspace_rule_refs contains ref if { some refs in data.workspace.defined_refs some ref in refs } -_parsed_current_file := data.workspace.parsed[input.regal.file.uri] +_parsed_current_file := data.workspace.parsed[input.params.textDocument.uri] _current_file_package := ast.ref_to_string(_parsed_current_file.package.path) @@ -103,7 +101,7 @@ items contains item if { "kind": kind.variable, "detail": "reference", "textEdit": { - "range": location.word_range(_word, _position), + "range": location.word_range(_word, input.params.position), "newText": ref, }, } diff --git a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs_test.rego b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs_test.rego index bf081285..ad683de6 100644 --- a/bundle/regal/lsp/completion/providers/rulerefs/rulerefs_test.rego +++ b/bundle/regal/lsp/completion/providers/rulerefs/rulerefs_test.rego @@ -5,7 +5,7 @@ import data.regal.ast import data.regal.lsp.completion.providers.rulerefs as provider workspace := { - "current_file.rego": `package foo + "file:///current_file.rego": `package foo import rego.v1 @@ -45,20 +45,16 @@ defined_refs[file_uri] contains concat(".", [package_name, ast.ref_to_string(rul } test_rule_refs_no_word if { - current_file_contents := concat("", [workspace["current_file.rego"], ` + current_file_contents := concat("", [workspace["file:///current_file.rego"], ` another_local_rule := `]) - regal_module := {"regal": { - "file": { - "name": "current_file.rego", - "uri": "current_file.rego", # would be file:// prefixed in server - "lines": split(current_file_contents, "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///current_file.rego"}, + "position": {"line": 9, "character": 20}, }, - "context": {"location": { - "row": 10, - "col": 21, - }}, - }} + "regal": {"file": {"lines": split(current_file_contents, "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as parsed_modules @@ -78,20 +74,16 @@ another_local_rule := `]) } test_rule_refs_partial_word if { - current_file_contents := concat("", [workspace["current_file.rego"], ` + current_file_contents := concat("", [workspace["file:///current_file.rego"], ` another_local_rule := imp`]) - regal_module := {"regal": { - "file": { - "name": "current_file.rego", - "uri": "current_file.rego", # would be file:// prefixed in server - "lines": split(current_file_contents, "\n"), + regal_module := { + "params": { + "textDocument": {"uri": "file:///current_file.rego"}, + "position": {"line": 9, "character": 25}, }, - "context": {"location": { - "row": 10, - "col": 26, - }}, - }} + "regal": {"file": {"lines": split(current_file_contents, "\n")}}, + } items := provider.items with input as regal_module with data.workspace.parsed as parsed_modules @@ -108,23 +100,19 @@ another_local_rule := imp`]) } test_rule_refs_not_in_rule if { - current_file_contents := concat("", [workspace["current_file.rego"], ` + current_file_contents := concat("", [workspace["file:///current_file.rego"], ` a`]) lines := split(current_file_contents, "\n") - regal_module := {"regal": { - "file": { - "name": "current_file.rego", - "uri": "current_file.rego", # would be file:// prefixed in server - "lines": lines, + regal_module := { + "params": { + "textDocument": {"uri": "file:///current_file.rego"}, + "position": {"line": count(lines) - 1, "character": 0}, }, - "context": {"location": { - "row": count(lines), - "col": 1, - }}, - }} + "regal": {"file": {"lines": lines}}, + } items := provider.items with input as regal_module with data.workspace.parsed as parsed_modules @@ -134,23 +122,19 @@ a`]) } test_rule_refs_no_recursion if { - current_file_contents := concat("", [workspace["current_file.rego"], ` + current_file_contents := concat("", [workspace["file:///current_file.rego"], ` local_rule if local`]) lines := split(current_file_contents, "\n") - regal_module := {"regal": { - "file": { - "name": "current_file.rego", - "uri": "current_file.rego", # would be file:// prefixed in server - "lines": lines, + regal_module := { + "params": { + "textDocument": {"uri": "file:///current_file.rego"}, + "position": {"line": count(lines) - 1, "character": 18}, }, - "context": {"location": { - "row": count(lines), - "col": 19, - }}, - }} + "regal": {"file": {"lines": lines}}, + } items := provider.items with input as regal_module with data.workspace.parsed as parsed_modules @@ -160,7 +144,7 @@ local_rule if local`]) } test_rule_refs_no_recursion_func if { - current_file_contents := concat("", [workspace["current_file.rego"], ` + current_file_contents := concat("", [workspace["file:///current_file.rego"], ` local_fun("") := foo { true @@ -170,17 +154,13 @@ local_func("foo") := local_f`]) lines := split(current_file_contents, "\n") - regal_module := {"regal": { - "file": { - "name": "current_file.rego", - "uri": "current_file.rego", # would be file:// prefixed in server - "lines": lines, + regal_module := { + "params": { + "textDocument": {"uri": "file:///current_file.rego"}, + "position": {"line": count(lines) - 1, "character": 25}, }, - "context": {"location": { - "row": count(lines), - "col": 26, - }}, - }} + "regal": {"file": {"lines": lines}}, + } items := provider.items with input as regal_module with data.workspace.parsed as parsed_modules diff --git a/bundle/regal/lsp/completion/providers/snippet/snippet.rego b/bundle/regal/lsp/completion/providers/snippet/snippet.rego index cf398259..c426cbb7 100644 --- a/bundle/regal/lsp/completion/providers/snippet/snippet.rego +++ b/bundle/regal/lsp/completion/providers/snippet/snippet.rego @@ -15,12 +15,11 @@ import data.regal.lsp.completion.location # description: all completion suggestions for snippets # scope: document items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] location.in_rule_body(line) - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) before := trim_suffix(line, word.text) # match empty line, or line ending with `if` or `|` plus whitespace @@ -36,7 +35,7 @@ items contains item if { "kind": kind.snippet, "detail": label, "textEdit": { - "range": location.word_range(word, position), + "range": location.word_range(word, input.params.position), "newText": snippet.body, }, "insertTextFormat": 2, # snippet @@ -44,11 +43,11 @@ items contains item if { } items contains item if { - position := location.to_position(input.regal.context.location) - line := input.regal.file.lines[position.line] + line := input.regal.file.lines[input.params.position.line] startswith("metadata", line) - word := location.word_at(line, input.regal.context.location.col) + word := location.word_at(line, input.params.position.character + 1) + range := location.word_range(word, input.params.position) some item in { { @@ -56,7 +55,7 @@ items contains item if { "kind": kind.snippet, "detail": "metadata annotation", "textEdit": { - "range": location.word_range(word, position), + "range": range, "newText": "# METADATA\n# title: ${1:title}\n# description: ${2:description}", }, "insertTextFormat": 2, # snippet @@ -66,7 +65,7 @@ items contains item if { "kind": kind.snippet, "detail": "metadata annotation", "textEdit": { - "range": location.word_range(word, position), + "range": range, "newText": "# METADATA\n# description: ${1:description}", }, "insertTextFormat": 2, # snippet 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 ed1e8e11..199173cd 100644 --- a/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego +++ b/bundle/regal/lsp/completion/providers/test_utils/test_utils.rego @@ -11,31 +11,36 @@ parsed_modules(workspace) := {file_uri: parsed_module | # METADATA # description: adds location metadata to provided module, to be used as input -input_module_with_location(module, policy, location) := object.union(module, {"regal": { - "file": { - "name": "p.rego", - "lines": split(policy, "\n"), +input_module_with_location(module, policy, location) := object.union(module, { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": location.row - 1, "character": location.col - 1}, }, - "context": {"location": location}, -}}) + "regal": {"file": { + "uri": "file:///p.rego", + "lines": split(policy, "\n"), + }}, +}) # METADATA # description: same as input_module_with_location, but accepts text content rather than a module -input_with_location(policy, location) := {"regal": { - "file": { - "name": "p.rego", - "lines": split(policy, "\n"), +input_with_location(policy, location) := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": location.row - 1, "character": location.col - 1}, }, - "context": {"location": location}, -}} + "regal": {"file": {"lines": split(policy, "\n")}}, +} # METADATA # description: same as input_with_location but with option to set rego_version too -input_with_location_and_version(policy, location, rego_version) := {"regal": { - "file": { - "name": "p.rego", +input_with_location_and_version(policy, location, rego_version) := { + "params": { + "textDocument": {"uri": "file:///p.rego"}, + "position": {"line": location.row - 1, "character": location.col - 1}, + }, + "regal": {"file": { "lines": split(policy, "\n"), "rego_version": rego_version, - }, - "context": {"location": location}, -}} + }}, +} diff --git a/cmd/fix.go b/cmd/fix.go index 7e4d0613..8a98881d 100644 --- a/cmd/fix.go +++ b/cmd/fix.go @@ -114,8 +114,7 @@ func fix(args []string, params *fixParams) (err error) { ) if len(args) == 1 { - configSearchPath = args[0] - if !strings.HasPrefix(args[0], "/") { + if configSearchPath = args[0]; !strings.HasPrefix(args[0], "/") { configSearchPath = filepath.Join(rio.Getwd(), args[0]) } } else { @@ -125,8 +124,7 @@ func fix(args []string, params *fixParams) (err error) { if configSearchPath == "" { log.Println("failed to determine relevant directory for config file search - won't search for custom config or rules") } else { - regalDir, err = config.FindRegalDirectory(configSearchPath) - if err == nil { + if regalDir, err = config.FindRegalDirectory(configSearchPath); err == nil { defer regalDir.Close() if customRulesPath := filepath.Join(regalDir.Name(), "rules"); rio.IsDir(customRulesPath) { @@ -171,8 +169,7 @@ func fix(args []string, params *fixParams) (err error) { log.Printf("found user config file: %s", userConfigFile.Name()) } - err := yaml.NewDecoder(userConfigFile).Decode(&userConfig) - if errors.Is(err, io.EOF) { + if err := yaml.NewDecoder(userConfigFile).Decode(&userConfig); errors.Is(err, io.EOF) { log.Printf("user config file %q is empty, will use the default config", userConfigFile.Name()) } else if err != nil { if regalDir != nil { @@ -239,8 +236,7 @@ func fix(args []string, params *fixParams) (err error) { continue } - absFiltered[i], err = filepath.Abs(f) - if err != nil { + if absFiltered[i], err = filepath.Abs(f); err != nil { return fmt.Errorf("failed to get absolute path for %s: %w", f, err) } } diff --git a/internal/ast/rule.go b/internal/ast/rule.go index 38e6dbb3..2017db2f 100644 --- a/internal/ast/rule.go +++ b/internal/ast/rule.go @@ -7,6 +7,8 @@ import ( "github.com/open-policy-agent/opa/v1/ast" ) +var noBody = ast.NewBody(ast.NewExpr(ast.InternedTerm(true))) + // GetRuleDetail returns a short descriptive string value for a given rule stating // if the rule is constant, multi-value, single-value etc and the type of the rule's // value if known. @@ -24,7 +26,6 @@ func GetRuleDetail(rule *ast.Rule, builtins map[string]*ast.Builtin) string { } detail := "single-value " - if rule.Head.Key != nil { detail += "map " } @@ -64,27 +65,16 @@ func GetRuleDetail(rule *ast.Rule, builtins map[string]*ast.Builtin) string { // IsConstant returns true if the rule is a "constant" rule, i.e. // one without conditions and scalar value in the head. func IsConstant(rule *ast.Rule) bool { - if rule.Head.Value == nil { - return false - } - - isScalar := false - - switch rule.Head.Value.Value.(type) { - case ast.Boolean, ast.Number, ast.String, ast.Null: - isScalar = true - } - - return isScalar && + return rule.Head.Value != nil && + ast.IsScalar(rule.Head.Value.Value) && rule.Head.Args == nil && - rule.Body.Equal(ast.NewBody(ast.NewExpr(ast.BooleanTerm(true)))) && + rule.Body.Equal(noBody) && rule.Else == nil } // simplifyType removes anything but the base type from the type name. func simplifyType(name string) string { result := name - if strings.Contains(result, ":") { result = result[strings.Index(result, ":")+1:] } diff --git a/internal/embeds/schemas/regal/lsp/common.json b/internal/embeds/schemas/regal/lsp/common.json index ec722b1b..57346684 100644 --- a/internal/embeds/schemas/regal/lsp/common.json +++ b/internal/embeds/schemas/regal/lsp/common.json @@ -53,14 +53,31 @@ "type": "string", "description": "URI of the workspace root" }, + "workspace_root_path": { + "type": "string", + "description": "File system path of the workspace root" + }, "web_server_base_uri": { "type": "string", "description": "Base URI for Regal's local web server server" + }, + "input_dot_json_path": { + "type": ["string", "null"], + "description": "Path to the input.json file, if found" + }, + "input_dot_json": { + "type": ["object", "null"], + "description": "Content of the input.json file, if found" + }, + "path_separator": { + "type": "string", + "description": "Path separator used by the operating system (e.g., '/' or '\\')" } }, "required": [ "workspace_root_uri", - "web_server_base_uri" + "web_server_base_uri", + "path_separator" ] }, "file": { diff --git a/internal/embeds/schemas/regal/lsp/completion.json b/internal/embeds/schemas/regal/lsp/completion.json index 0c4aae82..752aa345 100644 --- a/internal/embeds/schemas/regal/lsp/completion.json +++ b/internal/embeds/schemas/regal/lsp/completion.json @@ -7,11 +7,15 @@ "properties": { "textDocument": { "$ref": "#/$defs/textDocument" + }, + "position": { + "$ref": "#/$defs/position" } }, "type": "object", "required": [ - "textDocument" + "textDocument", + "position" ] }, "textDocument": { @@ -26,6 +30,24 @@ "required": [ "uri" ] + }, + "position": { + "type": "object", + "description": "Position in the text document", + "properties": { + "line": { + "type": "integer", + "description": "Line number (0-based)" + }, + "character": { + "type": "integer", + "description": "Character offset in the line (0-based)" + } + }, + "required": [ + "line", + "character" + ] } } } diff --git a/internal/explorer/stages.go b/internal/explorer/stages.go index 5470642b..5bf5dea8 100644 --- a/internal/explorer/stages.go +++ b/internal/explorer/stages.go @@ -81,32 +81,21 @@ func CompilerStages(path, rego string, useStrict, useAnno, usePrint bool) []Comp for i := range stages { stage := stages[i] - c = c.WithStageAfter(stage.name, - ast.CompilerStageDefinition{ - Name: stage.name + "Record", - MetricName: stage.metricName + "_record", - Stage: func(c0 *ast.Compiler) *ast.Error { - result = append(result, CompileResult{ - Stage: stage.name, - Result: getOne(c0.Modules), - }) - - return nil - }, - }) + c = c.WithStageAfter(stage.name, ast.CompilerStageDefinition{ + Name: stage.name + "Record", + MetricName: stage.metricName + "_record", + Stage: func(c0 *ast.Compiler) *ast.Error { + result = append(result, CompileResult{Stage: stage.name, Result: getOne(c0.Modules)}) + + return nil + }, + }) } - c.Compile(map[string]*ast.Module{ - path: mod, - }) - - if len(c.Errors) > 0 { + if c.Compile(map[string]*ast.Module{path: mod}); len(c.Errors) > 0 { // stage after the last than ran successfully stage := stages[len(result)-1] - result = append(result, CompileResult{ - Stage: stage.name + ": Failure", - Error: c.Errors.Error(), - }) + result = append(result, CompileResult{Stage: stage.name + ": Failure", Error: c.Errors.Error()}) } return result @@ -127,13 +116,13 @@ func Plan(ctx context.Context, path, rego string, usePrint bool) (string, error) } r := util.StringToByteSlice(rego) - b := &bundle.Bundle{Modules: []bundle.ModuleFile{{URL: "/url", Path: path, Raw: r, Parsed: mod}}} compiler := compile.New(). WithTarget(compile.TargetPlan). - WithBundle(b). + WithBundle(&bundle.Bundle{Modules: []bundle.ModuleFile{{URL: "/url", Path: path, Raw: r, Parsed: mod}}}). WithRegoAnnotationEntrypoints(true). WithEnablePrintStatements(usePrint) + if err := compiler.Build(ctx); err != nil { return "", err } diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 2a174929..17681422 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -202,12 +202,10 @@ func (c *Cache) GetFileAggregates(fileURIs ...string) map[string][]report.Aggreg allAggregates := make(map[string][]report.Aggregate) for sourceFile, aggregates := range c.aggregateData.Clone() { - if !includedFiles.Contains(sourceFile) && !getAll { - continue - } - - for _, aggregate := range aggregates { - allAggregates[aggregate.IndexKey()] = append(allAggregates[aggregate.IndexKey()], aggregate) + if getAll || includedFiles.Contains(sourceFile) { + for _, aggregate := range aggregates { + allAggregates[aggregate.IndexKey()] = append(allAggregates[aggregate.IndexKey()], aggregate) + } } } diff --git a/internal/lsp/completions/manager.go b/internal/lsp/completions/manager.go deleted file mode 100644 index 97ffc354..00000000 --- a/internal/lsp/completions/manager.go +++ /dev/null @@ -1,42 +0,0 @@ -package completions - -import ( - "context" - "fmt" - - "github.com/open-policy-agent/opa/v1/storage" - - "github.com/open-policy-agent/regal/internal/lsp/cache" - "github.com/open-policy-agent/regal/internal/lsp/completions/providers" - "github.com/open-policy-agent/regal/internal/lsp/rego/query" - "github.com/open-policy-agent/regal/internal/lsp/types" - "github.com/open-policy-agent/regal/internal/util" -) - -type Manager struct { - c *cache.Cache - policy *providers.Policy -} - -func NewDefaultManager(ctx context.Context, c *cache.Cache, store storage.Store, qc *query.Cache) *Manager { - return &Manager{c: c, policy: providers.NewPolicy(ctx, store, qc)} -} - -func (m *Manager) Run( - ctx context.Context, - params types.CompletionParams, - opts *providers.Options, -) ([]types.CompletionItem, error) { - completions, err := m.policy.Run(ctx, m.c, params, opts) - if err != nil { - return nil, fmt.Errorf("error running completion provider: %w", err) - } - - return util.Map(completions, removeMetadata), nil -} - -func removeMetadata(item types.CompletionItem) types.CompletionItem { - item.Regal = nil - - return item -} diff --git a/internal/lsp/completions/manager_test.go b/internal/lsp/completions/manager_test.go deleted file mode 100644 index f0e54f2c..00000000 --- a/internal/lsp/completions/manager_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package completions - -import ( - "testing" - - "github.com/open-policy-agent/opa/v1/ast" - "github.com/open-policy-agent/opa/v1/storage/inmem" - - "github.com/open-policy-agent/regal/internal/lsp/cache" - "github.com/open-policy-agent/regal/internal/lsp/completions/providers" - "github.com/open-policy-agent/regal/internal/lsp/rego/query" - "github.com/open-policy-agent/regal/internal/lsp/types" - "github.com/open-policy-agent/regal/internal/testutil" -) - -func TestManagerEarlyExitInsideComment(t *testing.T) { - t.Parallel() - - fileURI := "file:///foo/bar/file.rego" - fileContents := "package p\n\n# foo := http\n" - module := ast.MustParseModule(fileContents) - - c := cache.NewCache() - c.SetFileContents(fileURI, fileContents) - c.SetModule(fileURI, module) - - m := map[string]any{"workspace": map[string]any{"parsed": map[string]any{fileURI: module}}} - store := inmem.NewFromObjectWithOpts(m, inmem.OptRoundTripOnWrite(false)) - opts := &providers.Options{} - mgr := NewDefaultManager(t.Context(), c, store, query.NewCache()) - - completions := testutil.Must(mgr.Run(t.Context(), types.NewCompletionParams(fileURI, 2, 13, nil), opts))(t) - if len(completions) != 0 { - t.Errorf("Expected no completions, got: %v", completions) - } -} diff --git a/internal/lsp/completions/providers/options.go b/internal/lsp/completions/providers/options.go deleted file mode 100644 index 91fde816..00000000 --- a/internal/lsp/completions/providers/options.go +++ /dev/null @@ -1,16 +0,0 @@ -package providers - -import ( - "github.com/open-policy-agent/opa/v1/ast" - - "github.com/open-policy-agent/regal/internal/lsp/types" -) - -type Options struct { - // Builtins is a map of built-in functions to their definitions required in - // the context of the current completion request. - Builtins map[string]*ast.Builtin - RootURI string - Client types.Client - RegoVersion ast.RegoVersion -} diff --git a/internal/lsp/completions/providers/policy.go b/internal/lsp/completions/providers/policy.go deleted file mode 100644 index 0dae376a..00000000 --- a/internal/lsp/completions/providers/policy.go +++ /dev/null @@ -1,101 +0,0 @@ -package providers - -import ( - "context" - "errors" - "fmt" - - "github.com/open-policy-agent/opa/v1/ast" - "github.com/open-policy-agent/opa/v1/storage" - - rio "github.com/open-policy-agent/regal/internal/io" - "github.com/open-policy-agent/regal/internal/lsp/cache" - "github.com/open-policy-agent/regal/internal/lsp/rego" - "github.com/open-policy-agent/regal/internal/lsp/rego/query" - "github.com/open-policy-agent/regal/internal/lsp/types" - "github.com/open-policy-agent/regal/internal/lsp/uri" - "github.com/open-policy-agent/regal/pkg/roast/transform" -) - -// Policy provides suggestions that have been determined by Rego policy. -type Policy struct { - queryCache *query.Cache -} - -// NewPolicy creates a new Policy provider. This provider is distinctly different from the other providers -// as it acts like the entrypoint for all Rego-based providers, and not a single provider "function" like -// the Go providers do. -func NewPolicy(ctx context.Context, store storage.Store, qc *query.Cache) *Policy { - if err := qc.Store(ctx, query.Completion, store); err != nil { - panic(fmt.Errorf("failed to store cached query for completions: %w", err)) - } - - return &Policy{queryCache: qc} -} - -func (*Policy) Name() string { - return "policy" -} - -func (p *Policy) Run( - ctx context.Context, - c *cache.Cache, - params types.CompletionParams, - opts *Options, -) ([]types.CompletionItem, error) { - // TODO: Merge this into the rego package - if opts == nil { - return nil, errors.New("options must be provided") - } - - content, ok := c.GetFileContents(params.TextDocument.URI) - if !ok { - return nil, fmt.Errorf("could not get file contents for: %s", params.TextDocument.URI) - } - - // input.regal.context - location := rego.LocationFromPosition(params.Position) - regalContext := ast.NewObject( - ast.Item(ast.InternedTerm("location"), ast.ObjectTerm( - ast.Item(ast.InternedTerm("row"), ast.InternedTerm(location.Row)), - ast.Item(ast.InternedTerm("col"), ast.InternedTerm(location.Col)), - )), - ast.Item(ast.InternedTerm("client_identifier"), ast.InternedTerm(int(opts.Client.Identifier))), - ast.Item(ast.InternedTerm("workspace_root"), ast.InternedTerm(opts.RootURI)), - ) - - path := uri.ToPath(opts.Client.Identifier, 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.Client.Identifier, opts.RootURI)) - if inputDotJSONPath != "" && inputDotJSONContent != nil { - inputDotJSONValue, err := transform.ToOPAInputValue(inputDotJSONContent) - if err != nil { - return nil, fmt.Errorf("failed converting input dot JSON content to value: %w", err) - } - - regalContext.Insert(ast.InternedTerm("input_dot_json_path"), ast.InternedTerm(inputDotJSONPath)) - regalContext.Insert(ast.InternedTerm("input_dot_json"), ast.NewTerm(inputDotJSONValue)) - } - - // TODO: Schemas from annotations to be used for completions on types, etc. - - // input.regal - regalObj := transform.RegalContext(path, content, opts.RegoVersion.String()) - regalObj.Insert(ast.InternedTerm("context"), ast.NewTerm(regalContext)) - - fileRef := ast.Ref{ast.InternedTerm("file")} - fileObj, _ := regalObj.Find(fileRef) - //nolint:forcetypeassert - fileObj.(ast.Object).Insert(ast.InternedTerm("uri"), ast.InternedTerm(params.TextDocument.URI)) - - input := ast.NewObject(ast.Item(ast.InternedTerm("regal"), ast.NewTerm(regalObj))) - - var completions []types.CompletionItem - - if err := rego.CachedQueryEval(ctx, p.queryCache.Get(query.Completion), input, &completions); err != nil { - return nil, fmt.Errorf("failed querying for completion suggestions: %w", err) - } - - return completions, nil -} diff --git a/internal/lsp/completions/providers/policy_test.go b/internal/lsp/completions/providers/policy_test.go deleted file mode 100644 index 72946db9..00000000 --- a/internal/lsp/completions/providers/policy_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package providers - -import ( - "testing" - - "github.com/open-policy-agent/opa/v1/ast" - "github.com/open-policy-agent/opa/v1/storage/inmem" - - "github.com/open-policy-agent/regal/internal/lsp/cache" - "github.com/open-policy-agent/regal/internal/lsp/rego/query" - "github.com/open-policy-agent/regal/internal/lsp/test" - "github.com/open-policy-agent/regal/internal/lsp/types" - "github.com/open-policy-agent/regal/internal/parse" - "github.com/open-policy-agent/regal/pkg/roast/encoding" -) - -// testCaseFileURI is used in various tests in the providers package. -const testCaseFileURI = "file:///foo/bar/file.rego" - -//nolint:paralleltest -func TestPolicyProvider_Example1(t *testing.T) { - policy := `package p - -allow if { - user := data.users[0] - # try completion on next line - roles := u -} -` - module := parse.MustParseModule(policy) - moduleMap := make(map[string]any) - - encoding.MustJSONRoundTrip(module, &moduleMap) - - c := cache.NewCache() - c.SetFileContents(testCaseFileURI, policy) - - store := inmem.NewFromObjectWithOpts(map[string]any{ - "workspace": map[string]any{ - "parsed": map[string]any{ - testCaseFileURI: moduleMap, - }, - }, - }, inmem.OptRoundTripOnWrite(false)) - - params := types.NewCompletionParams(testCaseFileURI, 5, 11, nil) - opts := &Options{Client: types.NewGenericClient()} - - result, err := NewPolicy(t.Context(), store, query.NewCache()).Run(t.Context(), c, params, opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - test.AssertLabels(t, result, []string{"user"}) -} - -//nolint:paralleltest -func TestPolicyProvider_Example2(t *testing.T) { - file1 := ast.MustParseModule("package example\n\nfoo := true\n") - file2 := ast.MustParseModule("package example2\n\nimport data.example\n") - - store := inmem.NewFromObject(map[string]any{ - "workspace": map[string]any{ - "parsed": map[string]any{ - "file:///file1.rego": file1, - "file:///file2.rego": file2, - }, - "defined_refs": map[string][]string{ - "file:///file1.rego": {"example.foo"}, - "file:///file2.rego": {}, - }, - }, - }) - - fileEdited := "package example2\n\nimport data.example\n\nallow if {\n\tfoo :=\n}\n" - - c := cache.NewCache() - c.SetFileContents("file:///file2.rego", fileEdited) - - params := types.NewCompletionParams("file:///file2.rego", 5, 11, nil) - opts := &Options{Client: types.NewGenericClient()} - - result, err := NewPolicy(t.Context(), store, query.NewCache()).Run(t.Context(), c, params, opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - test.AssertLabels(t, result, []string{"input", "example", "example.foo"}) -} diff --git a/internal/lsp/completions/refs/defined.go b/internal/lsp/completions/refs/defined.go index 2f894b2a..e03579ab 100644 --- a/internal/lsp/completions/refs/defined.go +++ b/internal/lsp/completions/refs/defined.go @@ -19,8 +19,7 @@ import ( func DefinedInModule(module *ast.Module, builtins map[string]*ast.Builtin) map[string]types.Ref { modKey := module.Package.Path.String() - // first, create a reference for the package using the metadata - // if present + // first, create a reference for the package using the metadata, if present packagePrettyName := strings.TrimPrefix(module.Package.Path.String(), "data.") packageDescription := defaultDescription(packagePrettyName) @@ -30,35 +29,22 @@ func DefinedInModule(module *ast.Module, builtins map[string]*ast.Builtin) map[s } items := map[string]types.Ref{ - modKey: { - Label: modKey, - Kind: types.Package, - Detail: "Package", - Description: packageDescription, - }, + modKey: {Label: modKey, Kind: types.Package, Detail: "Package", Description: packageDescription}, } // Create groups of rules and functions sharing the same name ruleGroups := make(map[string][]*ast.Rule, len(module.Rules)) - for _, rule := range module.Rules { - name := rule.Head.Ref().String() - - if strings.HasPrefix(name, "test_") { - continue + if name := rule.Head.Ref().String(); !strings.HasPrefix(name, "test_") { + ruleGroups[name] = append(ruleGroups[name], rule) } - - ruleGroups[name] = append(ruleGroups[name], rule) } for g, rs := range ruleGroups { - // this should not happen, but we depend on rules being present below if len(rs) == 0 { - continue + continue // this should not happen, but we depend on rules being present below } - ruleKey := fmt.Sprintf("%s.%s", modKey, g) - isConstant := true for _, r := range rs { @@ -69,17 +55,12 @@ func DefinedInModule(module *ast.Module, builtins map[string]*ast.Builtin) map[s } } - isFunc := false - if rs[0].Head.Args != nil { - isFunc = true - } - kind := types.Rule switch { case isConstant: kind = types.ConstantRule - case isFunc: + case rs[0].Head.Args != nil: kind = types.Function } @@ -88,6 +69,8 @@ func DefinedInModule(module *ast.Module, builtins map[string]*ast.Builtin) map[s ruleDescription = documentAnnotatedRef(ruleAnnotation) } + ruleKey := fmt.Sprintf("%s.%s", modKey, g) + items[ruleKey] = types.Ref{ Kind: kind, Label: ruleKey, diff --git a/internal/lsp/completions/refs/used.go b/internal/lsp/completions/refs/used.go deleted file mode 100644 index 9c5c1efd..00000000 --- a/internal/lsp/completions/refs/used.go +++ /dev/null @@ -1,99 +0,0 @@ -package refs - -import ( - "context" - "fmt" - "sync" - - "github.com/open-policy-agent/opa/v1/ast" - "github.com/open-policy-agent/opa/v1/bundle" - "github.com/open-policy-agent/opa/v1/rego" - - rbundle "github.com/open-policy-agent/regal/bundle" - rio "github.com/open-policy-agent/regal/internal/io" - "github.com/open-policy-agent/regal/pkg/builtins" - "github.com/open-policy-agent/regal/pkg/config" - "github.com/open-policy-agent/regal/pkg/roast/rast" - "github.com/open-policy-agent/regal/pkg/roast/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 -// used. -func prepareQuery() (*rego.PreparedEvalQuery, error) { - dataBundle := bundle.Bundle{ - Manifest: bundle.Manifest{ - Roots: &[]string{"internal"}, - Metadata: map[string]any{"name": "internal"}, - }, - Data: map[string]any{ - "internal": map[string]any{ - "combined_config": map[string]any{ - "capabilities": rio.ToMap(config.CapabilitiesForThisVersion()), - }, - }, - }, - } - - regoArgs := append([]func(*rego.Rego){ - rego.ParsedBundle("regal", rbundle.LoadedBundle()), - rego.ParsedBundle("internal", &dataBundle), - rego.ParsedQuery(refNamesQuery), - }, builtins.RegalBuiltinRegoFuncs...) - - preparedQuery, err := rego.New(regoArgs...).PrepareForEval(context.Background()) - if err != nil { - return nil, err - } - - return &preparedQuery, nil -} - -// 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.ModuleToValue(module) - if err != nil { - return nil, fmt.Errorf("failed converting input to value: %w", err) - } - - pq, err := pqOnce() - if err != nil { - return nil, fmt.Errorf("failed to prepare rego query: %w", err) - } - - rs, err := pq.Eval(ctx, rego.EvalParsedInput(inputValue)) - if err != nil { - return nil, fmt.Errorf("failed to evaluate rego query: %w", err) - } - - if len(rs) == 0 || len(rs[0].Expressions) == 0 { - // no refs found - return []string{}, nil - } - - foundRefs, ok := rs[0].Expressions[0].Value.([]any) - if !ok { - return nil, fmt.Errorf("unexpected type %T", rs[0].Expressions[0].Value) - } - - refNames := make([]string, len(foundRefs)) - for i, ref := range foundRefs { - refNames[i], ok = ref.(string) - if !ok { - return nil, fmt.Errorf("unexpected type %T", ref) - } - } - - return refNames, nil -} diff --git a/internal/lsp/completions/refs/used_test.go b/internal/lsp/completions/refs/used_test.go deleted file mode 100644 index 276079fa..00000000 --- a/internal/lsp/completions/refs/used_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package refs - -import ( - "slices" - "testing" - - rparse "github.com/open-policy-agent/regal/internal/parse" -) - -func TestUsedInModule(t *testing.T) { - t.Parallel() - - mod := rparse.MustParseModule(` -package example - -import data.foo as wow -import data.bar - -allow if input.user == "admin" - -allow if data.users.admin == input.user - -deny contains wow.password if { - input.magic == true -} - -deny contains input.parrot if { - bar.parrot != "a bird" -} -`) - - items, err := UsedInModule(t.Context(), mod) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - - expectedItems := []string{ - "wow", - "bar", - "bar.parrot", - "data.users.admin", - "input.magic", - "input.parrot", - "input.user", - "wow.password", - } - - for _, item := range expectedItems { - if !slices.Contains(items, item) { - t.Errorf("Expected item %q not found in items", item) - } - } - - for _, item := range items { - if !slices.Contains(expectedItems, item) { - t.Errorf("Unexpected item %q found in items", item) - } - } -} diff --git a/internal/lsp/hover/hover.go b/internal/lsp/hover/hover.go index 22d43009..a7003115 100644 --- a/internal/lsp/hover/hover.go +++ b/internal/lsp/hover/hover.go @@ -15,6 +15,7 @@ import ( "github.com/open-policy-agent/regal/internal/lsp/rego" "github.com/open-policy-agent/regal/internal/lsp/rego/query" types2 "github.com/open-policy-agent/regal/internal/lsp/types" + "github.com/open-policy-agent/regal/internal/util" "github.com/open-policy-agent/regal/pkg/roast/util/concurrent" ) @@ -144,15 +145,14 @@ func UpdateBuiltinPositions(cache *cache.Cache, uri string, builtins map[string] builtinsOnLine := map[uint][]types2.BuiltinPosition{} - //nolint:gosec for _, call := range rego.AllBuiltinCalls(module, builtins) { - line := uint(call.Location.Row) + line := util.SafeIntToUint(call.Location.Row) builtinsOnLine[line] = append(builtinsOnLine[line], types2.BuiltinPosition{ Builtin: call.Builtin, Line: line, - Start: uint(call.Location.Col), - End: uint(call.Location.Col + len(call.Builtin.Name)), + Start: util.SafeIntToUint(call.Location.Col), + End: util.SafeIntToUint(call.Location.Col + len(call.Builtin.Name)), }) } diff --git a/internal/lsp/rego/query/query.go b/internal/lsp/rego/query/query.go index 6bf304f5..e83de153 100644 --- a/internal/lsp/rego/query/query.go +++ b/internal/lsp/rego/query/query.go @@ -25,7 +25,6 @@ import ( const ( Keywords = "data.regal.ast.keywords" RuleHeadLocations = "data.regal.ast.rule_head_locations" - Completion = "data.regal.lsp.completion.items" MainEval = "data.regal.lsp.main.eval" ) @@ -192,10 +191,6 @@ func parseQuery(query string) ast.Body { return ast.MustParseBody(query) } -func AllQueries() []string { - return []string{Keywords, RuleHeadLocations, Completion} -} - func isBundleDevelopmentMode() bool { return os.Getenv("REGAL_BUNDLE_PATH") != "" } diff --git a/internal/lsp/rego/rego.go b/internal/lsp/rego/rego.go index d6672e5f..d080c9de 100644 --- a/internal/lsp/rego/rego.go +++ b/internal/lsp/rego/rego.go @@ -60,12 +60,20 @@ type ( RegoVersion string `json:"rego_version"` SuccessfulParseCount uint `json:"successful_parse_count"` ParseErrors []types.Diagnostic `json:"parse_errors"` + + // This exists only for compatibility with some rules in the AST package, + // where we can't reference e.g. input.params.textDocument.uri without violating + // the input schema. We should find a better solution for this long-term. + URI string `json:"uri"` } Environment struct { - PathSeparator string `json:"path_separator"` - WorkspaceRootURI string `json:"workspace_root_uri"` - WebServerBaseURI string `json:"web_server_base_uri"` + PathSeparator string `json:"path_separator"` + WorkspaceRootURI string `json:"workspace_root_uri"` + WorkspaceRootPath string `json:"workspace_root_path"` + WebServerBaseURI string `json:"web_server_base_uri"` + InputDotJSON ast.Value `json:"input_dot_json,omitempty"` + InputDotJSONPath *string `json:"input_dot_json_path,omitempty"` } RegalContext struct { @@ -77,7 +85,8 @@ type ( } Requirements struct { - File FileRequirements `json:"file"` + File FileRequirements `json:"file"` + InputDotJSON bool `json:"input_dot_json"` } FileRequirements struct { diff --git a/internal/lsp/rego/router.go b/internal/lsp/rego/router.go index 15803e02..7f45ff41 100644 --- a/internal/lsp/rego/router.go +++ b/internal/lsp/rego/router.go @@ -3,16 +3,20 @@ package rego import ( "context" "errors" + "fmt" "strings" "github.com/sourcegraph/jsonrpc2" "github.com/open-policy-agent/opa/v1/storage" + "github.com/open-policy-agent/regal/internal/io" "github.com/open-policy-agent/regal/internal/lsp/handler" "github.com/open-policy-agent/regal/internal/lsp/rego/query" "github.com/open-policy-agent/regal/internal/lsp/types" + ruri "github.com/open-policy-agent/regal/internal/lsp/uri" "github.com/open-policy-agent/regal/internal/util" + "github.com/open-policy-agent/regal/pkg/roast/transform" ) var ( @@ -21,7 +25,6 @@ var ( "textDocument/documentLink": make([]types.DocumentLink, 0), "textDocument/documentHighlight": make([]types.DocumentHighlight, 0), "textDocument/documentSymbol": make([]types.DocumentSymbol, 0), - "textDocument/completion": make([]types.CompletionItem, 0), "textDocument/codeLens": make([]types.CodeLens, 0), "textDocument/signatureHelp": nil, } @@ -73,6 +76,13 @@ func NewRegoRouter(ctx context.Context, store storage.Store, qc *query.Cache, pr }, }, }, + "textDocument/completion": { + handler: textDocument[types.CompletionParams, *types.CompletionList], + requires: &Requirements{ + File: FileRequirements{Lines: true}, + InputDotJSON: true, + }, + }, "textDocument/documentLink": { handler: textDocument[types.DocumentLinkParams, []types.DocumentLink], }, @@ -114,6 +124,7 @@ func requirementsHandler(route Route) regoHandler { // Set up a basic RegalContext, which while not used by all routes, is provided for all. rctx := prvs.ContextProvider(uri, route.requires) rctx.Query = query + rctx.File.URI = uri if route.requires == nil { return route.handler(ctx, rctx, req) @@ -155,6 +166,23 @@ func requirementsHandler(route Route) regoHandler { } } + if route.requires.InputDotJSON { + path := ruri.ToPath(rctx.Client.Identifier, uri) + root := ruri.ToPath(rctx.Client.Identifier, rctx.Environment.WorkspaceRootURI) + + // TODO: Avoid the intermediate map[string]any step and unmarshal directly into ast.Value. + inputDotJSONPath, inputDotJSONContent := io.FindInput(path, root) + if inputDotJSONPath != "" && inputDotJSONContent != nil { + inputDotJSONValue, err := transform.ToOPAInputValue(inputDotJSONContent) + if err != nil { + return nil, fmt.Errorf("failed to convert input.json to value: %w", err) + } + + rctx.Environment.InputDotJSONPath = &inputDotJSONPath + rctx.Environment.InputDotJSON = inputDotJSONValue + } + } + return route.handler(ctx, rctx, req) } } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 7694ad11..698c74b1 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -32,8 +32,6 @@ import ( "github.com/open-policy-agent/regal/internal/lsp/bundles" "github.com/open-policy-agent/regal/internal/lsp/cache" "github.com/open-policy-agent/regal/internal/lsp/clients" - "github.com/open-policy-agent/regal/internal/lsp/completions" - "github.com/open-policy-agent/regal/internal/lsp/completions/providers" lsconfig "github.com/open-policy-agent/regal/internal/lsp/config" "github.com/open-policy-agent/regal/internal/lsp/documentsymbol" "github.com/open-policy-agent/regal/internal/lsp/examples" @@ -73,7 +71,6 @@ const ( var ( noDocumentSymbols = make([]types.DocumentSymbol, 0) - noCompletionItems = make([]types.CompletionItem, 0) noFoldingRanges = make([]types.FoldingRange, 0) noDiagnostics = make([]types.Diagnostic, 0) @@ -116,8 +113,7 @@ type LanguageServer struct { bundleCache *bundles.Cache queryCache *query.Cache - completionsManager *completions.Manager - regoRouter *rego.RegoRouter + regoRouter *rego.RegoRouter commandRequest chan types.ExecuteCommandParams lintWorkspaceJobs chan lintWorkspaceJob @@ -152,6 +148,11 @@ type lintWorkspaceJob struct { AggregateReportOnly bool } +type fileLoadFailure struct { + URI string + Error error +} + func NewLanguageServer(ctx context.Context, opts *LanguageServerOptions) *LanguageServer { ls := NewLanguageServerMinimal(ctx, opts, nil) ls.configWatcher = lsconfig.NewWatcher(&lsconfig.WatcherOpts{Logger: ls.log}) @@ -178,7 +179,6 @@ func NewLanguageServerMinimal(ctx context.Context, opts *LanguageServerOptions, commandRequest: make(chan types.ExecuteCommandParams, 10), templateFileJobs: make(chan lintFileJob, 10), templatingFiles: concurrent.MapOf(make(map[string]bool)), - completionsManager: completions.NewDefaultManager(ctx, c, store, qc), webServer: web.NewServer(c, opts.Logger), loadedBuiltins: concurrent.MapOf(make(map[string]map[string]*ast.Builtin)), workspaceDiagnosticsPoll: opts.WorkspaceDiagnosticsPoll, @@ -238,8 +238,6 @@ func (l *LanguageServer) Handle(ctx context.Context, _ *jsonrpc2.Conn, req *json return handler.WithParams(req, l.handleTextDocumentHover) case "textDocument/inlayHint": return handler.WithParams(req, l.handleTextDocumentInlayHint) - case "textDocument/completion": - return handler.WithContextAndParams(ctx, req, l.handleTextDocumentCompletion) case "workspace/didChangeWatchedFiles": return handler.WithParams(req, l.handleWorkspaceDidChangeWatchedFiles) case "workspace/diagnostic": @@ -284,6 +282,7 @@ func (l *LanguageServer) Handle(ctx context.Context, _ *jsonrpc2.Conn, req *json // Handles: // - textDocument/codeAction // - textDocument/codeLens + // - textDocument/completion // - textDocument/documentLink // - textDocument/documentHighlight // - textDocument/signatureHelp @@ -1443,31 +1442,6 @@ func (l *LanguageServer) handleTextDocumentInlayHint(params types.InlayHintParam return inlayhint.FromModule(module, bis), nil } -func (l *LanguageServer) handleTextDocumentCompletion(ctx context.Context, params types.CompletionParams) (any, error) { - // when config ignores a file, then we return an empty completion list as a no-op. - if l.ignoreURI(params.TextDocument.URI) { - return types.CompletionList{IsIncomplete: false, Items: []types.CompletionItem{}}, nil - } - - // items is allocated here so that the return value is always a non-nil CompletionList - items, err := l.completionsManager.Run(ctx, params, &providers.Options{ - Client: l.client, - RootURI: l.workspaceRootURI, - Builtins: l.builtinsForCurrentCapabilities(), - RegoVersion: l.regoVersionForURI(params.TextDocument.URI), - }) - if err != nil { - return nil, fmt.Errorf("failed to find completions: %w", err) - } - - if items == nil { - // make sure the items is always [] instead of null as is required by the spec - items = noCompletionItems - } - - return types.CompletionList{IsIncomplete: items != nil, Items: items}, nil -} - // Note: currently ignoring params.Query, as the client seems to do a good // job of filtering anyway, and that would merely be an optimization here. // But perhaps a good one to do at some point, and I'm not sure all clients @@ -1724,7 +1698,6 @@ func (l *LanguageServer) handleTextDocumentFormatting( // opa-fmt is the default formatter if not set in the client options formatter := "opa-fmt" - if l.client.InitOptions != nil && l.client.InitOptions.Formatter != nil { formatter = *l.client.InitOptions.Formatter } @@ -2103,11 +2076,6 @@ func (l *LanguageServer) updateRootURI(ctx context.Context, rootURI string) erro return nil } -type fileLoadFailure struct { - URI string - Error error -} - func (l *LanguageServer) loadWorkspaceContents(ctx context.Context, newOnly bool) ( []string, []fileLoadFailure, error, ) { @@ -2319,9 +2287,10 @@ func (l *LanguageServer) regalContext(uri string, req *rego.Requirements) rego.R Abs: l.toPath(uri), }, Environment: rego.Environment{ - PathSeparator: string(os.PathSeparator), - WebServerBaseURI: l.webServer.GetBaseURL(), - WorkspaceRootURI: l.workspaceRootURI, + PathSeparator: string(os.PathSeparator), + WebServerBaseURI: l.webServer.GetBaseURL(), + WorkspaceRootURI: l.workspaceRootURI, + WorkspaceRootPath: l.workspacePath(), }, } diff --git a/internal/lsp/types/completion/completion.go b/internal/lsp/types/completion/completion.go deleted file mode 100644 index 2f193adf..00000000 --- a/internal/lsp/types/completion/completion.go +++ /dev/null @@ -1,39 +0,0 @@ -package completion - -type ItemKind uint - -const ( - Text ItemKind = iota + 1 - Method - Function - Constructor - Field - Variable - Class - Interface - Module - Property - Unit - Value - Enum - Keyword - Snippet - Color - File - Reference - Folder - EnumMember - Constant - Struct - Event - Operator - TypeParameter -) - -type TriggerKind uint - -const ( - Invoked TriggerKind = iota + 1 - TriggerCharacter - TriggerForIncompleteCompletions -) diff --git a/internal/lsp/types/types.go b/internal/lsp/types/types.go index e97b295f..50ce7072 100644 --- a/internal/lsp/types/types.go +++ b/internal/lsp/types/types.go @@ -3,7 +3,6 @@ package types import ( "encoding/json" - "github.com/open-policy-agent/regal/internal/lsp/types/completion" "github.com/open-policy-agent/regal/internal/lsp/types/symbols" ) @@ -114,8 +113,8 @@ type ( } CompletionContext struct { - TriggerCharacter string `json:"triggerCharacter"` - TriggerKind completion.TriggerKind `json:"triggerKind"` + TriggerCharacter string `json:"triggerCharacter"` + TriggerKind uint `json:"triggerKind"` } CompletionList struct { @@ -129,19 +128,10 @@ type ( TextEdit *TextEdit `json:"textEdit,omitempty"` InserTextFormat *uint `json:"insertTextFormat,omitempty"` SortText *string `json:"sortText,omitempty"` - // Regal is used to store regal-specific metadata about the completion item. - // This is not part of the LSP spec, but used in the manager to post process - // items before returning them to the client. - Regal *CompletionItemRegalMetadata `json:"_regal,omitempty"` - Label string `json:"label"` - Detail string `json:"detail"` - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind - Kind completion.ItemKind `json:"kind"` - Preselect bool `json:"preselect"` - } - - CompletionItemRegalMetadata struct { - Provider string `json:"provider"` + Label string `json:"label"` + Detail string `json:"detail"` + Kind uint `json:"kind"` + Preselect bool `json:"preselect"` } CompletionItemLabelDetails struct { diff --git a/internal/lsp/uri/uri.go b/internal/lsp/uri/uri.go index c5e8578b..e61426b9 100644 --- a/internal/lsp/uri/uri.go +++ b/internal/lsp/uri/uri.go @@ -18,37 +18,24 @@ var drivePattern = regexp.MustCompile(`^\/?([A-Za-z]):`) // Since clients expect URIs to be in a specific format, this function // will convert the path to the appropriate format for the client. func FromPath(client clients.Identifier, path string) string { - path = strings.TrimPrefix(path, "file://") - path = strings.TrimPrefix(path, uriSeparator) + path = strings.TrimPrefix(strings.TrimPrefix(path, "file://"), uriSeparator) var driveLetter string if matches := drivePattern.FindStringSubmatch(path); len(matches) > 0 { driveLetter = matches[1] + ":" - } - - if driveLetter != "" { path = strings.TrimPrefix(path, driveLetter) } parts := strings.Split(filepath.ToSlash(path), uriSeparator) for i, part := range parts { - parts[i] = url.QueryEscape(part) - parts[i] = strings.ReplaceAll(parts[i], "+", "%20") - } - - if client == clients.IdentifierVSCode { - if driveLetter != "" { - return "file:///" + url.QueryEscape(driveLetter) + strings.Join(parts, uriSeparator) - } - - return "file:///" + strings.Join(parts, uriSeparator) + parts[i] = strings.ReplaceAll(url.QueryEscape(part), "+", "%20") } - if driveLetter != "" { - return "file:///" + driveLetter + strings.Join(parts, uriSeparator) + if client == clients.IdentifierVSCode && driveLetter != "" { + driveLetter = url.QueryEscape(driveLetter) } - return "file:///" + strings.Join(parts, uriSeparator) + return "file:///" + driveLetter + strings.Join(parts, uriSeparator) } // ToPath converts a URI to a file path from a format for a given client. @@ -59,15 +46,13 @@ func ToPath(client clients.Identifier, uri string) string { path, hadPrefix := strings.CutPrefix(uri, "file://") if hadPrefix { // if it looks like a URI, then try and decode the path - decodedPath, err := url.QueryUnescape(path) - if err == nil { + if decodedPath, err := url.QueryUnescape(path); err == nil { path = decodedPath } } // handling case for windows when the drive letter is set - if client == clients.IdentifierVSCode && - drivePattern.MatchString(path) { + if client == clients.IdentifierVSCode && drivePattern.MatchString(path) { path = strings.TrimPrefix(path, uriSeparator) } diff --git a/internal/util/util.go b/internal/util/util.go index 29c39352..3e0f6bac 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -42,13 +42,6 @@ func SearchMap(object map[string]any, path ...string) (any, error) { return current, nil } -// Must0 an error (as commonly returned by Go functions) and panics if the error is not nil. -func Must0(err error) { - if err != nil { - panic(err) - } -} - // Must takes a value and an error (as commonly returned by Go functions) and panics if the error is not nil. func Must[T any](v T, err error) T { if err != nil { diff --git a/pkg/linter/linter.go b/pkg/linter/linter.go index 0ce65a11..663c3998 100644 --- a/pkg/linter/linter.go +++ b/pkg/linter/linter.go @@ -64,7 +64,6 @@ type Linter struct { enableAll bool profiling bool instrumentation bool - hasCustomRules bool isPrepared bool preparedQuery *rego.PreparedEvalQuery @@ -124,64 +123,43 @@ func (l Linter) WithInputModules(input *rules.Input) Linter { // WithAddedBundle adds a bundle of rules and data to include in evaluation. func (l Linter) WithAddedBundle(b *bundle.Bundle) Linter { l.ruleBundles = append(l.ruleBundles, b) - l.isPrepared = false - return l + return l.notPrepared() } // WithCustomRules adds custom rules for evaluation, from the Rego (and data) files provided at paths. func (l Linter) WithCustomRules(paths []string) Linter { for _, path := range paths { - stat, err := os.Stat(path) - if err != nil { - l.customRuleError = fmt.Errorf("failed to stat custom rule file %s: %w", path, err) - - return l - } - - if stat.IsDir() { + if rio.IsDir(path) { l = l.WithCustomRulesFromFS(os.DirFS(path), ".") } else { contents, err := os.ReadFile(path) if err != nil { l.customRuleError = fmt.Errorf("failed to read custom rule file %s: %w", path, err) - return l + return l.notPrepared() } - l = l.WithCustomRulesFromFS(fstest.MapFS{ - filepath.Base(path): &fstest.MapFile{Data: contents}, - }, ".") + l = l.WithCustomRulesFromFS(fstest.MapFS{filepath.Base(path): &fstest.MapFile{Data: contents}}, ".") } } - l.isPrepared = false - - return l + return l.notPrepared() } // WithCustomRulesFromFS adds custom rules for evaluation from a filesystem implementing the fs.FS interface. // A root path within the filesystem must also be specified. Note, _test.rego files will be ignored. func (l Linter) WithCustomRulesFromFS(f fs.FS, rootPath string) Linter { - if f == nil { - return l - } - - l.hasCustomRules = true - l.isPrepared = false - - modules, err := rio.ModulesFromCustomRuleFS(f, rootPath) - if err != nil { - l.customRuleError = err - - return l - } - - for _, m := range modules { - l.customRuleModules = append(l.customRuleModules, m) + if f != nil { + modules, err := rio.ModulesFromCustomRuleFS(f, rootPath) + if err != nil { + l.customRuleError = err + } else { + l.customRuleModules = append(l.customRuleModules, outil.Values(modules)...) + } } - return l + return l.notPrepared() } // WithDebugMode enables debug mode. @@ -194,65 +172,57 @@ func (l Linter) WithDebugMode(debugMode bool) Linter { // WithUserConfig provides config overrides set by the user. func (l Linter) WithUserConfig(cfg config.Config) Linter { l.userConfig = &cfg - l.isPrepared = false - return l + return l.notPrepared() } // WithDisabledRules disables provided rules. This overrides configuration provided in file. func (l Linter) WithDisabledRules(disable ...string) Linter { l.disable = disable - l.isPrepared = false - return l + return l.notPrepared() } // WithDisableAll disables all rules when set to true. This overrides configuration provided in file. func (l Linter) WithDisableAll(disableAll bool) Linter { l.disableAll = disableAll - l.isPrepared = false - return l + return l.notPrepared() } // WithDisabledCategories disables provided categories of rules. This overrides configuration provided in file. func (l Linter) WithDisabledCategories(disableCategory ...string) Linter { l.disableCategory = disableCategory - l.isPrepared = false - return l + return l.notPrepared() } // WithEnabledRules enables provided rules. This overrides configuration provided in file. func (l Linter) WithEnabledRules(enable ...string) Linter { l.enable = enable - l.isPrepared = false - return l + return l.notPrepared() } // WithEnableAll enables all rules when set to true. This overrides configuration provided in file. func (l Linter) WithEnableAll(enableAll bool) Linter { l.enableAll = enableAll - l.isPrepared = false - return l + return l.notPrepared() } // WithEnabledCategories enables provided categories of rules. This overrides configuration provided in file. func (l Linter) WithEnabledCategories(enableCategory ...string) Linter { l.enableCategory = enableCategory - l.isPrepared = false - return l + return l.notPrepared() } // WithIgnore excludes files matching patterns. This overrides configuration provided in file. func (l Linter) WithIgnore(ignore []string) Linter { l.ignoreFiles = ignore - l.isPrepared = false - return l + return l.notPrepared() } // WithMetrics enables metrics collection. @@ -287,9 +257,8 @@ func (l Linter) WithInstrumentation(enabled bool) Linter { // referenced in the linter configuration with absolute file paths or URIs. func (l Linter) WithPathPrefix(pathPrefix string) Linter { l.pathPrefix = pathPrefix - l.isPrepared = false - return l + return l.notPrepared() } // WithExportAggregates enables the setting of intermediate aggregate data @@ -345,14 +314,11 @@ func (l Linter) Prepare(ctx context.Context) (Linter, error) { l.combinedCfg = conf l.dataBundle = l.createDataBundle(*conf) - l.preparedQuery, err = l.prepareQuery(ctx) - if err != nil { + if l.preparedQuery, err = l.prepareQuery(ctx); err != nil { return l, fmt.Errorf("failed to prepare query: %w", err) } - l.isPrepared = true - - return l, nil + return l.notPrepared(), nil } // MustPrepare prepares the linter and panics on errors. Mostly used for tests. @@ -374,9 +340,7 @@ func (l Linter) Lint(ctx context.Context) (report.Report, error) { if !l.isPrepared { var err error - - l, err = l.Prepare(ctx) - if err != nil { + if l, err = l.Prepare(ctx); err != nil { return report.Report{}, fmt.Errorf("failed to prepare linter: %w", err) } } @@ -601,6 +565,12 @@ func (l Linter) prepareQuery(ctx context.Context) (*rego.PreparedEvalQuery, erro return &pq, nil } +func (l Linter) notPrepared() Linter { + l.isPrepared = false + + return l +} + func (l Linter) validate(conf *config.Config) error { if len(l.inputPaths) == 0 && l.inputModules == nil && len(l.overriddenAggregates) == 0 { return errors.New("nothing provided to lint") @@ -724,6 +694,10 @@ func (l Linter) createDataBundle(conf config.Config) *bundle.Bundle { } func (l Linter) prepareRegoArgs(query ast.Body) ([]func(*rego.Rego), error) { + if l.customRuleError != nil { + return nil, fmt.Errorf("failed to load custom rules: %w", l.customRuleError) + } + regoArgs := append([]func(*rego.Rego){ rego.StoreReadAST(true), rego.Metrics(l.metrics), @@ -743,14 +717,8 @@ func (l Linter) prepareRegoArgs(query ast.Body) ([]func(*rego.Rego), error) { regoArgs = append(regoArgs, rego.ParsedBundle("internal", l.dataBundle)) } - if l.hasCustomRules { - if l.customRuleError != nil { - return nil, fmt.Errorf("failed to load custom rules: %w", l.customRuleError) - } - - for _, m := range l.customRuleModules { - regoArgs = append(regoArgs, rego.ParsedModule(m)) - } + for _, m := range l.customRuleModules { + regoArgs = append(regoArgs, rego.ParsedModule(m)) } if l.ruleBundles != nil { @@ -780,9 +748,10 @@ func (l Linter) lint(ctx context.Context, input rules.Input) (report.Report, err operationCollect := len(input.FileNames) > 1 || l.useCollectQuery - var wg sync.WaitGroup - - var mu sync.Mutex + var ( + wg sync.WaitGroup + mu sync.Mutex + ) // the error channel is buffered to prevent blocking // caused by the context cancellation happening before @@ -803,10 +772,7 @@ func (l Linter) lint(ctx context.Context, input rules.Input) (report.Report, err return } - evalArgs := []rego.EvalOption{ - rego.EvalParsedInput(inputValue), - rego.EvalInstrument(l.instrumentation), - } + evalArgs := []rego.EvalOption{rego.EvalParsedInput(inputValue), rego.EvalInstrument(l.instrumentation)} if l.baseCache != nil { evalArgs = append(evalArgs, rego.EvalBaseCache(l.baseCache)) diff --git a/pkg/reporter/reporter.go b/pkg/reporter/reporter.go index 80874e32..1195e16a 100644 --- a/pkg/reporter/reporter.go +++ b/pkg/reporter/reporter.go @@ -139,12 +139,7 @@ func (tr PrettyReporter) Publish(_ context.Context, r report.Report) error { } if r.Summary.RulesSkipped > 0 { - footer += fmt.Sprintf( - " %d %s skipped:\n", - r.Summary.RulesSkipped, - pluralize("rule", r.Summary.RulesSkipped), - ) - + footer += fmt.Sprintf(" %d %s skipped:\n", r.Summary.RulesSkipped, pluralize("rule", r.Summary.RulesSkipped)) for _, notice := range r.Notices { if notice.Severity != "none" { footer += fmt.Sprintf("- %s: %s\n", notice.Title, notice.Description) @@ -305,9 +300,7 @@ func (tr CompactReporter) Publish(_ context.Context, r report.Report) error { // Publish prints a JSON report to the configured output. func (tr JSONReporter) Publish(_ context.Context, r report.Report) error { - if r.Violations == nil { - r.Violations = []report.Violation{} - } + r.Violations = util.NullToEmpty(r.Violations) bs, err := encoding.JSON().MarshalIndent(r, "", " ") if err != nil { @@ -432,10 +425,7 @@ func getLocation(violation report.Violation) *sarif.Location { physicalLocation := sarif.NewPhysicalLocation(). WithArtifactLocation(sarif.NewSimpleArtifactLocation(violation.Location.File)) - region := sarif.NewRegion(). - WithStartLine(violation.Location.Row). - WithStartColumn(violation.Location.Column) - + region := sarif.NewRegion().WithStartLine(violation.Location.Row).WithStartColumn(violation.Location.Column) if violation.Location.End != nil { region = region.WithEndLine(violation.Location.End.Row).WithEndColumn(violation.Location.End.Column) } @@ -468,9 +458,7 @@ func getUniqueViolationURLs(violations []report.Violation) map[string]string { // Publish prints a JUnit XML report to the configured output. func (tr JUnitReporter) Publish(_ context.Context, r report.Report) error { - testSuites := junit.Testsuites{ - Name: "regal", - } + testSuites := junit.Testsuites{Name: "regal"} // group by file & sort by file files := make([]string, 0) @@ -484,9 +472,7 @@ func (tr JUnitReporter) Publish(_ context.Context, r report.Report) error { slices.Sort(files) for _, file := range files { - testsuite := junit.Testsuite{ - Name: file, - } + testsuite := junit.Testsuite{Name: file} for _, violation := range violationsPerFile[file] { //nolint:gocritic text := ""