Skip to content

Commit

Permalink
[lsp] Implement rulerefs in rego (#849)
Browse files Browse the repository at this point in the history
* [lsp] Implement rulerefs in rego

Signed-off-by: Charlie Egan <[email protected]>

* [lsp] Add client id to rego input context

Signed-off-by: Charlie Egan <[email protected]>

* [lsp] update local provider

This update has the locals provider also use the workspace contents

Signed-off-by: Charlie Egan <[email protected]>

---------

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 committed Jun 19, 2024
1 parent 1191c41 commit 7bf1c93
Show file tree
Hide file tree
Showing 14 changed files with 540 additions and 372 deletions.
7 changes: 6 additions & 1 deletion bundle/regal/lsp/completion/providers/locals/locals.rego
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import rego.v1
import data.regal.lsp.completion.kind
import data.regal.lsp.completion.location

parsed_current_file := data.workspace.parsed[input.regal.file.uri]

items contains item if {
position := location.to_position(input.regal.context.location)

Expand All @@ -16,7 +18,10 @@ items contains item if {

not excluded(line, position)

some local in location.find_locals(input.rules, input.regal.context.location)
some local in location.find_locals(
parsed_current_file.rules,
input.regal.context.location,
)

startswith(local, word.text)

Expand Down
61 changes: 36 additions & 25 deletions bundle/regal/lsp/completion/providers/locals/locals_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import rego.v1
import data.regal.lsp.completion.providers.locals

test_no_locals_in_completion_items if {
policy := `package policy
workspace := {"file:///p.rego": `package policy
import rego.v1
Expand All @@ -14,26 +14,25 @@ foo := 1
bar if {
foo == 1
}
`
`}

module := regal.parse_module("p.rego", policy)
regal_module := object.union(module, {"regal": {
regal_module := {"regal": {
"file": {
"name": "p.rego",
"lines": split(policy, "\n"),
"lines": split(workspace["file:///p.rego"], "\n"),
},
"context": {"location": {
"row": 8,
"col": 9,
}},
}})
items := locals.items with input as regal_module
}}
items := locals.items with input as regal_module with data.workspace.parsed as parsed_modules(workspace)

count(items) == 0
}

test_locals_in_completion_items if {
policy := `package policy
workspace := {"file:///p.rego": `package policy
import rego.v1
Expand All @@ -43,27 +42,29 @@ function(bar) if {
baz := 1
qux := b
}
`
`}

module := object.union(regal.parse_module("p.rego", policy), {"regal": {
regal_module := {"regal": {
"file": {
"name": "p.rego",
"lines": split(policy, "\n"),
"uri": "file:///p.rego",
"lines": split(workspace["file:///p.rego"], "\n"),
},
"context": {"location": {
"row": 9,
"col": 10,
}},
}})
items := locals.items with input as module
}}

items := locals.items with input as regal_module with data.workspace.parsed as parsed_modules(workspace)

count(items) == 2
expect_item(items, "bar", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}})
expect_item(items, "baz", {"end": {"character": 9, "line": 8}, "start": {"character": 8, "line": 8}})
}

test_locals_in_completion_items_function_call if {
policy := `package policy
workspace := {"file:///p.rego": `package policy
import rego.v1
Expand All @@ -73,49 +74,59 @@ function(bar) if {
baz := 1
qux := other_function(b)
}
`
module := object.union(regal.parse_module("p.rego", policy), {"regal": {
`}

regal_module := {"regal": {
"file": {
"name": "p.rego",
"lines": split(policy, "\n"),
"uri": "file:///p.rego",
"lines": split(workspace["file:///p.rego"], "\n"),
},
"context": {"location": {
"row": 9,
"col": 25,
}},
}})
items := locals.items with input as module
}}

items := locals.items with input as regal_module with data.workspace.parsed as parsed_modules(workspace)

count(items) == 2
expect_item(items, "bar", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}})
expect_item(items, "baz", {"end": {"character": 24, "line": 8}, "start": {"character": 23, "line": 8}})
}

test_locals_in_completion_items_rule_head_assignment if {
policy := `package policy
workspace := {"file:///p.rego": `package policy
import rego.v1
function(bar) := f if {
foo := 1
}
`
module := object.union(regal.parse_module("p.rego", policy), {"regal": {
`}

regal_module := {"regal": {
"file": {
"name": "p.rego",
"lines": split(policy, "\n"),
"uri": "file:///p.rego",
"lines": split(workspace["file:///p.rego"], "\n"),
},
"context": {"location": {
"row": 5,
"col": 19,
}},
}})
items := locals.items with input as module
}}
items := locals.items with input as regal_module with data.workspace.parsed as parsed_modules(workspace)

count(items) == 1
expect_item(items, "foo", {"end": {"character": 18, "line": 4}, "start": {"character": 17, "line": 4}})
}

parsed_modules(workspace) := {file_uri: parsed_module |
some file_uri, contents in workspace
parsed_module := regal.parse_module(file_uri, contents)
}

expect_item(items, label, range) if {
expected := {"detail": "local variable", "kind": 6}

Expand Down
140 changes: 140 additions & 0 deletions bundle/regal/lsp/completion/providers/rulerefs/rulerefs.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package regal.lsp.completion.providers.rulerefs

import rego.v1

import data.regal.ast
import data.regal.lsp.completion.kind
import data.regal.lsp.completion.location

ref_is_internal(ref) if contains(ref, "._")

workspace_rule_refs contains ref if {
some file, parsed in data.workspace.parsed

package_name := concat(".", [path.value |
some i, path in parsed["package"].path
])

some rule in parsed.rules

rule_ref := ast.ref_to_string(rule.head.ref)

ref := concat(".", [package_name, rule_ref])
}

parsed_current_file := data.workspace.parsed[input.regal.file.uri]

current_file_package := concat(".", [segment.value |
some segment in parsed_current_file["package"].path
])

current_file_imports contains ref if {
some imp in parsed_current_file.imports

ref := ast.ref_to_string(imp.path.value)
}

current_package_refs contains ref if {
some ref in workspace_rule_refs

startswith(ref, current_file_package)
}

imported_package_refs contains ref if {
some ref in workspace_rule_refs
some pkg_ref in current_file_imports

not ref_is_internal(ref)

startswith(ref, pkg_ref)
}

other_package_refs contains ref if {
some ref in workspace_rule_refs

not ref in imported_package_refs
not ref in current_package_refs
}

# from the current package
rule_ref_suggestions contains pkg_ref if {
some ref in current_package_refs

pkg_ref := trim_prefix(ref, sprintf("%s.", [current_file_package]))
}

# from imported packages
rule_ref_suggestions contains pkg_ref if {
some ref in imported_package_refs
some imported_package in current_file_imports

startswith(ref, imported_package)

parts := split(imported_package, ".")
prefix := concat(".", array.slice(parts, 0, count(parts) - 1))
pkg_ref := trim_prefix(ref, sprintf("%s.", [prefix]))
}

# from any other package
rule_ref_suggestions contains ref if {
some ref in other_package_refs

not ref_is_internal(ref)
}

# also suggest the unimported packages themselves
# e.g. data.foo.rule will also generate data.foo as a suggestion
rule_ref_suggestions contains pkg if {
some ref in other_package_refs

not ref_is_internal(ref)

parts := split(ref, ".")
pkg := concat(".", array.slice(parts, 0, count(parts) - 1))
}

grouped_refs[size] contains ref if {
some ref in rule_ref_suggestions
size := count(indexof_n(ref, "."))
}

default defermine_ref_prefix(_) := ""

defermine_ref_prefix(word) := word if {
not word in {":="}
}

items := [item |
position := location.to_position(input.regal.context.location)

line := input.regal.file.lines[position.line]
line != ""
location.in_rule_body(line)

last_word := regal.last(regex.split(`\s+`, trim_space(line)))

prefix := defermine_ref_prefix(last_word)

sorted_counts := sort(object.keys(grouped_refs))

some group_size in sorted_counts
some ref in sort(grouped_refs[group_size])

startswith(ref, prefix)

item := {
"label": ref,
"kind": kind.variable,
"detail": "rule ref",
"textEdit": {
"range": {
"start": {
"line": position.line,
"character": position.character - count(last_word),
},
"end": position,
},
"newText": ref,
},
}
]
Loading

0 comments on commit 7bf1c93

Please sign in to comment.