use std::{
collections::{HashMap, HashSet},
fmt::Display,
iter::{Chain, Once},
sync::{Arc, LazyLock},
vec,
};
use crate::{
ast::AnyId,
parser::{
err::{expected_to_string, ExpectedTokenConfig},
unescape::UnescapeError,
Loc, Node,
},
};
use lalrpop_util as lalr;
use miette::{Diagnostic, LabeledSpan, SourceSpan};
use nonempty::NonEmpty;
use smol_str::{SmolStr, ToSmolStr};
use thiserror::Error;
use super::ast::PR;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum UserError {
#[error("An empty list was passed")]
EmptyList(Node<()>),
#[error("Invalid escape codes")]
StringEscape(Node<NonEmpty<UnescapeError>>),
#[error("`{0}` is a reserved identifier")]
ReservedIdentifierUsed(Node<SmolStr>),
#[error("duplicate annotations: `{}`", .0)]
DuplicateAnnotations(AnyId, Node<()>, Node<()>),
}
impl UserError {
pub(crate) fn primary_source_span(&self) -> Option<SourceSpan> {
match self {
Self::EmptyList(n) => n.loc.as_ref().map(|loc| loc.span),
Self::StringEscape(n) => n.loc.as_ref().map(|loc| loc.span),
Self::ReservedIdentifierUsed(n) => n.loc.as_ref().map(|loc| loc.span),
Self::DuplicateAnnotations(_, n, _) => n.loc.as_ref().map(|loc| loc.span),
}
}
}
pub(crate) type RawLocation = usize;
pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, UserError>;
pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, UserError>;
type OwnedRawParseError = lalr::ParseError<RawLocation, String, UserError>;
static SCHEMA_TOKEN_CONFIG: LazyLock<ExpectedTokenConfig> = LazyLock::new(|| ExpectedTokenConfig {
friendly_token_names: HashMap::from([
("IN", "`in`"),
("PRINCIPAL", "`principal`"),
("ACTION", "`action`"),
("RESOURCE", "`resource`"),
("CONTEXT", "`context`"),
("STRINGLIT", "string literal"),
("ENTITY", "`entity`"),
("NAMESPACE", "`namespace`"),
("TYPE", "`type`"),
("SET", "`Set`"),
("IDENTIFIER", "identifier"),
("TAGS", "`tags`"),
("ENUM", "`enum`"),
]),
impossible_tokens: HashSet::new(),
special_identifier_tokens: HashSet::from([
"NAMESPACE",
"ENTITY",
"IN",
"TYPE",
"APPLIESTO",
"PRINCIPAL",
"ACTION",
"RESOURCE",
"CONTEXT",
"ATTRIBUTES",
"TAGS",
"LONG",
"STRING",
"BOOL",
"ENUM",
]),
identifier_sentinel: "IDENTIFIER",
first_set_identifier_tokens: HashSet::from(["SET"]),
first_set_sentinel: "\"{\"",
});
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseError {
pub(crate) err: OwnedRawParseError,
src: Arc<str>,
}
impl ParseError {
pub(crate) fn from_raw_parse_error(err: RawParseError<'_>, src: Arc<str>) -> Self {
Self {
err: err.map_token(|token| token.to_string()),
src,
}
}
pub(crate) fn from_raw_error_recovery(recovery: RawErrorRecovery<'_>, src: Arc<str>) -> Self {
Self::from_raw_parse_error(recovery.error, src)
}
}
impl ParseError {
pub fn primary_source_span(&self) -> Option<SourceSpan> {
match &self.err {
OwnedRawParseError::InvalidToken { location } => Some(SourceSpan::from(*location)),
OwnedRawParseError::UnrecognizedEof { location, .. } => {
Some(SourceSpan::from(*location))
}
OwnedRawParseError::UnrecognizedToken {
token: (token_start, _, token_end),
..
} => Some(SourceSpan::from(*token_start..*token_end)),
OwnedRawParseError::ExtraToken {
token: (token_start, _, token_end),
} => Some(SourceSpan::from(*token_start..*token_end)),
OwnedRawParseError::User { error } => error.primary_source_span(),
}
}
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { err, .. } = self;
match 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 std::error::Error for ParseError {}
impl Diagnostic for ParseError {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.src as &dyn miette::SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let primary_source_span = self.primary_source_span();
match &self.err {
OwnedRawParseError::InvalidToken { .. } => primary_source_span
.map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
OwnedRawParseError::UnrecognizedEof { expected, .. } => {
primary_source_span.map(|span| {
Box::new(std::iter::once(LabeledSpan::new_with_span(
expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
span,
))) as _
})
}
OwnedRawParseError::UnrecognizedToken { expected, .. } => {
primary_source_span.map(|span| {
Box::new(std::iter::once(LabeledSpan::new_with_span(
expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
span,
))) as _
})
}
OwnedRawParseError::ExtraToken { .. } => primary_source_span
.map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
OwnedRawParseError::User {
error: UserError::DuplicateAnnotations(_, n1, n2),
} => {
let spans: Vec<_> = [&n1.loc, &n2.loc]
.into_iter()
.filter_map(|opt_loc| opt_loc.as_ref())
.map(|loc| LabeledSpan::underline(loc.span))
.collect();
if spans.is_empty() {
None
} else {
Some(Box::new(spans.into_iter()))
}
}
OwnedRawParseError::User { .. } => primary_source_span
.map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseErrors(pub(crate) Box<NonEmpty<ParseError>>);
impl ParseErrors {
pub fn new(first: ParseError, tail: impl IntoIterator<Item = ParseError>) -> Self {
Self(Box::new(NonEmpty {
head: first,
tail: tail.into_iter().collect(),
}))
}
pub fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
let v = i.into_iter().collect::<Vec<_>>();
Some(Self(Box::new(NonEmpty::from_vec(v)?)))
}
pub fn iter(&self) -> impl Iterator<Item = &ParseError> {
self.0.iter()
}
}
impl Display for ParseErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.first())
}
}
impl IntoIterator for ParseErrors {
type Item = ParseError;
type IntoIter = Chain<Once<ParseError>, vec::IntoIter<ParseError>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl std::error::Error for ParseErrors {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(self.0.first())
}
}
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>> {
Diagnostic::code(self.0.first())
}
fn severity(&self) -> Option<miette::Severity> {
Diagnostic::severity(self.0.first())
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Diagnostic::help(self.0.first())
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Diagnostic::url(self.0.first())
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Diagnostic::source_code(self.0.first())
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
Diagnostic::labels(self.0.first())
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
Diagnostic::diagnostic_source(self.0.first())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToJsonSchemaErrors(NonEmpty<ToJsonSchemaError>);
impl ToJsonSchemaErrors {
pub fn new(errs: NonEmpty<ToJsonSchemaError>) -> Self {
Self(errs)
}
pub fn iter(&self) -> impl Iterator<Item = &ToJsonSchemaError> {
self.0.iter()
}
}
impl IntoIterator for ToJsonSchemaErrors {
type Item = ToJsonSchemaError;
type IntoIter = <NonEmpty<ToJsonSchemaError> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl From<ToJsonSchemaError> for ToJsonSchemaErrors {
fn from(value: ToJsonSchemaError) -> Self {
Self(NonEmpty::singleton(value))
}
}
impl Display for ToJsonSchemaErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.first()) }
}
impl std::error::Error for ToJsonSchemaErrors {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.first().source()
}
fn description(&self) -> &str {
#[expect(
deprecated,
reason = "description() is deprecated but we still want to forward it"
)]
self.0.first().description()
}
fn cause(&self) -> Option<&dyn std::error::Error> {
#[expect(
deprecated,
reason = "cause() is deprecated but we still want to forward it"
)]
self.0.first().cause()
}
}
impl Diagnostic for ToJsonSchemaErrors {
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.0.first().code()
}
fn severity(&self) -> Option<miette::Severity> {
self.0.first().severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.0.first().help()
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.0.first().url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.0.first().source_code()
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.0.first().labels()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.0.first().diagnostic_source()
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq, Diagnostic)]
pub enum ToJsonSchemaError {
#[error(transparent)]
#[diagnostic(transparent)]
DuplicateDeclarations(#[from] DuplicateDeclarations),
#[error(transparent)]
#[diagnostic(transparent)]
DuplicateContext(#[from] DuplicateContext),
#[error(transparent)]
#[diagnostic(transparent)]
DuplicatePrincipalOrResource(#[from] DuplicatePrincipalOrResource),
#[error(transparent)]
#[diagnostic(transparent)]
NoPrincipalOrResource(#[from] NoPrincipalOrResource),
#[error(transparent)]
#[diagnostic(transparent)]
DuplicateNamespaces(#[from] DuplicateNamespace),
#[error(transparent)]
#[diagnostic(transparent)]
UnknownTypeName(#[from] UnknownTypeName),
#[error(transparent)]
#[diagnostic(transparent)]
ReservedName(#[from] ReservedName),
#[error(transparent)]
#[diagnostic(transparent)]
ReservedSchemaKeyword(#[from] ReservedSchemaKeyword),
}
impl ToJsonSchemaError {
pub(crate) fn duplicate_context(
name: &impl ToSmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
) -> Self {
Self::DuplicateContext(DuplicateContext {
name: name.to_smolstr(),
loc1,
loc2,
})
}
pub(crate) fn duplicate_decls(
decl: &impl ToSmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
) -> Self {
Self::DuplicateDeclarations(DuplicateDeclarations {
decl: decl.to_smolstr(),
loc1,
loc2,
})
}
pub(crate) fn duplicate_namespace(
namespace_id: &impl ToSmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
) -> Self {
Self::DuplicateNamespaces(DuplicateNamespace {
namespace_id: namespace_id.to_smolstr(),
loc1,
loc2,
})
}
pub(crate) fn duplicate_principal(
name: &impl ToSmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
) -> Self {
Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
name: name.to_smolstr(),
kind: PR::Principal,
loc1,
loc2,
})
}
pub(crate) fn duplicate_resource(
name: &impl ToSmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
) -> Self {
Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
name: name.to_smolstr(),
kind: PR::Resource,
loc1,
loc2,
})
}
pub(crate) fn no_principal(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
Self::NoPrincipalOrResource(NoPrincipalOrResource {
kind: PR::Principal,
name: name.to_smolstr(),
missing_or_empty: MissingOrEmpty::Missing,
name_loc,
})
}
pub(crate) fn no_resource(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
Self::NoPrincipalOrResource(NoPrincipalOrResource {
kind: PR::Resource,
name: name.to_smolstr(),
missing_or_empty: MissingOrEmpty::Missing,
name_loc,
})
}
pub(crate) fn empty_principal(
name: &impl ToSmolStr,
name_loc: Option<Loc>,
loc: Option<Loc>,
) -> Self {
Self::NoPrincipalOrResource(NoPrincipalOrResource {
kind: PR::Principal,
name: name.to_smolstr(),
missing_or_empty: MissingOrEmpty::Empty { loc },
name_loc,
})
}
pub(crate) fn empty_resource(
name: &impl ToSmolStr,
name_loc: Option<Loc>,
loc: Option<Loc>,
) -> Self {
Self::NoPrincipalOrResource(NoPrincipalOrResource {
kind: PR::Resource,
name: name.to_smolstr(),
missing_or_empty: MissingOrEmpty::Empty { loc },
name_loc,
})
}
pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Option<Loc>) -> Self {
Self::ReservedName(ReservedName {
name: name.to_smolstr(),
loc,
})
}
pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Option<Loc>) -> Self {
Self::ReservedSchemaKeyword(ReservedSchemaKeyword {
keyword: keyword.to_smolstr(),
loc,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("this uses a reserved schema keyword: `{keyword}`")]
pub struct ReservedSchemaKeyword {
keyword: SmolStr,
loc: Option<Loc>,
}
impl Diagnostic for ReservedSchemaKeyword {
impl_diagnostic_from_source_loc_opt_field!(loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new("Keywords such as `entity`, `extension`, `set` and `record` cannot be used as common type names"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("use of the reserved `__cedar` namespace")]
pub struct ReservedName {
name: SmolStr,
loc: Option<Loc>,
}
impl Diagnostic for ReservedName {
impl_diagnostic_from_source_loc_opt_field!(loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(
"Names containing `__cedar` (for example: `__cedar::A`, `A::__cedar`, or `A::__cedar::B`) are reserved",
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("unknown type name: `{name}`")]
pub struct UnknownTypeName {
name: SmolStr,
loc: Option<Loc>,
}
impl Diagnostic for UnknownTypeName {
impl_diagnostic_from_source_loc_opt_field!(loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
let msg = format!(
"Did you mean to define `{}` as an entity type or common type?",
self.name
);
Some(Box::new(msg))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("duplicate `{kind}` declaration in action `{name}`")]
pub struct DuplicatePrincipalOrResource {
name: SmolStr,
kind: PR,
loc1: Option<Loc>,
loc2: Option<Loc>,
}
impl Diagnostic for DuplicatePrincipalOrResource {
impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
let msg = format!("Actions may only have a single {kind} declaration, but a {kind} declaration may specify a list of entity types like `{kind}: [X, Y, Z]`", kind=self.kind);
Some(Box::new(msg))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("duplicate context declaration in action `{name}`")]
pub struct DuplicateContext {
name: SmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
}
impl Diagnostic for DuplicateContext {
impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(
"Try either deleting one of the declarations, or merging into a single declaration",
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("`{decl}` is declared twice")]
pub struct DuplicateDeclarations {
decl: SmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
}
impl Diagnostic for DuplicateDeclarations {
impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{}", match .missing_or_empty {
MissingOrEmpty::Missing => format!("missing `{kind}` declaration for `{name}`"),
MissingOrEmpty::Empty { .. } => format!("for action `{name}`, `{kind}` is `[]`, which is invalid")
})]
pub struct NoPrincipalOrResource {
kind: PR,
name: SmolStr,
missing_or_empty: MissingOrEmpty,
name_loc: Option<Loc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MissingOrEmpty {
Missing,
Empty {
loc: Option<Loc>,
},
}
pub const NO_PR_HELP_MSG: &str =
"Every action must define both `principal` and `resource` targets, and the `principal` and `resource` lists must not be `[]`.";
impl Diagnostic for NoPrincipalOrResource {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.name_loc
.as_ref()
.map(|l| &l.src as &dyn miette::SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
match &self.missing_or_empty {
MissingOrEmpty::Missing => self.name_loc.as_ref().map(|loc| {
Box::new(std::iter::once(miette::LabeledSpan::underline(loc.span))) as _
}),
MissingOrEmpty::Empty { loc } => {
let action_name = self.name_loc.as_ref().map(|loc| {
miette::LabeledSpan::new_with_span(Some("for this action".into()), loc.span)
});
let decl = loc.as_ref().map(|loc| {
miette::LabeledSpan::new_with_span(Some("must not be `[]`".into()), loc.span)
});
let spans: Vec<_> = [action_name, decl].into_iter().flatten().collect();
if spans.is_empty() {
None
} else {
Some(Box::new(spans.into_iter()))
}
}
}
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(NO_PR_HELP_MSG))
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
#[error("duplicate namespace id: `{namespace_id}`")]
pub struct DuplicateNamespace {
namespace_id: SmolStr,
loc1: Option<Loc>,
loc2: Option<Loc>,
}
impl Diagnostic for DuplicateNamespace {
impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
}
pub mod schema_warnings {
use crate::parser::Loc;
use miette::Diagnostic;
use smol_str::SmolStr;
use thiserror::Error;
#[derive(Eq, PartialEq, Debug, Clone, Error)]
#[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")]
pub struct ShadowsBuiltinWarning {
pub(crate) name: SmolStr,
pub(crate) loc: Option<Loc>,
}
impl Diagnostic for ShadowsBuiltinWarning {
impl_diagnostic_from_source_loc_opt_field!(loc);
fn severity(&self) -> Option<miette::Severity> {
Some(miette::Severity::Warning)
}
}
#[derive(Eq, PartialEq, Debug, Clone, Error)]
#[error("The common type name {name} shadows an entity name")]
pub struct ShadowsEntityWarning {
pub(crate) name: SmolStr,
pub(crate) entity_loc: Option<Loc>,
pub(crate) common_loc: Option<Loc>,
}
impl Diagnostic for ShadowsEntityWarning {
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let spans: Vec<_> = [&self.entity_loc, &self.common_loc]
.into_iter()
.filter_map(|loc| loc.as_ref())
.map(miette::LabeledSpan::underline)
.collect();
if spans.is_empty() {
None
} else {
Some(Box::new(spans.into_iter()))
}
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.entity_loc
.as_ref()
.map(|loc| &loc.src as &dyn miette::SourceCode)
}
fn severity(&self) -> Option<miette::Severity> {
Some(miette::Severity::Warning)
}
}
}
#[derive(Eq, PartialEq, Debug, Clone, Error, Diagnostic)]
#[non_exhaustive]
pub enum SchemaWarning {
#[error(transparent)]
#[diagnostic(transparent)]
ShadowsBuiltin(#[from] schema_warnings::ShadowsBuiltinWarning),
#[error(transparent)]
#[diagnostic(transparent)]
ShadowsEntity(#[from] schema_warnings::ShadowsEntityWarning),
}