use std::collections::{BTreeSet, HashMap, HashSet};
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, SourceSpan};
use smol_str::SmolStr;
use thiserror::Error;
use crate::ast::{self, ExprConstructionError, InputInteger, PolicyID, RestrictedExprError, Var};
use crate::parser::fmt::join_with_conjunction;
use crate::parser::loc::Loc;
use crate::parser::node::Node;
use crate::parser::unescape::UnescapeError;
use super::cst;
pub(crate) type RawLocation = usize;
pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
pub(crate) type RawUserError = Node<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(transparent)]
ToAST(#[from] ToASTError),
#[error(transparent)]
#[diagnostic(transparent)]
RestrictedExpr(#[from] RestrictedExprError),
#[error(transparent)]
#[diagnostic(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::ToAST(to_ast_err) => Some(to_ast_err.source_loc().span),
ParseError::RestrictedExpr(restricted_expr_err) => match restricted_expr_err {
RestrictedExprError::InvalidRestrictedExpression { expr, .. } => {
expr.source_loc().map(|loc| loc.span)
}
},
ParseError::ParseLiteral(parse_lit_err) => parse_lit_err
.labels()
.and_then(|mut it| it.next().map(|lspan| *lspan.inner())),
}
}
}
#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq)]
pub enum ParseLiteralError {
#[error("`{0}` is not a literal")]
ParseLiteral(String),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{kind}")]
pub struct ToASTError {
kind: ToASTErrorKind,
loc: Loc,
}
impl Diagnostic for ToASTError {
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
Some(Box::new(iter::once(LabeledSpan::underline(self.loc.span))))
}
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.kind.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.kind.url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.loc.src as &dyn miette::SourceCode)
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.kind.diagnostic_source()
}
}
impl ToASTError {
pub fn new(kind: ToASTErrorKind, loc: Loc) -> Self {
Self { kind, loc }
}
pub fn kind(&self) -> &ToASTErrorKind {
&self.kind
}
pub(crate) fn source_loc(&self) -> &Loc {
&self.loc
}
}
#[derive(Debug, Diagnostic, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToASTErrorKind {
#[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}")]
#[diagnostic(help("try removing the template slot(s) from this policy"))]
UnexpectedTemplate {
slot: cst::Slot,
},
#[error("duplicate annotation: @{0}")]
DuplicateAnnotation(ast::AnyId),
#[error("found template slot {slot} in a `{clausetype}` clause")]
#[diagnostic(help("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: `{0}`")]
#[diagnostic(help(
"a policy must have exactly `principal`, `action`, and `resource` constraints"
))]
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("'=' is not a valid operator in Cedar")]
#[diagnostic(help("try using '==' instead"))]
InvalidSingleEq,
#[error("not a valid policy effect: `{0}`")]
#[diagnostic(help("effect must be either `permit` or `forbid`"))]
InvalidEffect(cst::Ident),
#[error("not a valid policy condition: `{0}`")]
#[diagnostic(help("condition must be either `when` or `unless`"))]
InvalidCondition(cst::Ident),
#[error("expected a variable that is valid in the policy scope; found: `{0}`")]
#[diagnostic(help(
"policy scopes must contain a `principal`, `action`, and `resource` element in that order"
))]
InvalidScopeConstraintVariable(cst::Ident),
#[error("not a valid method name: `{0}`")]
InvalidMethodName(String),
#[error("found the variable `{got}` where the variable `{expected}` must be used")]
#[diagnostic(help(
"policy scopes must contain a `principal`, `action`, and `resource` element in that order"
))]
IncorrectVariable {
expected: Var,
got: Var,
},
#[error("not a valid policy scope constraint: {0}")]
#[diagnostic(help(
"policy scope constraints must be either `==`, `in`, `is`, or `_ is _ in _`"
))]
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}`")]
#[diagnostic(help("action entities must have type `Action`, optionally in a namespace"))]
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; the valid Cedar variables are `principal`, `action`, `resource`, and `context`")]
#[diagnostic(help("did you mean to enclose `{0}` in quotes to make a string?"))]
ArbitraryVariable(SmolStr),
#[error("not a valid attribute name: `{0}`")]
#[diagnostic(help("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")]
#[diagnostic(help("use a method-style call: `e.{0}(..)`"))]
FunctionCallOnMethod(crate::ast::Id),
#[error("`{0}` is a function, not a method")]
#[diagnostic(help("use a function-style call: `{0}(..)`"))]
MethodCallOnFunction(crate::ast::Id),
#[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
InvalidPattern(String),
#[error("right hand side of an `is` expression must be an entity type name, but got `{0}`")]
#[diagnostic(help("try using `==` to test for equality"))]
IsInvalidName(String),
#[error("expected {expected}, found {got}")]
WrongNode {
expected: &'static str,
got: String,
#[help]
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(transparent)]
#[diagnostic(transparent)]
ExprConstructionError(#[from] ExprConstructionError),
#[error("integer literal `{0}` is too large")]
#[diagnostic(help("maximum allowed integer literal is `{}`", InputInteger::MAX))]
IntegerLiteralTooLarge(u64),
#[error("too many occurrences of `{0}`")]
#[diagnostic(help("cannot chain more the 4 applications of a unary operator"))]
UnaryOpLimit(crate::ast::UnaryOp),
#[error("`{0}(...)` is not a valid function call")]
#[diagnostic(help("variables cannot be called as functions"))]
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("type constraints using `:` are not supported")]
#[diagnostic(help("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}`")]
#[diagnostic(help("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} argument{}", if .expected == &1 { "" } else { "s" }, if .got == &1 { "" } else { "s" })]
WrongArity {
name: &'static str,
expected: usize,
got: usize,
},
#[error(transparent)]
#[diagnostic(transparent)]
Unescape(#[from] UnescapeError),
#[error(transparent)]
#[diagnostic(transparent)]
RefCreation(#[from] RefCreationError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidIs(#[from] InvalidIsError),
#[error("`{0}` is not a valid template slot")]
#[diagnostic(help("a template slot may only be `?principal` or `?resource`"))]
InvalidSlot(SmolStr),
}
impl ToASTErrorKind {
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, Diagnostic, 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 }
}
}
#[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, Diagnostic, Error, PartialEq, Eq)]
pub enum InvalidIsError {
#[error("`is` cannot appear in the action scope")]
#[diagnostic(help("try moving `action is ..` into a `when` condition"))]
ActionScope,
#[error("`is` cannot appear in the scope at the same time as `{0}`")]
#[diagnostic(help("try moving `is` into a `when` condition"))]
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 } => SourceSpan::from(*location),
OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
OwnedRawParseError::UnrecognizedToken {
token: (token_start, _, token_end),
..
} => SourceSpan::from(*token_start..*token_end),
OwnedRawParseError::ExtraToken {
token: (token_start, _, token_end),
} => SourceSpan::from(*token_start..*token_end),
OwnedRawParseError::User { error } => error.loc.span,
}
}
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 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, &POLICY_TOKEN_CONFIG),
primary_source_span,
),
OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span(
expected_to_string(expected, &POLICY_TOKEN_CONFIG),
primary_source_span,
),
OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span),
OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
};
Some(Box::new(iter::once(labeled_span)))
}
}
#[derive(Debug)]
pub struct ExpectedTokenConfig {
pub friendly_token_names: HashMap<&'static str, &'static str>,
pub impossible_tokens: HashSet<&'static str>,
pub special_identifier_tokens: HashSet<&'static str>,
pub identifier_sentinel: &'static str,
pub first_set_identifier_tokens: HashSet<&'static str>,
pub first_set_sentinel: &'static str,
}
lazy_static! {
static ref POLICY_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig {
friendly_token_names: 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"),
]),
impossible_tokens: HashSet::from(["\"=\"", "\"%\"", "\"/\"", "OTHER_SLOT"]),
special_identifier_tokens: HashSet::from([
"PERMIT",
"FORBID",
"WHEN",
"UNLESS",
"IN",
"HAS",
"LIKE",
"IS",
"THEN",
"ELSE",
"PRINCIPAL",
"ACTION",
"RESOURCE",
"CONTEXT",
]),
identifier_sentinel: "IDENTIFIER",
first_set_identifier_tokens: HashSet::from(["TRUE", "FALSE", "IF"]),
first_set_sentinel: "\"!\"",
};
}
pub fn expected_to_string(expected: &[String], config: &ExpectedTokenConfig) -> Option<String> {
let mut expected = expected
.iter()
.filter(|e| !config.impossible_tokens.contains(e.as_str()))
.map(|e| e.as_str())
.collect::<BTreeSet<_>>();
if expected.contains(config.identifier_sentinel) {
for token in config.special_identifier_tokens.iter() {
expected.remove(*token);
}
if !expected.contains(config.first_set_sentinel) {
for token in config.first_set_identifier_tokens.iter() {
expected.remove(*token);
}
}
}
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 config.friendly_token_names.get(token) {
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(ToString::to_string).collect()
}
}
impl Display for ParseErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.first() {
Some(first_err) => write!(f, "{first_err}"), None => write!(f, "{}", Self::DESCRIPTION_IF_EMPTY),
}
}
}
impl std::error::Error for ParseErrors {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.first().and_then(std::error::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 std::error::Error> {
self.first().and_then(std::error::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<miette::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 miette::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()
}
}