fix: Imports without a name binding (#543)

Handle imports that don't use the `as` clause to create a binding.
These imports are bound to the last identifier in the imported path.

Fix both interpreter and compiler.
Add tests.

fixes #541

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-01-15 03:57:52 +05:30
committed by GitHub
parent 740db8a0f5
commit 9426b2ec02
4 changed files with 234 additions and 5 deletions

View File

@@ -335,7 +335,18 @@ impl<'a> Compiler<'a> {
}
}
// No rule found - undefined variable
// No rule found; fall back to module-level imports.
let import_key = format!("{}.{}", &self.current_package, root);
if let Some(import_expr) = self.policy.inner.imports.get(&import_key) {
let import_reg =
self.compile_rego_expr_with_span(import_expr, import_expr.span(), false)?;
if chain.components.is_empty() {
return Ok(import_reg);
}
return self.compile_chain_access(import_reg, &chain.components, span);
}
// No rule or import found - undefined variable
Err(CompilerError::UndefinedVariable {
name: root.to_string(),
}

View File

@@ -337,8 +337,9 @@ impl Analyzer {
}
for import in &m.imports {
if let Some(var) = &import.r#as {
scope.unscoped.insert(var.source_str());
// Ensure default alias imports (e.g. import data.pkg.mod) are visible to this scope.
if let Some(alias_span) = import_alias_span(import) {
scope.unscoped.insert(alias_span.source_str());
}
}
}
@@ -1167,8 +1168,8 @@ pub fn compute_module_globals(
// Add import aliases specific to this module
for import in &m.imports {
if let Some(var) = &import.r#as {
crate::Rc::make_mut(&mut module_globals).insert(var.text().to_string());
if let Some(alias_span) = import_alias_span(import) {
crate::Rc::make_mut(&mut module_globals).insert(alias_span.text().to_string());
}
}
@@ -1198,3 +1199,20 @@ pub fn compute_module_globals(
Ok(result)
}
// Extract the binding name an import contributes, even without an explicit `as` clause.
fn import_alias_span(import: &Import) -> Option<Span> {
if let Some(alias) = &import.r#as {
return Some(alias.clone());
}
match import.refr.as_ref() {
RefDot { field, .. } => Some(field.0.clone()),
RefBrack { index, .. } => match index.as_ref() {
Expr::String { span, .. } => Some(span.clone()),
_ => None,
},
Var { span, .. } => Some(span.clone()),
_ => None,
}
}

View File

@@ -56,6 +56,109 @@ cases:
a: 10
c: 22
r: true
- note: import data inside rule body
modules:
- |
package lib
import rego.v1
a_value := "a_value"
- |
package rules
import data.lib
import rego.v1
sample if {
lib.a_value == "a_value"
}
query: data.rules.sample
want_result: true
- note: import data alias
modules:
- |
package lib
import rego.v1
value := "a_value"
- |
package rules
import data.lib as mylib
import rego.v1
sample if {
mylib.value == "a_value"
}
query: data.rules.sample
want_result: true
- note: import nested package chain
modules:
- |
package lib.inner
import rego.v1
nested := {"key": "value"}
- |
package rules
import data.lib.inner
import rego.v1
lookup := value if {
value := inner.nested.key
}
query: data.rules.lookup
want_result: "value"
- note: import alias shadowed by rule
modules:
- |
package lib
import rego.v1
value := "from data"
- |
package rules
import data.lib as lib_alias
import rego.v1
lib_alias := {"value": "from rule"}
# TODO:
# OPA currently reports this rule as undefined. This implies that
# lib_alias retains the imported value eventhough there is a rule
# with same name. In regorus, the rule takes precedence.
# Needs investigation to figure out which behavior is correct.
shadow if {
lib_alias.value == "from rule"
}
query: data.rules.shadow
want_result: true
- note: import used in comprehension
modules:
- |
package lib
import rego.v1
dataset := {"a", "b"}
- |
package rules
import data.lib
import rego.v1
present if {
some item in lib.dataset
item == "b"
}
query: data.rules.present
want_result: true
- note: import overridden by rule
modules:

View File

@@ -0,0 +1,97 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: import_data_inside_rule_body
modules:
- |
package lib
import rego.v1
a_value := "a_value"
- |
package rules
import data.lib
import rego.v1
sample if {
lib.a_value == "a_value"
}
query: data.rules.sample
want_result: true
- note: import_data_alias
modules:
- |
package lib
import rego.v1
value := "a_value"
- |
package rules
import data.lib as mylib
import rego.v1
sample if {
mylib.value == "a_value"
}
query: data.rules.sample
want_result: true
- note: import_nested_package_chain
modules:
- |
package lib.inner
import rego.v1
nested := {"key": "value"}
- |
package rules
import data.lib.inner
import rego.v1
lookup := value if {
value := inner.nested.key
}
query: data.rules.lookup
want_result: "value"
- note: import_alias_shadowed_by_rule
modules:
- |
package lib
import rego.v1
value := "from data"
- |
package rules
import data.lib as lib_alias
import rego.v1
lib_alias := {"value": "from rule"}
shadow if {
lib_alias.value == "from rule"
}
query: data.rules.shadow
want_result: true
- note: import_used_in_comprehension
modules:
- |
package lib
import rego.v1
dataset := {"a", "b"}
- |
package rules
import data.lib
import rego.v1
present if {
some item in lib.dataset
item == "b"
}
query: data.rules.present
want_result: true