cedar_policy_core/parser/
err.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::collections::{BTreeSet, HashMap, HashSet};
18use std::fmt::{self, Display, Write};
19use std::iter;
20use std::ops::{Deref, DerefMut};
21use std::str::FromStr;
22use std::sync::{Arc, LazyLock};
23
24use either::Either;
25use lalrpop_util as lalr;
26use miette::{Diagnostic, LabeledSpan, SourceSpan};
27use nonempty::NonEmpty;
28use smol_str::SmolStr;
29use thiserror::Error;
30
31use crate::ast::{self, ReservedNameError};
32use crate::parser::fmt::join_with_conjunction;
33use crate::parser::node::Node;
34use crate::parser::unescape::UnescapeError;
35use crate::parser::Loc;
36
37use super::cst;
38
39pub(crate) type RawLocation = usize;
40pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;
41pub(crate) type RawUserError = Node<String>;
42
43pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, RawUserError>;
44pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, RawUserError>;
45
46type OwnedRawParseError = lalr::ParseError<RawLocation, String, RawUserError>;
47
48/// Errors that can occur when parsing Cedar policies or expressions.
49#[derive(Clone, Debug, Diagnostic, Error, PartialEq, Eq)]
50pub enum ParseError {
51    /// Error from the text -> CST parser
52    #[error(transparent)]
53    #[diagnostic(transparent)]
54    ToCST(#[from] ToCSTError),
55    /// Error from the CST -> AST transform
56    #[error(transparent)]
57    #[diagnostic(transparent)]
58    ToAST(#[from] ToASTError),
59}
60
61/// Errors possible from `Literal::from_str()`
62#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq)]
63pub enum LiteralParseError {
64    /// Failed to parse the input
65    #[error(transparent)]
66    #[diagnostic(transparent)]
67    Parse(#[from] ParseErrors),
68    /// Parsed successfully as an expression, but failed to construct a literal
69    #[error("invalid literal: {0}")]
70    InvalidLiteral(ast::Expr),
71}
72
73/// Error from the CST -> AST transform
74#[derive(Debug, Error, Clone, PartialEq, Eq)]
75#[error("{kind}")]
76pub struct ToASTError {
77    kind: ToASTErrorKind,
78    loc: Option<Loc>,
79}
80
81// Construct `labels` and `source_code` based on the `loc` in this
82// struct; and everything else forwarded directly to `kind`.
83impl Diagnostic for ToASTError {
84    impl_diagnostic_from_source_loc_opt_field!(loc);
85
86    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
87        self.kind.code()
88    }
89
90    fn severity(&self) -> Option<miette::Severity> {
91        self.kind.severity()
92    }
93
94    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
95        self.kind.help()
96    }
97
98    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
99        self.kind.url()
100    }
101
102    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
103        self.kind.diagnostic_source()
104    }
105}
106
107impl ToASTError {
108    /// Construct a new `ToASTError`.
109    pub fn new(kind: ToASTErrorKind, loc: Option<Loc>) -> Self {
110        Self { kind, loc }
111    }
112
113    /// Get the error kind.
114    pub fn kind(&self) -> &ToASTErrorKind {
115        &self.kind
116    }
117
118    pub(crate) fn source_loc(&self) -> Option<&Loc> {
119        self.loc.as_ref()
120    }
121}
122
123const POLICY_SCOPE_HELP: &str =
124    "policy scopes must contain a `principal`, `action`, and `resource` element in that order";
125
126/// Details about a particular kind of `ToASTError`.
127//
128// This is NOT a publicly exported error type.
129#[derive(Debug, Diagnostic, Error, Clone, PartialEq, Eq)]
130#[non_exhaustive]
131pub enum ToASTErrorKind {
132    /// Returned when we attempt to parse a template with a conflicting id
133    #[error("a template with id `{0}` already exists in the policy set")]
134    DuplicateTemplateId(ast::PolicyID),
135    /// Returned when we attempt to parse a policy with a conflicting id
136    #[error("a policy with id `{0}` already exists in the policy set")]
137    DuplicatePolicyId(ast::PolicyID),
138    /// Returned when a template is encountered but a static policy is expected
139    #[error(transparent)]
140    #[diagnostic(transparent)]
141    ExpectedStaticPolicy(#[from] parse_errors::ExpectedStaticPolicy),
142    /// Returned when a static policy is encountered but a template is expected
143    #[error(transparent)]
144    #[diagnostic(transparent)]
145    ExpectedTemplate(#[from] parse_errors::ExpectedTemplate),
146    /// Returned when we attempt to parse a policy or template with duplicate or
147    /// conflicting annotations
148    #[error("duplicate annotation: @{0}")]
149    DuplicateAnnotation(ast::AnyId),
150    /// Returned when a policy contains template slots in a when/unless clause.
151    /// This is not currently supported; see [RFC 3](https://github.com/cedar-policy/rfcs/pull/3).
152    #[error(transparent)]
153    #[diagnostic(transparent)]
154    SlotsInConditionClause(#[from] parse_errors::SlotsInConditionClause),
155    /// Returned when a policy is missing one of the three required scope elements
156    /// (`principal`, `action`, and `resource`)
157    #[error("this policy is missing the `{0}` variable in the scope")]
158    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
159    MissingScopeVariable(ast::Var),
160    /// Returned when a policy has an extra scope element
161    #[error("this policy has an extra element in the scope: {0}")]
162    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
163    ExtraScopeElement(Box<cst::VariableDef>),
164    /// Returned when a policy uses a reserved keyword as an identifier.
165    #[error("this identifier is reserved and cannot be used: {0}")]
166    ReservedIdentifier(cst::Ident),
167    /// Returned when a policy contains an invalid identifier.
168    /// This error is not currently returned, but is here for future-proofing;
169    /// see [`cst::Ident::Invalid`].
170    #[error("invalid identifier: {0}")]
171    InvalidIdentifier(String),
172    /// Returned when a policy uses '=' as a binary operator.
173    /// '=' is not an operator in Cedar; we can suggest '==' instead.
174    #[error("'=' is not a valid operator in Cedar")]
175    #[diagnostic(help("try using '==' instead"))]
176    InvalidSingleEq,
177    /// Returned when a policy uses an effect keyword beyond `permit` or `forbid`
178    #[error("invalid policy effect: {0}")]
179    #[diagnostic(help("effect must be either `permit` or `forbid`"))]
180    InvalidEffect(cst::Ident),
181    /// Returned when a policy uses a condition keyword beyond `when` or `unless`
182    #[error("invalid policy condition: {0}")]
183    #[diagnostic(help("condition must be either `when` or `unless`"))]
184    InvalidCondition(cst::Ident),
185    /// Returned when a policy uses a variable in the scope beyond `principal`,
186    /// `action`, or `resource`
187    #[error("found an invalid variable in the policy scope: {0}")]
188    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
189    InvalidScopeVariable(cst::Ident),
190    /// Returned when a policy scope clause contains the wrong variable.
191    /// (`principal` must be in the first clause, etc...)
192    #[error("found the variable `{got}` where the variable `{expected}` must be used")]
193    #[diagnostic(help("{POLICY_SCOPE_HELP}"))]
194    IncorrectVariable {
195        /// The variable that is expected
196        expected: ast::Var,
197        /// The variable that was present
198        got: ast::Var,
199    },
200    /// Returned when a policy scope uses an operator not allowed in scopes
201    #[error("invalid operator in the policy scope: {0}")]
202    #[diagnostic(help("policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"))]
203    InvalidScopeOperator(cst::RelOp),
204    /// Returned when an action scope uses an operator not allowed in action scopes
205    /// (special case of `InvalidScopeOperator`)
206    #[error("invalid operator in the action scope: {0}")]
207    #[diagnostic(help("action scope clauses can only use `==` or `in`"))]
208    InvalidActionScopeOperator(cst::RelOp),
209    /// Returned when the action scope clause contains an `is`
210    #[error("`is` cannot appear in the action scope")]
211    #[diagnostic(help("try moving `action is ..` into a `when` condition"))]
212    IsInActionScope,
213    /// Returned when an `is` operator is used together with `==`
214    #[error("`is` cannot be used together with `==`")]
215    #[diagnostic(help("try using `_ is _ in _`"))]
216    IsWithEq,
217    /// Returned when an entity uid used as an action does not have the type `Action`
218    #[error(transparent)]
219    #[diagnostic(transparent)]
220    InvalidActionType(#[from] parse_errors::InvalidActionType),
221    /// Returned when a condition clause is empty
222    #[error("{}condition clause cannot be empty", match .0 { Some(ident) => format!("`{ident}` "), None => "".to_string() })]
223    EmptyClause(Option<cst::Ident>),
224    /// Returned when membership chains do not resolve to an expression,
225    /// violating an internal invariant
226    #[error("internal invariant violated. Membership chain did not resolve to an expression")]
227    #[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
228    MembershipInvariantViolation,
229    /// Returned for a non-parse-able string literal
230    #[error("invalid string literal: {0}")]
231    InvalidString(String),
232    /// Returned when attempting to use an arbitrary variable name.
233    /// Cedar does not support arbitrary variables.
234    #[error("invalid variable: {0}")]
235    #[diagnostic(help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `{0}` in quotes to make a string?"))]
236    ArbitraryVariable(SmolStr),
237    /// Returned when attempting to use an invalid attribute name
238    #[error("invalid attribute name: {0}")]
239    #[diagnostic(help("attribute names can either be identifiers or string literals"))]
240    InvalidAttribute(SmolStr),
241    /// Returned when the RHS of a `has` operation is invalid
242    #[error("invalid RHS of a `has` operation: {0}")]
243    #[diagnostic(help("valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal"))]
244    InvalidHasRHS(SmolStr),
245    /// Returned when attempting to use an attribute with a namespace
246    #[error("`{0}` cannot be used as an attribute as it contains a namespace")]
247    PathAsAttribute(String),
248    /// Returned when a policy attempts to call a method function-style
249    #[error("`{0}` is a method, not a function")]
250    #[diagnostic(help("use a method-style call `e.{0}(..)`"))]
251    FunctionCallOnMethod(ast::UnreservedId),
252    /// Returned when a policy attempts to call a function in the method style
253    #[error("`{0}` is a function, not a method")]
254    #[diagnostic(help("use a function-style call `{0}(..)`"))]
255    MethodCallOnFunction(ast::UnreservedId),
256    /// Returned when the right hand side of a `like` expression is not a constant pattern literal
257    #[error("right hand side of a `like` expression must be a pattern literal, but got `{0}`")]
258    InvalidPattern(String),
259    /// Returned when the right hand side of a `is` expression is not an entity type name
260    #[error("right hand side of an `is` expression must be an entity type name, but got `{rhs}`")]
261    #[diagnostic(help("{}", invalid_is_help(lhs, rhs)))]
262    InvalidIsType {
263        /// LHS of the invalid `is` expression, as a string
264        lhs: String,
265        /// RHS of the invalid `is` expression, as a string
266        rhs: String,
267    },
268    /// Returned when an unexpected node is in the policy scope
269    #[error("expected {expected}, found {got}")]
270    WrongNode {
271        /// What the expected AST node kind was
272        expected: &'static str,
273        /// What AST node was present in the policy source
274        got: String,
275        /// Optional free-form text with a suggestion for how to fix the problem
276        #[help]
277        suggestion: Option<String>,
278    },
279    /// Returned when a policy contains ambiguous ordering of operators.
280    /// This can be resolved by using parenthesis to make order explicit
281    #[error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")]
282    AmbiguousOperators,
283    /// Returned when a policy uses the division operator (`/`), which is not supported
284    #[error("division is not supported")]
285    UnsupportedDivision,
286    /// Returned when a policy uses the remainder/modulo operator (`%`), which is not supported
287    #[error("remainder/modulo is not supported")]
288    UnsupportedModulo,
289    /// Any `ExpressionConstructionError` can also happen while converting CST to AST
290    #[error(transparent)]
291    #[diagnostic(transparent)]
292    ExpressionConstructionError(#[from] ast::ExpressionConstructionError),
293    /// Returned when a policy contains an integer literal that is out of range
294    #[error("integer literal `{0}` is too large")]
295    #[diagnostic(help("maximum allowed integer literal is `{}`", ast::InputInteger::MAX))]
296    IntegerLiteralTooLarge(u64),
297    /// Returned when a unary operator is chained more than 4 times in a row
298    #[error("too many occurrences of `{0}`")]
299    #[diagnostic(help("cannot chain more the 4 applications of a unary operator"))]
300    UnaryOpLimit(ast::UnaryOp),
301    /// Returned when a variable is called as a function, which is not allowed.
302    /// Functions are not first class values in Cedar
303    #[error("`{0}(...)` is not a valid function call")]
304    #[diagnostic(help("variables cannot be called as functions"))]
305    VariableCall(ast::Var),
306    /// Returned when a policy attempts to call a method on a value that has no methods
307    #[error("attempted to call `{0}.{1}(...)`, but `{0}` does not have any methods")]
308    NoMethods(ast::Name, ast::UnreservedId),
309    /// Returned when a policy attempts to call a method that does not exist
310    #[error("`{id}` is not a valid method")]
311    UnknownMethod {
312        /// The user-provided method id
313        id: ast::UnreservedId,
314        /// The hint to resolve the error
315        #[help]
316        hint: Option<String>,
317    },
318    /// Returned when a policy attempts to call a function that does not exist
319    #[error("`{id}` is not a valid function")]
320    UnknownFunction {
321        /// The user-provided function id
322        id: ast::Name,
323        /// The hint to resolve the error
324        #[help]
325        hint: Option<String>,
326    },
327    /// Returned when a policy attempts to write an entity literal
328    #[error("invalid entity literal: {0}")]
329    #[diagnostic(help("entity literals should have a form like `Namespace::User::\"alice\"`"))]
330    InvalidEntityLiteral(String),
331    /// Returned when an expression is the target of a function call.
332    /// Functions are not first class values in Cedar
333    #[error("function calls must be of the form `<name>(arg1, arg2, ...)`")]
334    ExpressionCall,
335    /// Returned when a policy attempts to access the fields of a value with no fields
336    #[error("invalid member access `{lhs}.{field}`, `{lhs}` has no fields or methods")]
337    InvalidAccess {
338        /// what we attempted to access a field of
339        lhs: ast::Name,
340        /// field we attempted to access
341        field: SmolStr,
342    },
343    /// Returned when a policy attempts to index on a fields of a value with no fields
344    #[error("invalid indexing expression `{lhs}[\"{}\"]`, `{lhs}` has no fields", .field.escape_debug())]
345    InvalidIndex {
346        /// what we attempted to access a field of
347        lhs: ast::Name,
348        /// field we attempted to access
349        field: SmolStr,
350    },
351    /// Returned when the contents of an indexing expression is not a string literal
352    #[error("the contents of an index expression must be a string literal")]
353    NonStringIndex,
354    /// Returned when a user attempts to use type-constraint `:` syntax. This
355    /// syntax was not adopted, but `is` can be used to write type constraints
356    /// in the policy scope.
357    #[error("type constraints using `:` are not supported")]
358    #[diagnostic(help("try using `is` instead"))]
359    TypeConstraints,
360    /// Returned when a string needs to be fully normalized
361    #[error("`{kind}` needs to be normalized (e.g., whitespace removed): {src}")]
362    #[diagnostic(help("the normalized form is `{normalized_src}`"))]
363    NonNormalizedString {
364        /// The kind of string we are expecting
365        kind: &'static str,
366        /// The source string passed in
367        src: String,
368        /// The normalized form of the string
369        normalized_src: String,
370    },
371    /// Returned when a CST node is empty during CST to AST/EST conversion.
372    /// This should have resulted in an error during the text to CST
373    /// conversion, which will terminate parsing. So it should be unreachable
374    /// in later stages.
375    #[error("internal invariant violated. Parsed data node should not be empty")]
376    #[diagnostic(help("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the text that failed to parse"))]
377    EmptyNodeInvariantViolation,
378    /// Returned when a function or method is called with the wrong arity
379    #[error("call to `{name}` requires exactly {expected} argument{}, but got {got} argument{}", if .expected == &1 { "" } else { "s" }, if .got == &1 { "" } else { "s" })]
380    WrongArity {
381        /// Name of the function or method being called
382        name: &'static str,
383        /// The expected number of arguments
384        expected: usize,
385        /// The number of arguments present in source
386        got: usize,
387    },
388    /// Returned when a string contains invalid escapes
389    #[error(transparent)]
390    #[diagnostic(transparent)]
391    Unescape(#[from] UnescapeError),
392    /// Returned when a policy scope has incorrect entity uids or template slots
393    #[error(transparent)]
394    #[diagnostic(transparent)]
395    WrongEntityArgument(#[from] parse_errors::WrongEntityArgument),
396    /// Returned when a policy contains a template slot other than `?principal` or `?resource`
397    #[error("`{0}` is not a valid template slot")]
398    #[diagnostic(help("a template slot may only be `?principal` or `?resource`"))]
399    InvalidSlot(SmolStr),
400    /// Returned when an entity type contains a reserved namespace or typename (as of this writing, just `__cedar`)
401    #[error(transparent)]
402    #[diagnostic(transparent)]
403    ReservedNamespace(#[from] ReservedNameError),
404    /// Returned when a policy uses `_ in _ is _` instead of `_ is _ in _` in the policy scope
405    #[error("when `is` and `in` are used together, `is` must come first")]
406    #[diagnostic(help("try `_ is _ in _`"))]
407    InvertedIsIn,
408    /// Represents an attempt to convert a CST Error node
409    #[cfg(feature = "tolerant-ast")]
410    #[error("Trying to convert CST error node")]
411    CSTErrorNode,
412    ///  Represents an attempt to convert a CST Error node
413    #[cfg(feature = "tolerant-ast")]
414    #[error("Trying to convert AST error node")]
415    ASTErrorNode,
416}
417
418fn invalid_is_help(lhs: &str, rhs: &str) -> String {
419    // in the specific case where rhs is double-quotes surrounding a valid
420    // (possibly reserved) identifier, give a different help message
421    match strip_surrounding_doublequotes(rhs).map(ast::Id::from_str) {
422        Some(Ok(stripped)) => format!("try removing the quotes: `{lhs} is {stripped}`"),
423        _ => format!("try using `==` to test for equality: `{lhs} == {rhs}`"),
424    }
425}
426
427/// If `s` has exactly `"` as both its first and last character, returns `Some`
428/// with the first and last character removed.
429/// In all other cases, returns `None`.
430fn strip_surrounding_doublequotes(s: &str) -> Option<&str> {
431    s.strip_prefix('"')?.strip_suffix('"')
432}
433
434impl ToASTErrorKind {
435    /// Constructor for the [`ToASTErrorKind::WrongNode`] error
436    pub fn wrong_node(
437        expected: &'static str,
438        got: impl Into<String>,
439        suggestion: Option<impl Into<String>>,
440    ) -> Self {
441        Self::WrongNode {
442            expected,
443            got: got.into(),
444            suggestion: suggestion.map(Into::into),
445        }
446    }
447
448    /// Constructor for the [`ToASTErrorKind::WrongArity`] error
449    pub fn wrong_arity(name: &'static str, expected: usize, got: usize) -> Self {
450        Self::WrongArity {
451            name,
452            expected,
453            got,
454        }
455    }
456
457    /// Constructor for the [`ToASTErrorKind::SlotsInConditionClause`] error
458    pub fn slots_in_condition_clause(slot: ast::Slot, clause_type: &'static str) -> Self {
459        parse_errors::SlotsInConditionClause { slot, clause_type }.into()
460    }
461
462    /// Constructor for the [`ToASTErrorKind::ExpectedStaticPolicy`] error
463    pub fn expected_static_policy(slot: ast::Slot) -> Self {
464        parse_errors::ExpectedStaticPolicy { slot }.into()
465    }
466
467    /// Constructor for the [`ToASTErrorKind::ExpectedTemplate`] error
468    pub fn expected_template() -> Self {
469        parse_errors::ExpectedTemplate::new().into()
470    }
471
472    /// Constructor for the [`ToASTErrorKind::WrongEntityArgument`] error when
473    /// one kind of entity argument was expected
474    pub fn wrong_entity_argument_one_expected(
475        expected: parse_errors::Ref,
476        got: parse_errors::Ref,
477    ) -> Self {
478        parse_errors::WrongEntityArgument {
479            expected: Either::Left(expected),
480            got,
481        }
482        .into()
483    }
484
485    /// Constructor for the [`ToASTErrorKind::WrongEntityArgument`] error when
486    /// one of two kinds of entity argument was expected
487    pub fn wrong_entity_argument_two_expected(
488        r1: parse_errors::Ref,
489        r2: parse_errors::Ref,
490        got: parse_errors::Ref,
491    ) -> Self {
492        let expected = Either::Right((r1, r2));
493        parse_errors::WrongEntityArgument { expected, got }.into()
494    }
495}
496
497/// Error subtypes for [`ToASTErrorKind`]
498pub mod parse_errors {
499
500    use std::sync::Arc;
501
502    use super::*;
503
504    /// Details about a `ExpectedStaticPolicy` error.
505    #[derive(Debug, Clone, Error, PartialEq, Eq)]
506    #[error("expected a static policy, got a template containing the slot {}", slot.id)]
507    pub struct ExpectedStaticPolicy {
508        /// Slot that was found (which is not valid in a static policy)
509        pub(crate) slot: ast::Slot,
510    }
511
512    impl Diagnostic for ExpectedStaticPolicy {
513        fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
514            Some(Box::new(
515                "try removing the template slot(s) from this policy",
516            ))
517        }
518
519        impl_diagnostic_from_source_loc_opt_field!(slot.loc);
520    }
521
522    impl From<ast::UnexpectedSlotError> for ExpectedStaticPolicy {
523        fn from(err: ast::UnexpectedSlotError) -> Self {
524            match err {
525                ast::UnexpectedSlotError::FoundSlot(slot) => Self { slot },
526            }
527        }
528    }
529
530    /// Details about a `ExpectedTemplate` error.
531    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
532    #[error("expected a template, got a static policy")]
533    #[diagnostic(help("a template should include slot(s) `?principal` or `?resource`"))]
534    pub struct ExpectedTemplate {
535        /// A private field, just so the public interface notes this as a
536        /// private-fields struct and not a empty-fields struct for semver
537        /// purposes (e.g., consumers cannot construct this type with
538        /// `ExpectedTemplate {}`)
539        _dummy: (),
540    }
541
542    impl ExpectedTemplate {
543        pub(crate) fn new() -> Self {
544            Self { _dummy: () }
545        }
546    }
547
548    /// Details about a `SlotsInConditionClause` error.
549    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
550    #[error("found template slot {} in a `{clause_type}` clause", slot.id)]
551    #[diagnostic(help("slots are currently unsupported in `{clause_type}` clauses"))]
552    pub struct SlotsInConditionClause {
553        /// Slot that was found in a when/unless clause
554        pub(crate) slot: ast::Slot,
555        /// Clause type, e.g. "when" or "unless"
556        pub(crate) clause_type: &'static str,
557    }
558
559    /// Details about an `InvalidActionType` error.
560    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
561    #[diagnostic(help("action entities must have type `Action`, optionally in a namespace"))]
562    pub struct InvalidActionType {
563        pub(crate) euids: NonEmpty<Arc<ast::EntityUID>>,
564    }
565
566    impl std::fmt::Display for InvalidActionType {
567        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568            let subject = if self.euids.len() > 1 {
569                "entity uids"
570            } else {
571                "an entity uid"
572            };
573            write!(f, "expected {subject} with type `Action` but got ")?;
574            join_with_conjunction(f, "and", self.euids.iter(), |f, e| write!(f, "`{e}`"))
575        }
576    }
577
578    /// Details about an `WrongEntityArgument` error.
579    #[derive(Debug, Clone, Diagnostic, Error, PartialEq, Eq)]
580    #[error("expected {}, found {got}", match .expected { Either::Left(r) => r.to_string(), Either::Right((r1, r2)) => format!("{r1} or {r2}") })]
581    pub struct WrongEntityArgument {
582        /// What kinds of references the given scope clause required.
583        /// Some scope clauses require exactly one kind of reference, some require one of two
584        pub(crate) expected: Either<Ref, (Ref, Ref)>,
585        /// The kind of reference that was present in the policy
586        pub(crate) got: Ref,
587    }
588
589    /// The 3 kinds of literals that can be in a policy scope
590    #[derive(Debug, Clone, PartialEq, Eq)]
591    pub enum Ref {
592        /// A single entity uids
593        Single,
594        /// A list of entity uids
595        Set,
596        /// A template slot
597        Template,
598    }
599
600    impl std::fmt::Display for Ref {
601        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602            match self {
603                Ref::Single => write!(f, "single entity uid"),
604                Ref::Template => write!(f, "template slot"),
605                Ref::Set => write!(f, "set of entity uids"),
606            }
607        }
608    }
609}
610
611/// Error from the text -> CST parser
612#[derive(Clone, Debug, Error, PartialEq, Eq)]
613pub struct ToCSTError {
614    err: OwnedRawParseError,
615    src: Arc<str>,
616}
617
618impl ToCSTError {
619    /// Extract a primary source span locating the error.
620    pub fn primary_source_span(&self) -> Option<SourceSpan> {
621        match &self.err {
622            OwnedRawParseError::InvalidToken { location } => Some(SourceSpan::from(*location)),
623            OwnedRawParseError::UnrecognizedEof { location, .. } => {
624                Some(SourceSpan::from(*location))
625            }
626            OwnedRawParseError::UnrecognizedToken {
627                token: (token_start, _, token_end),
628                ..
629            } => Some(SourceSpan::from(*token_start..*token_end)),
630            OwnedRawParseError::ExtraToken {
631                token: (token_start, _, token_end),
632            } => Some(SourceSpan::from(*token_start..*token_end)),
633            OwnedRawParseError::User { error } => error.loc.clone().map(|loc| loc.span),
634        }
635    }
636
637    pub(crate) fn from_raw_parse_err(err: RawParseError<'_>, src: Arc<str>) -> Self {
638        Self {
639            err: err.map_token(|token| token.to_string()),
640            src,
641        }
642    }
643
644    pub(crate) fn from_raw_err_recovery(recovery: RawErrorRecovery<'_>, src: Arc<str>) -> Self {
645        Self::from_raw_parse_err(recovery.error, src)
646    }
647}
648
649impl Display for ToCSTError {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        match &self.err {
652            OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
653            OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
654            OwnedRawParseError::UnrecognizedToken {
655                token: (_, token, _),
656                ..
657            } => write!(f, "unexpected token `{token}`"),
658            OwnedRawParseError::ExtraToken {
659                token: (_, token, _),
660                ..
661            } => write!(f, "extra token `{token}`"),
662            OwnedRawParseError::User { error } => write!(f, "{error}"),
663        }
664    }
665}
666
667impl Diagnostic for ToCSTError {
668    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
669        Some(&self.src as &dyn miette::SourceCode)
670    }
671
672    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
673        let span = self.primary_source_span()?;
674        let labeled_span = match &self.err {
675            OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(span),
676            OwnedRawParseError::UnrecognizedEof { expected, .. } => {
677                LabeledSpan::new_with_span(expected_to_string(expected, &POLICY_TOKEN_CONFIG), span)
678            }
679            OwnedRawParseError::UnrecognizedToken { expected, .. } => {
680                LabeledSpan::new_with_span(expected_to_string(expected, &POLICY_TOKEN_CONFIG), span)
681            }
682            OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(span),
683            OwnedRawParseError::User { .. } => LabeledSpan::underline(span),
684        };
685        Some(Box::new(iter::once(labeled_span)))
686    }
687}
688
689/// Defines configurable rules for how tokens in an `UnrecognizedToken` or
690/// `UnrecognizedEof` error should be displayed to users.
691#[derive(Debug)]
692pub struct ExpectedTokenConfig {
693    /// Defines user-friendly names for tokens used by our parser. Keys are the
694    /// names of tokens as defined in the `.lalrpop` grammar file. A token may
695    /// be omitted from this map if the name is already friendly enough.
696    pub friendly_token_names: HashMap<&'static str, &'static str>,
697
698    /// Some tokens defined in our grammar always cause later processing to fail.
699    /// Our policy grammar defines a token for the mod operator `%`, but we
700    /// reject any CST that uses the operator. To reduce confusion we filter
701    /// these from the list of expected tokens in an error message.
702    pub impossible_tokens: HashSet<&'static str>,
703
704    /// Both our policy and schema grammar have a generic identifier token
705    /// and some more specific identifier tokens that we use to parse specific
706    /// constructs. It is very often not useful to explicitly list out all of
707    /// these special identifier because the parser really just wants any
708    /// generic identifier. That it would accept these does not give any
709    /// useful information.
710    pub special_identifier_tokens: HashSet<&'static str>,
711
712    /// If this token is expected, then the parser expected a generic identifier, so
713    /// we omit the specific identifiers favor of saying we expect an "identifier".
714    pub identifier_sentinel: &'static str,
715
716    /// Special identifiers that may be worth displaying even if the parser
717    /// wants a generic identifier. These can tokens will be parsed as something
718    /// other than an identifier when they occur as the first token in an
719    /// expression (or a type, in the case of the schema grammar).
720    pub first_set_identifier_tokens: HashSet<&'static str>,
721
722    /// If this token is expected, then the parser was looking to start parsing
723    /// an expression (or type, in the schema). We know that we should report the
724    /// tokens that aren't parsed as identifiers at the start of an expression.
725    pub first_set_sentinel: &'static str,
726}
727
728static POLICY_TOKEN_CONFIG: LazyLock<ExpectedTokenConfig> = LazyLock::new(|| ExpectedTokenConfig {
729    friendly_token_names: HashMap::from([
730        ("TRUE", "`true`"),
731        ("FALSE", "`false`"),
732        ("IF", "`if`"),
733        ("PERMIT", "`permit`"),
734        ("FORBID", "`forbid`"),
735        ("WHEN", "`when`"),
736        ("UNLESS", "`unless`"),
737        ("IN", "`in`"),
738        ("HAS", "`has`"),
739        ("LIKE", "`like`"),
740        ("IS", "`is`"),
741        ("THEN", "`then`"),
742        ("ELSE", "`else`"),
743        ("PRINCIPAL", "`principal`"),
744        ("ACTION", "`action`"),
745        ("RESOURCE", "`resource`"),
746        ("CONTEXT", "`context`"),
747        ("PRINCIPAL_SLOT", "`?principal`"),
748        ("RESOURCE_SLOT", "`?resource`"),
749        ("IDENTIFIER", "identifier"),
750        ("NUMBER", "number"),
751        ("STRINGLIT", "string literal"),
752    ]),
753    impossible_tokens: HashSet::from(["\"=\"", "\"%\"", "\"/\"", "OTHER_SLOT"]),
754    special_identifier_tokens: HashSet::from([
755        "PERMIT",
756        "FORBID",
757        "WHEN",
758        "UNLESS",
759        "IN",
760        "HAS",
761        "LIKE",
762        "IS",
763        "THEN",
764        "ELSE",
765        "PRINCIPAL",
766        "ACTION",
767        "RESOURCE",
768        "CONTEXT",
769    ]),
770    identifier_sentinel: "IDENTIFIER",
771    first_set_identifier_tokens: HashSet::from(["TRUE", "FALSE", "IF"]),
772    first_set_sentinel: "\"!\"",
773});
774
775/// Format lalrpop expected error messages
776pub fn expected_to_string(expected: &[String], config: &ExpectedTokenConfig) -> Option<String> {
777    let mut expected = expected
778        .iter()
779        .filter(|e| !config.impossible_tokens.contains(e.as_str()))
780        .map(|e| e.as_str())
781        .collect::<BTreeSet<_>>();
782    if expected.contains(config.identifier_sentinel) {
783        for token in config.special_identifier_tokens.iter() {
784            expected.remove(*token);
785        }
786        if !expected.contains(config.first_set_sentinel) {
787            for token in config.first_set_identifier_tokens.iter() {
788                expected.remove(*token);
789            }
790        }
791    }
792    if expected.is_empty() {
793        return None;
794    }
795
796    let mut expected_string = "expected ".to_owned();
797    // PANIC SAFETY Shouldn't be `Err` since we're writing strings to a string
798    #[allow(clippy::expect_used)]
799    join_with_conjunction(
800        &mut expected_string,
801        "or",
802        expected,
803        |f, token| match config.friendly_token_names.get(token) {
804            Some(friendly_token_name) => write!(f, "{friendly_token_name}"),
805            None => write!(f, "{}", token.replace('"', "`")),
806        },
807    )
808    .expect("failed to format expected tokens");
809    Some(expected_string)
810}
811
812/// Represents one or more [`ParseError`]s encountered when parsing a policy or
813/// template.
814#[derive(Clone, Debug, PartialEq, Eq)]
815pub struct ParseErrors(NonEmpty<ParseError>);
816
817impl ParseErrors {
818    /// Construct a `ParseErrors` with a single element
819    pub(crate) fn singleton(err: impl Into<ParseError>) -> Self {
820        Self(NonEmpty::singleton(err.into()))
821    }
822
823    /// Construct a new `ParseErrors` with at least one element
824    pub(crate) fn new(first: ParseError, rest: impl IntoIterator<Item = ParseError>) -> Self {
825        Self(NonEmpty {
826            head: first,
827            tail: rest.into_iter().collect::<Vec<_>>(),
828        })
829    }
830
831    /// Construct a new `ParseErrors` from another `NonEmpty` type
832    pub(crate) fn new_from_nonempty(errs: NonEmpty<ParseError>) -> Self {
833        Self(errs)
834    }
835
836    pub(crate) fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
837        NonEmpty::collect(i).map(Self::new_from_nonempty)
838    }
839
840    /// Flatten a `Vec<ParseErrors>` into a single `ParseErrors`, returning
841    /// `None` if the input vector is empty.
842    pub(crate) fn flatten(errs: impl IntoIterator<Item = ParseErrors>) -> Option<Self> {
843        let mut errs = errs.into_iter();
844        let mut first = errs.next()?;
845        for inner in errs {
846            first.extend(inner);
847        }
848        Some(first)
849    }
850
851    /// If there are any `Err`s in the input, this function will return a
852    /// combined version of all errors. Otherwise, it will return a vector of
853    /// all the `Ok` values.
854    pub(crate) fn transpose<T>(
855        i: impl IntoIterator<Item = Result<T, ParseErrors>>,
856    ) -> Result<Vec<T>, Self> {
857        let iter = i.into_iter();
858        let (lower, upper) = iter.size_hint();
859        let capacity = upper.unwrap_or(lower);
860
861        let mut oks = Vec::with_capacity(capacity);
862        let mut errs = Vec::new();
863
864        for r in iter {
865            match r {
866                Ok(v) => oks.push(v),
867                Err(e) => errs.push(e),
868            }
869        }
870
871        if errs.is_empty() {
872            Ok(oks)
873        } else {
874            // PANIC SAFETY: `errs` is not empty so `flatten` will return `Some(..)`
875            #[allow(clippy::unwrap_used)]
876            Err(Self::flatten(errs).unwrap())
877        }
878    }
879}
880
881impl Display for ParseErrors {
882    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
883        write!(f, "{}", self.first()) // intentionally showing only the first error; see #326
884    }
885}
886
887impl std::error::Error for ParseErrors {
888    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
889        self.first().source()
890    }
891
892    #[allow(deprecated)]
893    fn description(&self) -> &str {
894        self.first().description()
895    }
896
897    #[allow(deprecated)]
898    fn cause(&self) -> Option<&dyn std::error::Error> {
899        self.first().cause()
900    }
901}
902
903// Except for `.related()`, everything else is forwarded to the first error.
904// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
905// information for the first error, even if they don't realize there are multiple errors here.
906// See #326.
907impl Diagnostic for ParseErrors {
908    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
909        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
910        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
911        errs.next().map(move |first_err| match first_err.related() {
912            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
913            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
914        })
915    }
916
917    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
918        self.first().code()
919    }
920
921    fn severity(&self) -> Option<miette::Severity> {
922        self.first().severity()
923    }
924
925    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
926        self.first().help()
927    }
928
929    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
930        self.first().url()
931    }
932
933    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
934        self.first().source_code()
935    }
936
937    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
938        self.first().labels()
939    }
940
941    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
942        self.first().diagnostic_source()
943    }
944}
945
946impl AsRef<NonEmpty<ParseError>> for ParseErrors {
947    fn as_ref(&self) -> &NonEmpty<ParseError> {
948        &self.0
949    }
950}
951
952impl AsMut<NonEmpty<ParseError>> for ParseErrors {
953    fn as_mut(&mut self) -> &mut NonEmpty<ParseError> {
954        &mut self.0
955    }
956}
957
958impl Deref for ParseErrors {
959    type Target = NonEmpty<ParseError>;
960
961    fn deref(&self) -> &Self::Target {
962        &self.0
963    }
964}
965
966impl DerefMut for ParseErrors {
967    fn deref_mut(&mut self) -> &mut Self::Target {
968        &mut self.0
969    }
970}
971
972impl<T: Into<ParseError>> From<T> for ParseErrors {
973    fn from(err: T) -> Self {
974        ParseErrors::singleton(err.into())
975    }
976}
977
978impl<T: Into<ParseError>> Extend<T> for ParseErrors {
979    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
980        self.0.extend(iter.into_iter().map(Into::into))
981    }
982}
983
984impl IntoIterator for ParseErrors {
985    type Item = ParseError;
986    type IntoIter = iter::Chain<iter::Once<Self::Item>, std::vec::IntoIter<Self::Item>>;
987
988    fn into_iter(self) -> Self::IntoIter {
989        self.0.into_iter()
990    }
991}
992
993impl<'a> IntoIterator for &'a ParseErrors {
994    type Item = &'a ParseError;
995    type IntoIter = iter::Chain<iter::Once<Self::Item>, std::slice::Iter<'a, ParseError>>;
996
997    fn into_iter(self) -> Self::IntoIter {
998        iter::once(&self.head).chain(self.tail.iter())
999    }
1000}