Skip to content

Commit 8b844e4

Browse files
authored
fix(interpreter,rvm): resolve function calls through import aliases (#769)
* fix(interpreter,rvm): resolve function calls through import aliases * fix(rvm): assert every-quantifier results so failing cases don't pass
1 parent 4c183e9 commit 8b844e4

4 files changed

Lines changed: 785 additions & 7 deletions

File tree

src/interpreter.rs

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,7 +1258,34 @@ impl Interpreter {
12581258
// Apply with modifiers.
12591259
for wm in &stmt.with_mods {
12601260
let path = Parser::get_path_ref_components(&wm.refr)?;
1261-
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
1261+
let mut path: Vec<String> = path.iter().map(|s| s.text().to_string()).collect();
1262+
1263+
// Matching OPA, a leading import alias is rewritten before
1264+
// any lookups: functions register as overrides below,
1265+
// anything else becomes a data override. Only the alias
1266+
// component is replaced so bracketed keys containing dots
1267+
// survive the rewrite.
1268+
let rewritten: Option<Vec<String>> = match path.split_first() {
1269+
Some((head, rest)) if head.as_str() != "data" => {
1270+
self.lookup_import(head).and_then(|import_expr| {
1271+
// Use the import target's parsed components, not
1272+
// its dot-joined string, so bracketed keys
1273+
// containing dots survive in the import path too.
1274+
let comps = Parser::get_path_ref_components(import_expr).ok()?;
1275+
Some(
1276+
comps
1277+
.iter()
1278+
.map(|s| s.text().to_string())
1279+
.chain(rest.iter().cloned())
1280+
.collect(),
1281+
)
1282+
})
1283+
}
1284+
_ => None,
1285+
};
1286+
if let Some(new_path) = rewritten {
1287+
path = new_path;
1288+
}
12621289
let mut target = path.join(".");
12631290

12641291
let mut target_is_function = self.lookup_function_by_name(&target).is_some()
@@ -1297,11 +1324,17 @@ impl Interpreter {
12971324
if self.lookup_function_by_name(&function_path).is_none() {
12981325
// Lookup without current module path prefixed.
12991326
function_path = get_path_string(&wm.r#as, None)?;
1300-
if self.lookup_function_by_name(&function_path).is_none()
1301-
&& !Self::is_builtin(wm.r#as.span(), &function_path)
1302-
{
1303-
// bail!(wm.r#as.span().error("could not evaluate expression"));
1304-
skip_exec = true;
1327+
if self.lookup_function_by_name(&function_path).is_none() {
1328+
// Resolve an aliased replacement before builtins.
1329+
let resolved = self
1330+
.resolve_fcn_path_through_imports(&function_path)
1331+
.filter(|r| self.compiled_policy.functions.contains_key(r));
1332+
if let Some(resolved) = resolved {
1333+
function_path = resolved;
1334+
} else if !Self::is_builtin(wm.r#as.span(), &function_path) {
1335+
// bail!(wm.r#as.span().error("could not evaluate expression"));
1336+
skip_exec = true;
1337+
}
13051338
}
13061339
}
13071340
self.with_functions
@@ -2371,6 +2404,72 @@ impl Interpreter {
23712404
}
23722405
}
23732406

2407+
/// Look up the import of the current module with the given alias, e.g.
2408+
/// the `data.a.b` import expression for `b` after `import data.a.b`.
2409+
fn lookup_import(&self, alias: &str) -> Option<&Ref<Expr>> {
2410+
if self.compiled_policy.imports.is_empty() {
2411+
return None;
2412+
}
2413+
let import_key = format!("{}.{}", self.current_module_path, alias);
2414+
self.compiled_policy.imports.get(&import_key)
2415+
}
2416+
2417+
/// Look up the dot-joined target path of an import of the current module
2418+
/// with the given alias, e.g. `data.a.b` for `b` after `import data.a.b`.
2419+
fn lookup_import_alias(&self, alias: &str) -> Option<String> {
2420+
get_path_string(self.lookup_import(alias)?, None).ok()
2421+
}
2422+
2423+
/// Rewrite a path whose leading component is an import alias of the
2424+
/// current module to the import's target, e.g. `b.f` to `data.a.b.f`
2425+
/// after `import data.a.b`.
2426+
fn rewrite_path_through_imports(&self, path: &str) -> Option<String> {
2427+
if path.starts_with("data.") {
2428+
return None;
2429+
}
2430+
2431+
let (alias, rest) = match path.split_once('.') {
2432+
Some((alias, rest)) => (alias, Some(rest)),
2433+
None => (path, None),
2434+
};
2435+
let target = self.lookup_import_alias(alias)?;
2436+
Some(match rest {
2437+
Some(rest) => format!("{target}.{rest}"),
2438+
None => target,
2439+
})
2440+
}
2441+
2442+
/// Rewrite an import-aliased call path to its target, e.g. `b.f(1)` to
2443+
/// `data.a.b.f` after `import data.a.b`. Resolves only when the target is
2444+
/// a known function or default function, so an alias whose target defines
2445+
/// the called function shadows a like-named builtin namespace, while other
2446+
/// spellings keep their prior meaning (e.g. a builtin call). OPA instead
2447+
/// rewrites aliases unconditionally and rejects calls to a missing target
2448+
/// at compile time.
2449+
fn resolve_fcn_path_through_imports(&self, path: &str) -> Option<String> {
2450+
let candidate = self.rewrite_path_through_imports(path)?;
2451+
(self.compiled_policy.functions.contains_key(&candidate)
2452+
|| self.is_default_function(&candidate))
2453+
.then_some(candidate)
2454+
}
2455+
2456+
/// True if `path` is the exact path of a `default` function rule.
2457+
/// `default_rules` also indexes every prefix of a rule path, so it cannot
2458+
/// be consulted alone: `rule_paths` holds only exact rule paths, and the
2459+
/// non-empty argument list distinguishes functions from value rules.
2460+
fn is_default_function(&self, path: &str) -> bool {
2461+
self.compiled_policy.rule_paths.contains(path)
2462+
&& self
2463+
.compiled_policy
2464+
.default_rules
2465+
.get(path)
2466+
.is_some_and(|rules| {
2467+
rules.iter().any(|(rule, _)| {
2468+
matches!(rule.as_ref(), Rule::Default { args, .. } if !args.is_empty())
2469+
})
2470+
})
2471+
}
2472+
23742473
fn eval_builtin_call(
23752474
&mut self,
23762475
span: &Span,
@@ -2542,6 +2641,13 @@ impl Interpreter {
25422641
param_values.push(self.eval_expr(p)?);
25432642
}
25442643

2644+
// Resolve a leading import alias before the `with` override and builtin
2645+
// lookups, so an override keyed by the full path reaches aliased calls
2646+
// and the alias shadows a like-named builtin namespace (matching OPA).
2647+
let fcn_path = self
2648+
.resolve_fcn_path_through_imports(&fcn_path)
2649+
.unwrap_or(fcn_path);
2650+
25452651
let orig_fcn_path = fcn_path.clone();
25462652

25472653
let mut with_functions_saved = None;
@@ -2718,7 +2824,12 @@ impl Interpreter {
27182824
let value = match self.eval_rule_bodies(ctx, span, bodies) {
27192825
Ok(v) => v,
27202826
Err(e) => {
2721-
// If the rule produces an error, save the error.
2827+
// If the rule produces an error, save the error. Restore
2828+
// the caller's module even so: leaving the callee's module
2829+
// in place would make the rest of the caller's body
2830+
// resolve paths through the wrong module's imports when
2831+
// the error is swallowed below in non-strict mode.
2832+
self.set_current_module(prev_module)?;
27222833
errors.push(e);
27232834
self.scopes = scopes;
27242835
continue;

src/languages/rego/compiler/function_calls.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ impl<'a> Compiler<'a> {
6363
let original_fcn_path = fcn_path.clone();
6464
let full_fcn_path = if self.policy.inner.rules.contains_key(&fcn_path) {
6565
fcn_path
66+
} else if let Some(resolved) = self.resolve_fcn_path_through_imports(&original_fcn_path) {
67+
// Resolve a leading import alias before module-prefixing and builtins.
68+
resolved
6669
} else {
6770
get_path_string(fcn, Some(&self.current_package))
6871
.map_err(|_| CompilerError::InvalidFunctionExpressionWithPackage.at(&span))?
@@ -220,6 +223,37 @@ impl<'a> Compiler<'a> {
220223
Ok(dest)
221224
}
222225

226+
/// Rewrite an import-aliased call path to its target, e.g. `b.f(1)` to
227+
/// `data.a.b.f` after `import data.a.b`. Resolves only when the target is
228+
/// a known function (the `rules` map cannot be used here: it also indexes
229+
/// value rules and every rule-path prefix, which must not become callable
230+
/// through an alias), so an alias whose target defines the called
231+
/// function shadows a like-named builtin namespace, while other spellings
232+
/// keep their prior meaning (e.g. a builtin call). OPA instead rewrites
233+
/// aliases unconditionally and rejects calls to a missing target at
234+
/// compile time.
235+
fn resolve_fcn_path_through_imports(&self, path: &str) -> Option<String> {
236+
if self.policy.inner.imports.is_empty() || path.starts_with("data.") {
237+
return None;
238+
}
239+
let (alias, rest) = match path.split_once('.') {
240+
Some((alias, rest)) => (alias, Some(rest)),
241+
None => (path, None),
242+
};
243+
let import_key = format!("{}.{}", self.current_package, alias);
244+
let import_expr = self.policy.inner.imports.get(&import_key)?;
245+
let target = get_path_string(import_expr, None).ok()?;
246+
let candidate = match rest {
247+
Some(rest) => format!("{target}.{rest}"),
248+
None => target,
249+
};
250+
self.policy
251+
.inner
252+
.functions
253+
.contains_key(&candidate)
254+
.then_some(candidate)
255+
}
256+
223257
fn lookup_builtin_arity(&self, name: &str) -> Option<usize> {
224258
if name == "print" {
225259
Some(2)

0 commit comments

Comments
 (0)