use std::collections::HashMap;
use std::error::Error;
use std::fmt::{self, Display, Write};
use std::iter;
use std::ops::{Deref, DerefMut};
use either::Either;
use lalrpop_util as lalr;
use lazy_static::lazy_static;
use miette::{Diagnostic, LabeledSpan, Severity, SourceCode, SourceSpan};
use smol_str::SmolStr;
use thiserror::Error;
use crate::ast::{self, RestrictedExprError};
use crate::ast::{PolicyID, Var};
use crate::parser::unescape::UnescapeError;
use crate::parser::fmt::join_with_conjunction;
use crate::parser::node::ASTNode;
use super::cst;
pub(crate) type RawLocation = usize;
pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
pub(crate) type RawUserError = ASTNode<String>;
pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, RawUserError>;
pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, RawUserError>;
type OwnedRawParseError = lalr::ParseError<RawLocation, String, RawUserError>;
#[derive(Clone, Debug, Diagnostic, Error, PartialEq, Eq)]
pub enum ParseError {
#[error(transparent)]
#[diagnostic(transparent)]
ToCST(#[from] ToCSTError),
#[error(transparent)]
#[diagnostic(code(cedar_policy_core::parser::to_ast_err))]
ToAST(#[from] ToASTError),
#[error(transparent)]
RestrictedExpr(#[from] RestrictedExprError),
#[error(transparent)]
ParseLiteral(#[from] ParseLiteralError),
}
impl ParseError {
pub fn primary_source_span(&self) -> Option<SourceSpan> {
match self {
ParseError::ToCST(to_cst_err) => Some(to_cst_err.primary_source_span()),
ParseError::RestrictedExpr(restricted_expr_err) => match restricted_expr_err {
RestrictedExprError::InvalidRestrictedExpression { .. } => None,
},
ParseError::ToAST(_) | ParseError::ParseLiteral(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Error, Eq)]
pub enum ParseLiteralError {
#[error("the source `{0}` is not a literal")]
ParseLiteral(String),
}
#[derive(Debug, Diagnostic, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToASTError {
#[error("a template with id `{0}` already exists in the policy set")]
DuplicateTemplateId(PolicyID),
#[error("a policy with id `{0}` already exists in the policy set")]
DuplicatePolicyId(PolicyID),
#[error(
"expected a static policy, got a template containing the slot {slot}; try removing the template slot(s) from this policy"
)]
UnexpectedTemplate {
slot: cst::Slot,
},
#[error("this policy uses poorly formed or duplicate annotations")]
BadAnnotations,
#[error("found template slot {slot} in a `{clausetype}` clause; slots are currently unsupported in `{clausetype}` clauses")]
SlotsInConditionClause {
slot: cst::Slot,
clausetype: &'static str,
},
#[error("this policy is missing the `{0}` variable in the scope")]
MissingScopeConstraint(Var),
#[error("this policy has an extra head constraint in the scope; a policy must have exactly `principal`, `action`, and `resource` constraints: `{0}`")]
ExtraHeadConstraints(cst::VariableDef),
#[error("this identifier is reserved and cannot be used: `{0}`")]
ReservedIdentifier(cst::Ident),
#[error("not a valid identifier: `{0}`")]
InvalidIdentifier(String),
#[error("not a valid policy effect: `{0}`. Effect must be either `permit` or `forbid`")]
InvalidEffect(cst::Ident),
#[error("not a valid policy condition: `{0}`. Condition must be either `when` or `unless`")]
InvalidCondition(cst::Ident),
#[error("expected a variable that is valid in the policy scope. Must be one of `principal`, `action`, or `resource`. Found: `{0}`")]
InvalidScopeConstraintVariable(cst::Ident),
#[error("not a valid method name: `{0}`")]
InvalidMethodName(String),
#[error("the variable `{got}` is invalid in this policy scope clause, the variable `{expected}` is expected")]
IncorrectVariable {
expected: Var,
got: Var,
},
#[error("policy scope constraints must either `==` or `in`. Found `{0}`")]
InvalidConstraintOperator(cst::RelOp),
#[error(
"the right hand side of equality in the policy scope must be a single entity uid or a template slot"
)]
InvalidScopeEqualityRHS,
#[error("expected an entity uid with the type `Action` but got `{0}`. Action entities must have type `Action`")]
InvalidActionType(crate::ast::EntityUID),
#[error("{}condition clause cannot be empty", match .0 { Some(ident) => format!("`{}` ", ident), None => "".to_string() })]
EmptyClause(Option<cst::Ident>),
#[error("internal invariant violated. No parse errors were reported but annotation information was missing")]
AnnotationInvariantViolation,
#[error("internal invariant violated. Membership chain did not resolve to an expression")]
MembershipInvariantViolation,
#[error("invalid string literal: `{0}`")]
InvalidString(String),
#[error("arbitrary variables are not supported; did you mean to enclose `{0}` in quotes to make a string? The valid Cedar variable are `principal`, `action`, `resource`, and `context`")]
ArbitraryVariable(SmolStr),
#[error(
"invalid attribute name: `{0}`. Attribute names can either be identifiers or string literals"
)]
InvalidAttribute(SmolStr),
#[error("record literal has invalid attributes")]
InvalidAttributesInRecordLiteral,
#[error("`{0}` cannot be used as an attribute as it contains a namespace")]
PathAsAttribute(String),
#[error("`{0}` is a method, not a function. Use a method-style call: `e.{0}(..)`")]
FunctionCallOnMethod(crate::ast::Id),
#[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
InvalidPattern(String),
#[error("expected {expected}, found {got}{}",
match .suggestion {
None => "".into(),
Some(s) => format!("; {s}"),
}
)]
WrongNode {
expected: &'static str,
got: String,
suggestion: Option<String>,
},
#[error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")]
AmbiguousOperators,
#[error("division is not supported")]
UnsupportedDivision,
#[error("remainder/modulo is not supported")]
UnsupportedModulo,
#[error("multiplication must be by an integer literal")]
NonConstantMultiplication,
#[error(
"integer literal `{0}` is too large. Maximum allowed integer literal is `{}`",
i64::MAX
)]
IntegerLiteralTooLarge(u64),
#[error(
"too many occurrences of `{0}`. Cannot chain more the 4 applications of a unary operator"
)]
UnaryOpLimit(crate::ast::UnaryOp),
#[error("variables cannot be called as functions. `{0}(...)` is not a valid function call")]
VariableCall(crate::ast::Var),
#[error("attempted to call `{0}.{1}`, but `{0}` does not have any methods")]
NoMethods(crate::ast::Name, ast::Id),
#[error("`{0}` is not a function")]
NotAFunction(crate::ast::Name),
#[error("entity literals are not supported")]
UnsupportedEntityLiterals,
#[error("function calls must be of the form: `<name>(arg1, arg2, ...)`")]
ExpressionCall,
#[error("incorrect member access `{0}.{1}`, `{0}` has no fields or methods")]
InvalidAccess(crate::ast::Name, SmolStr),
#[error("incorrect indexing expression `{0}[{1}]`, `{0}` has no fields")]
InvalidIndex(crate::ast::Name, SmolStr),
#[error("the contents of an index expression must be a string literal")]
NonStringIndex,
#[error("duplicate key `{key}` in record literal")]
DuplicateKeyInRecordLiteral {
key: SmolStr,
},
#[error("type constraints using `:` are not supported. Try using `is` instead")]
TypeConstraints,
#[error("a path is not valid in this context")]
InvalidPath,
#[error("`{kind}` needs to be normalized (e.g., whitespace removed): `{src}`. The normalized form is `{normalized_src}`")]
NonNormalizedString {
kind: &'static str,
src: String,
normalized_src: String,
},
#[error("data should not be empty")]
MissingNodeData,
#[error("the right hand side of a `has` expression must be a field name or string literal")]
HasNonLiteralRHS,
#[error("`{0}` is not a valid expression")]
InvalidExpression(cst::Name),
#[error("call to `{name}` requires exactly {expected} argument{}, but got {got} arguments", if .expected == &1 { "" } else { "s" })]
WrongArity {
name: &'static str,
expected: usize,
got: usize,
},
#[error(transparent)]
Unescape(#[from] UnescapeError),
#[error(transparent)]
RefCreation(#[from] RefCreationError),
#[error(transparent)]
InvalidIs(#[from] InvalidIsError),
}
impl ToASTError {
pub fn wrong_node(
expected: &'static str,
got: impl Into<String>,
suggestion: Option<impl Into<String>>,
) -> Self {
Self::WrongNode {
expected,
got: got.into(),
suggestion: suggestion.map(Into::into),
}
}
pub fn wrong_arity(name: &'static str, expected: usize, got: usize) -> Self {
Self::WrongArity {
name,
expected,
got,
}
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum RefCreationError {
#[error("expected {}, got: {got}", match .expected { Either::Left(r) => r.to_string(), Either::Right((r1, r2)) => format!("{r1} or {r2}") })]
RefCreation {
expected: Either<Ref, (Ref, Ref)>,
got: Ref,
},
}
impl RefCreationError {
pub fn one_expected(expected: Ref, got: Ref) -> Self {
Self::RefCreation {
expected: Either::Left(expected),
got,
}
}
pub fn two_expected(r1: Ref, r2: Ref, got: Ref) -> Self {
let expected = Either::Right((r1, r2));
Self::RefCreation { expected, got }
}
}
impl From<RefCreationError> for ParseError {
fn from(value: RefCreationError) -> Self {
ParseError::ToAST(value.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ref {
Single,
Set,
Template,
}
impl std::fmt::Display for Ref {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ref::Single => write!(f, "single entity uid"),
Ref::Template => write!(f, "template slot"),
Ref::Set => write!(f, "set of entity uids"),
}
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum InvalidIsError {
#[error("`is` cannot appear in the action scope")]
ActionScope,
#[error("`is` cannot appear in the scope at the same time as `{0}`")]
WrongOp(cst::RelOp),
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub struct ToCSTError {
err: OwnedRawParseError,
}
impl ToCSTError {
pub fn primary_source_span(&self) -> SourceSpan {
match &self.err {
OwnedRawParseError::InvalidToken { location } => *location..*location,
OwnedRawParseError::UnrecognizedEof { location, .. } => *location..*location,
OwnedRawParseError::UnrecognizedToken {
token: (token_start, _, token_end),
..
} => *token_start..*token_end,
OwnedRawParseError::ExtraToken {
token: (token_start, _, token_end),
} => *token_start..*token_end,
OwnedRawParseError::User { error } => error.info.0.clone(),
}
.into()
}
pub(crate) fn from_raw_parse_err(err: RawParseError<'_>) -> Self {
Self {
err: err.map_token(|token| token.to_string()),
}
}
pub(crate) fn from_raw_err_recovery(recovery: RawErrorRecovery<'_>) -> Self {
Self::from_raw_parse_err(recovery.error)
}
}
impl Display for ToCSTError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.err {
OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
OwnedRawParseError::UnrecognizedToken {
token: (_, token, _),
..
} => write!(f, "unexpected token `{token}`"),
OwnedRawParseError::ExtraToken {
token: (_, token, _),
..
} => write!(f, "extra token `{token}`"),
OwnedRawParseError::User { error } => write!(f, "{error}"),
}
}
}
impl Diagnostic for ToCSTError {
fn code(&self) -> Option<Box<dyn Display + '_>> {
Some(Box::new("cedar_policy_core::parser::to_cst_error"))
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let primary_source_span = self.primary_source_span();
let labeled_span = match &self.err {
OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span),
OwnedRawParseError::UnrecognizedEof { expected, .. } => {
LabeledSpan::new_with_span(expected_to_string(expected), primary_source_span)
}
OwnedRawParseError::UnrecognizedToken { expected, .. } => {
LabeledSpan::new_with_span(expected_to_string(expected), primary_source_span)
}
OwnedRawParseError::ExtraToken {
token: (token_start, _, token_end),
} => LabeledSpan::underline(*token_start..*token_end),
OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
};
Some(Box::new(iter::once(labeled_span)))
}
}
lazy_static! {
static ref FRIENDLY_TOKEN_NAMES: HashMap<&'static str, &'static str> = HashMap::from([
("TRUE", "`true`"),
("FALSE", "`false`"),
("IF", "`if`"),
("PERMIT", "`permit`"),
("FORBID", "`forbid`"),
("WHEN", "`when`"),
("UNLESS", "`unless`"),
("IN", "`in`"),
("HAS", "`has`"),
("LIKE", "`like`"),
("IS", "`is`"),
("THEN", "`then`"),
("ELSE", "`else`"),
("PRINCIPAL", "`principal`"),
("ACTION", "`action`"),
("RESOURCE", "`resource`"),
("CONTEXT", "`context`"),
("PRINCIPAL_SLOT", "`?principal`"),
("RESOURCE_SLOT", "`?resource`"),
("IDENTIFIER", "identifier"),
("NUMBER", "number"),
("STRINGLIT", "string literal"),
]);
}
fn expected_to_string(expected: &[String]) -> Option<String> {
if expected.is_empty() {
return None;
}
let mut expected_string = "expected ".to_owned();
#[allow(clippy::expect_used)]
join_with_conjunction(&mut expected_string, "or", expected, |f, token| {
match FRIENDLY_TOKEN_NAMES.get(token.as_str()) {
Some(friendly_token_name) => write!(f, "{}", friendly_token_name),
None => write!(f, "{}", token.replace('"', "`")),
}
})
.expect("failed to format expected tokens");
Some(expected_string)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ParseErrors(pub Vec<ParseError>);
impl ParseErrors {
const DESCRIPTION_IF_EMPTY: &'static str = "unknown parse error";
pub fn new() -> Self {
ParseErrors(Vec::new())
}
pub fn with_capacity(capacity: usize) -> Self {
ParseErrors(Vec::with_capacity(capacity))
}
pub(super) fn push(&mut self, err: impl Into<ParseError>) {
self.0.push(err.into());
}
pub fn errors_as_strings(&self) -> Vec<String> {
self.0
.iter()
.map(|parser_error| format!("{}", parser_error))
.collect()
}
}
impl Display for ParseErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.first() {
Some(first_err) => Display::fmt(first_err, f),
None => write!(f, "{}", Self::DESCRIPTION_IF_EMPTY),
}
}
}
impl Error for ParseErrors {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.first().and_then(Error::source)
}
#[allow(deprecated)]
fn description(&self) -> &str {
match self.first() {
Some(first_err) => first_err.description(),
None => Self::DESCRIPTION_IF_EMPTY,
}
}
#[allow(deprecated)]
fn cause(&self) -> Option<&dyn Error> {
self.first().and_then(Error::cause)
}
}
impl Diagnostic for ParseErrors {
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
errs.next().map(move |first_err| match first_err.related() {
Some(first_err_related) => Box::new(first_err_related.chain(errs)),
None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
})
}
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().and_then(Diagnostic::code)
}
fn severity(&self) -> Option<Severity> {
self.first().and_then(Diagnostic::severity)
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().and_then(Diagnostic::help)
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.first().and_then(Diagnostic::url)
}
fn source_code(&self) -> Option<&dyn SourceCode> {
self.first().and_then(Diagnostic::source_code)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.first().and_then(Diagnostic::labels)
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.first().and_then(Diagnostic::diagnostic_source)
}
}
impl AsRef<Vec<ParseError>> for ParseErrors {
fn as_ref(&self) -> &Vec<ParseError> {
&self.0
}
}
impl AsMut<Vec<ParseError>> for ParseErrors {
fn as_mut(&mut self) -> &mut Vec<ParseError> {
&mut self.0
}
}
impl AsRef<[ParseError]> for ParseErrors {
fn as_ref(&self) -> &[ParseError] {
self.0.as_ref()
}
}
impl AsMut<[ParseError]> for ParseErrors {
fn as_mut(&mut self) -> &mut [ParseError] {
self.0.as_mut()
}
}
impl Deref for ParseErrors {
type Target = Vec<ParseError>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ParseErrors {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Into<ParseError>> From<T> for ParseErrors {
fn from(err: T) -> Self {
vec![err.into()].into()
}
}
impl From<Vec<ParseError>> for ParseErrors {
fn from(errs: Vec<ParseError>) -> Self {
ParseErrors(errs)
}
}
impl<T: Into<ParseError>> FromIterator<T> for ParseErrors {
fn from_iter<I: IntoIterator<Item = T>>(errs: I) -> Self {
ParseErrors(errs.into_iter().map(Into::into).collect())
}
}
impl<T: Into<ParseError>> Extend<T> for ParseErrors {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
self.0.extend(iter.into_iter().map(Into::into))
}
}
impl IntoIterator for ParseErrors {
type Item = ParseError;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a ParseErrors {
type Item = &'a ParseError;
type IntoIter = std::slice::Iter<'a, ParseError>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a mut ParseErrors {
type Item = &'a mut ParseError;
type IntoIter = std::slice::IterMut<'a, ParseError>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter_mut()
}
}