From 9426b2ec026aee7159adf7fab365593ae97ab5da Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Thu, 15 Jan 2026 03:57:52 +0530 Subject: [PATCH] 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 --- src/languages/rego/compiler/references.rs | 13 ++- src/scheduler.rs | 26 +++++- tests/interpreter/cases/import/tests.yaml | 103 ++++++++++++++++++++++ tests/rvm/rego/cases/imports.yaml | 97 ++++++++++++++++++++ 4 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 tests/rvm/rego/cases/imports.yaml diff --git a/src/languages/rego/compiler/references.rs b/src/languages/rego/compiler/references.rs index 3c44b72..e5bfc7a 100644 --- a/src/languages/rego/compiler/references.rs +++ b/src/languages/rego/compiler/references.rs @@ -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(), } diff --git a/src/scheduler.rs b/src/scheduler.rs index 1d7316c..d1d8332 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -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 { + 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, + } +} diff --git a/tests/interpreter/cases/import/tests.yaml b/tests/interpreter/cases/import/tests.yaml index 721a4ad..4da1151 100644 --- a/tests/interpreter/cases/import/tests.yaml +++ b/tests/interpreter/cases/import/tests.yaml @@ -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: diff --git a/tests/rvm/rego/cases/imports.yaml b/tests/rvm/rego/cases/imports.yaml new file mode 100644 index 0000000..198a392 --- /dev/null +++ b/tests/rvm/rego/cases/imports.yaml @@ -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