Skip to content

Commit 4816432

Browse files
Improve error on incorrect function/method calls (#482)
1 parent fe5d65c commit 4816432

File tree

4 files changed

+128
-42
lines changed

4 files changed

+128
-42
lines changed

cedar-policy-core/src/est/expr.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,19 +1266,25 @@ impl TryFrom<&ASTNode<Option<cst::Member>>> for Expr {
12661266
}
12671267
}
12681268

1269-
fn extract_single_argument(
1270-
es: impl ExactSizeIterator<Item = Expr>,
1269+
/// Return the single argument in `args` iterator, or return a wrong arity error
1270+
/// if the iterator has 0 elements or more than 1 element.
1271+
pub fn extract_single_argument<T>(
1272+
args: impl ExactSizeIterator<Item = T>,
12711273
fn_name: &'static str,
12721274
span: miette::SourceSpan,
1273-
) -> Result<Expr, ParseErrors> {
1274-
let mut iter = es.fuse().peekable();
1275+
) -> Result<T, ToASTError> {
1276+
let mut iter = args.fuse().peekable();
12751277
let first = iter.next();
12761278
let second = iter.peek();
12771279
match (first, second) {
1278-
(None, _) => Err(ToASTError::new(ToASTErrorKind::wrong_arity(fn_name, 1, 0), span).into()),
1279-
(Some(_), Some(_)) => {
1280-
Err(ToASTError::new(ToASTErrorKind::wrong_arity(fn_name, 1, iter.len()), span).into())
1281-
}
1280+
(None, _) => Err(ToASTError::new(
1281+
ToASTErrorKind::wrong_arity(fn_name, 1, 0),
1282+
span,
1283+
)),
1284+
(Some(_), Some(_)) => Err(ToASTError::new(
1285+
ToASTErrorKind::wrong_arity(fn_name, 1, iter.len() + 1),
1286+
span,
1287+
)),
12821288
(Some(first), None) => Ok(first),
12831289
}
12841290
}

cedar-policy-core/src/parser/cst_to_ast.rs

Lines changed: 105 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use crate::ast::{
4444
ExprConstructionError, PatternElem, PolicySetError, PrincipalConstraint,
4545
PrincipalOrResourceConstraint, ResourceConstraint,
4646
};
47+
use crate::est::extract_single_argument;
4748
use itertools::Either;
4849
use smol_str::SmolStr;
4950
use std::cmp::Ordering;
@@ -451,32 +452,37 @@ impl ast::Id {
451452
errs: &mut ParseErrors,
452453
span: miette::SourceSpan,
453454
) -> Option<ast::Expr> {
454-
let mut adj_args = args.iter_mut().peekable();
455-
match (self.as_ref(), adj_args.next(), adj_args.peek()) {
456-
("contains", Some(a), None) => {
457-
// move the value out of the argument, replacing it with a dummy,
458-
// after this we can no longer use the original args
459-
let arg = mem::replace(a, ast::Expr::val(false));
460-
Some(construct_method_contains(e, arg, span))
461-
}
462-
("containsAll", Some(a), None) => {
463-
let arg = mem::replace(a, ast::Expr::val(false));
464-
Some(construct_method_contains_all(e, arg, span))
465-
}
466-
("containsAny", Some(a), None) => {
467-
let arg = mem::replace(a, ast::Expr::val(false));
468-
Some(construct_method_contains_any(e, arg, span))
469-
}
470-
(name, _, _) => {
471-
if EXTENSION_STYLES.methods.contains(&name) {
455+
match self.as_ref() {
456+
"contains" => extract_single_argument(args.into_iter(), "contains", span)
457+
.map(|arg| construct_method_contains(e, arg, span))
458+
.map_err(|err| errs.push(err))
459+
.ok(),
460+
"containsAll" => extract_single_argument(args.into_iter(), "containsAll", span)
461+
.map(|arg| construct_method_contains_all(e, arg, span))
462+
.map_err(|err| errs.push(err))
463+
.ok(),
464+
"containsAny" => extract_single_argument(args.into_iter(), "containsAny", span)
465+
.map(|arg| construct_method_contains_any(e, arg, span))
466+
.map_err(|err| errs.push(err))
467+
.ok(),
468+
id => {
469+
if EXTENSION_STYLES.methods.contains(&id) {
472470
args.insert(0, e);
473471
// INVARIANT (MethodStyleArgs), we call insert above, so args is non-empty
474-
Some(construct_ext_meth(name.to_string(), args, span))
472+
Some(construct_ext_meth(id.to_string(), args, span))
475473
} else {
476-
errs.push(ToASTError::new(
477-
ToASTErrorKind::InvalidMethodName(name.to_string()),
478-
span,
479-
));
474+
let unqual_name = ast::Name::unqualified_name(self.clone());
475+
if EXTENSION_STYLES.functions.contains(&unqual_name) {
476+
errs.push(ToASTError::new(
477+
ToASTErrorKind::MethodCallOnFunction(unqual_name.id),
478+
span,
479+
));
480+
} else {
481+
errs.push(ToASTError::new(
482+
ToASTErrorKind::InvalidMethodName(id.to_string()),
483+
span,
484+
));
485+
}
480486
None
481487
}
482488
}
@@ -2008,15 +2014,14 @@ impl ast::Name {
20082014
// error on standard methods
20092015
if self.path.is_empty() {
20102016
let id = self.id.as_ref();
2011-
match id {
2012-
"contains" | "containsAll" | "containsAny" => {
2013-
errs.push(ToASTError::new(
2014-
ToASTErrorKind::FunctionCallOnMethod(self.id),
2015-
span,
2016-
));
2017-
return None;
2018-
}
2019-
_ => {}
2017+
if EXTENSION_STYLES.methods.contains(id)
2018+
|| matches!(id, "contains" | "containsAll" | "containsAny")
2019+
{
2020+
errs.push(ToASTError::new(
2021+
ToASTErrorKind::FunctionCallOnMethod(self.id),
2022+
span,
2023+
));
2024+
return None;
20202025
}
20212026
}
20222027
if EXTENSION_STYLES.functions.contains(&self) {
@@ -4116,4 +4121,72 @@ mod tests {
41164121
}
41174122
);
41184123
}
4124+
4125+
#[test]
4126+
fn invalid_methods_function_calls() {
4127+
let invalid_exprs = [
4128+
(
4129+
r#"contains([], 1)"#,
4130+
ExpectedErrorMessage::error_and_help(
4131+
"`contains` is a method, not a function",
4132+
"use a method-style call: `e.contains(..)`",
4133+
),
4134+
),
4135+
(
4136+
r#"[].contains()"#,
4137+
ExpectedErrorMessage::error(
4138+
"call to `contains` requires exactly 1 argument, but got 0 arguments",
4139+
),
4140+
),
4141+
(
4142+
r#"[].contains(1, 2)"#,
4143+
ExpectedErrorMessage::error(
4144+
"call to `contains` requires exactly 1 argument, but got 2 arguments",
4145+
),
4146+
),
4147+
(
4148+
r#"[].containsAll()"#,
4149+
ExpectedErrorMessage::error(
4150+
"call to `containsAll` requires exactly 1 argument, but got 0 arguments",
4151+
),
4152+
),
4153+
(
4154+
r#"[].containsAll(1, 2)"#,
4155+
ExpectedErrorMessage::error(
4156+
"call to `containsAll` requires exactly 1 argument, but got 2 arguments",
4157+
),
4158+
),
4159+
(
4160+
r#"[].containsAny()"#,
4161+
ExpectedErrorMessage::error(
4162+
"call to `containsAny` requires exactly 1 argument, but got 0 arguments",
4163+
),
4164+
),
4165+
(
4166+
r#"[].containsAny(1, 2)"#,
4167+
ExpectedErrorMessage::error(
4168+
"call to `containsAny` requires exactly 1 argument, but got 2 arguments",
4169+
),
4170+
),
4171+
(
4172+
r#""1.1.1.1".ip()"#,
4173+
ExpectedErrorMessage::error_and_help(
4174+
"`ip` is a function, not a method",
4175+
"use a function-style call: `ip(..)`",
4176+
),
4177+
),
4178+
(
4179+
r#"greaterThan(1, 2)"#,
4180+
ExpectedErrorMessage::error_and_help(
4181+
"`greaterThan` is a method, not a function",
4182+
"use a method-style call: `e.greaterThan(..)`",
4183+
),
4184+
),
4185+
];
4186+
for (src, expected) in invalid_exprs {
4187+
assert_matches!(parse_expr(src), Err(e) => {
4188+
expect_err(src, &e, &expected);
4189+
});
4190+
}
4191+
}
41194192
}

cedar-policy-core/src/parser/err.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ pub enum ToASTErrorKind {
260260
#[error("`{0}` is a method, not a function")]
261261
#[diagnostic(help("use a method-style call: `e.{0}(..)`"))]
262262
FunctionCallOnMethod(crate::ast::Id),
263+
/// Returned when a policy attempts to call a function in the method style
264+
#[error("`{0}` is a function, not a method")]
265+
#[diagnostic(help("use a function-style call: `{0}(..)`"))]
266+
MethodCallOnFunction(crate::ast::Id),
263267
/// Returned when the right hand side of a `like` expression is not a constant pattern literal
264268
#[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
265269
InvalidPattern(String),
@@ -357,10 +361,10 @@ pub enum ToASTErrorKind {
357361
/// Returned when a CST expression is invalid
358362
#[error("`{0}` is not a valid expression")]
359363
InvalidExpression(cst::Name),
360-
/// Returned when a function has wrong arity
364+
/// Returned when a function or method is called with the wrong arity
361365
#[error("call to `{name}` requires exactly {expected} argument{}, but got {got} argument{}", if .expected == &1 { "" } else { "s" }, if .got == &1 { "" } else { "s" })]
362366
WrongArity {
363-
/// Name of the function being called
367+
/// Name of the function or method being called
364368
name: &'static str,
365369
/// The expected number of arguments
366370
expected: usize,

cedar-policy/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535

3636
### Changed
3737

38+
- Improve parser error messages to more reliably notice that a function or
39+
method does exists when it is called with an incorrect number of arguments or
40+
using the wrong call style.
3841
- Include source spans on more parser error messages.
3942
- Better integration with `miette` for various error types. If you have
4043
previously been just using the `Display` trait to get the error message from a

0 commit comments

Comments
 (0)