cedar_policy_validator/diagnostics/
validation_errors.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
17//! Defines errors returned by the validator.
18
19use miette::Diagnostic;
20use thiserror::Error;
21
22use std::fmt::Display;
23
24use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field;
25use cedar_policy_core::parser::Loc;
26
27use std::collections::BTreeSet;
28
29use cedar_policy_core::ast::{EntityType, EntityUID, Expr, ExprKind, PolicyID, Var};
30use cedar_policy_core::parser::join_with_conjunction;
31
32use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
33use itertools::Itertools;
34use smol_str::SmolStr;
35
36/// Structure containing details about an unrecognized entity type error.
37#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
38// #[error(error_in_policy!("unrecognized entity type `{actual_entity_type}`"))]
39#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
40pub struct UnrecognizedEntityType {
41    /// Source location
42    pub source_loc: Option<Loc>,
43    /// Policy ID where the error occurred
44    pub policy_id: PolicyID,
45    /// The entity type seen in the policy.
46    pub actual_entity_type: String,
47    /// An entity type from the schema that the user might reasonably have
48    /// intended to write.
49    pub suggested_entity_type: Option<String>,
50}
51
52impl Diagnostic for UnrecognizedEntityType {
53    impl_diagnostic_from_source_loc_opt_field!(source_loc);
54
55    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
56        match &self.suggested_entity_type {
57            Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
58            None => None,
59        }
60    }
61}
62
63/// Structure containing details about an unrecognized action id error.
64#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
65#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
66pub struct UnrecognizedActionId {
67    /// Source location
68    pub source_loc: Option<Loc>,
69    /// Policy ID where the error occurred
70    pub policy_id: PolicyID,
71    /// Action Id seen in the policy.
72    pub actual_action_id: String,
73    /// An action id from the schema that the user might reasonably have
74    /// intended to write.
75    pub suggested_action_id: Option<String>,
76}
77
78impl Diagnostic for UnrecognizedActionId {
79    impl_diagnostic_from_source_loc_opt_field!(source_loc);
80
81    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
82        match &self.suggested_action_id {
83            Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
84            None => None,
85        }
86    }
87}
88
89/// Structure containing details about an invalid action application error.
90#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
91#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
92pub struct InvalidActionApplication {
93    /// Source location
94    pub source_loc: Option<Loc>,
95    /// Policy ID where the error occurred
96    pub policy_id: PolicyID,
97    /// `true` if changing `==` to `in` wouuld fix the principal clause
98    pub would_in_fix_principal: bool,
99    /// `true` if changing `==` to `in` wouuld fix the resource clause
100    pub would_in_fix_resource: bool,
101}
102
103impl Diagnostic for InvalidActionApplication {
104    impl_diagnostic_from_source_loc_opt_field!(source_loc);
105
106    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
107        match (self.would_in_fix_principal, self.would_in_fix_resource) {
108            (true, false) => Some(Box::new(
109                "try replacing `==` with `in` in the principal clause",
110            )),
111            (false, true) => Some(Box::new(
112                "try replacing `==` with `in` in the resource clause",
113            )),
114            (true, true) => Some(Box::new(
115                "try replacing `==` with `in` in the principal clause and the resource clause",
116            )),
117            (false, false) => None,
118        }
119    }
120}
121
122/// Structure containing details about an unexpected type error.
123#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
124#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
125    match .expected.iter().next() {
126        Some(single) if .expected.len() == 1 => format!("{}", single),
127        _ => .expected.iter().join(", or ")
128    },
129    .actual)]
130pub struct UnexpectedType {
131    /// Source location
132    pub source_loc: Option<Loc>,
133    /// Policy ID where the error occurred
134    pub policy_id: PolicyID,
135    /// Type(s) which were expected
136    pub expected: BTreeSet<Type>,
137    /// Type which was encountered
138    pub actual: Type,
139    /// Optional help for resolving the error
140    pub help: Option<UnexpectedTypeHelp>,
141}
142
143impl Diagnostic for UnexpectedType {
144    impl_diagnostic_from_source_loc_opt_field!(source_loc);
145
146    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
147        self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
148    }
149}
150
151/// Help for resolving a type error
152#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
153pub enum UnexpectedTypeHelp {
154    /// Try using `like`
155    #[error("try using `like` to examine the contents of a string")]
156    TryUsingLike,
157    /// Try using `contains`, `containsAny`, or `containsAll`
158    #[error(
159        "try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
160    )]
161    TryUsingContains,
162    /// Try using `contains`
163    #[error("try using `contains` to test if a single element is in a set")]
164    TryUsingSingleContains,
165    /// Try using `has`
166    #[error("try using `has` to test for an attribute")]
167    TryUsingHas,
168    /// Try using `is`
169    #[error("try using `is` to test for an entity type")]
170    TryUsingIs,
171    /// Try using `in`
172    #[error("try using `in` for entity hierarchy membership")]
173    TryUsingIn,
174    /// Cedar doesn't support type tests
175    #[error("Cedar only supports run time type tests for entities")]
176    TypeTestNotSupported,
177    /// Cedar doesn't support string concatenation
178    #[error("Cedar does not support string concatenation")]
179    ConcatenationNotSupported,
180    /// Cedar doesn't support set union, intersection, or difference
181    #[error("Cedar does not support computing the union, intersection, or difference of sets")]
182    SetOperationsNotSupported,
183}
184
185/// Structure containing details about an incompatible type error.
186#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
187pub struct IncompatibleTypes {
188    /// Source location
189    pub source_loc: Option<Loc>,
190    /// Policy ID where the error occurred
191    pub policy_id: PolicyID,
192    /// Types which are incompatible
193    pub types: BTreeSet<Type>,
194    /// Hint for resolving the error
195    pub hint: LubHelp,
196    /// `LubContext` for the error
197    pub context: LubContext,
198}
199
200impl Diagnostic for IncompatibleTypes {
201    impl_diagnostic_from_source_loc_opt_field!(source_loc);
202
203    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
204        Some(Box::new(format!(
205            "for policy `{}`, {} must have compatible types. {}",
206            self.policy_id, self.context, self.hint
207        )))
208    }
209}
210
211impl Display for IncompatibleTypes {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        write!(f, "the types ")?;
214        join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
215        write!(f, " are not compatible")
216    }
217}
218
219/// Hints for resolving an incompatible-types error
220#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
221pub enum LubHelp {
222    /// Attribute qualifier problems
223    #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
224    AttributeQualifier,
225    /// Width subtyping
226    #[error("Compatible record types must have exactly the same attributes")]
227    RecordWidth,
228    /// Entities are nominally typed
229    #[error("Different entity types are never compatible even when their attributes would be compatible")]
230    EntityType,
231    /// Entity and record types are never compatible
232    #[error("Entity and record types are never compatible even when their attributes would be compatible")]
233    EntityRecord,
234    /// Catchall
235    #[error("Types must be exactly equal to be compatible")]
236    None,
237}
238
239/// Text describing where the incompatible-types error was found
240#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
241pub enum LubContext {
242    /// In the elements of a set
243    #[error("elements of a set")]
244    Set,
245    /// In the branches of a conditional
246    #[error("both branches of a conditional")]
247    Conditional,
248    /// In the operands to `==`
249    #[error("both operands to a `==` expression")]
250    Equality,
251    /// In the operands of `contains`
252    #[error("elements of the first operand and the second operand to a `contains` expression")]
253    Contains,
254    /// In the operand of `containsAny` or `containsAll`
255    #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
256    ContainsAnyAll,
257}
258
259/// Structure containing details about a missing attribute error.
260#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
261#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
262pub struct UnsafeAttributeAccess {
263    /// Source location
264    pub source_loc: Option<Loc>,
265    /// Policy ID where the error occurred
266    pub policy_id: PolicyID,
267    /// More details about the missing-attribute error
268    pub attribute_access: AttributeAccess,
269    /// Optional suggestion for resolving the error
270    pub suggestion: Option<String>,
271    /// When this is true, the attribute might still exist, but the validator
272    /// cannot guarantee that it will.
273    pub may_exist: bool,
274}
275
276impl Diagnostic for UnsafeAttributeAccess {
277    impl_diagnostic_from_source_loc_opt_field!(source_loc);
278
279    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
280        match (&self.suggestion, self.may_exist) {
281            (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
282            (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
283            (Some(suggestion), true) => Some(Box::new(format!("did you mean `{suggestion}`? (there may also be additional attributes that the validator is not able to reason about)"))),
284            (None, false) => None,
285        }
286    }
287}
288
289/// Structure containing details about an unsafe optional attribute error.
290#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
291#[error("unable to guarantee safety of access to optional attribute {attribute_access}")]
292pub struct UnsafeOptionalAttributeAccess {
293    /// Source location
294    pub source_loc: Option<Loc>,
295    /// Policy ID where the error occurred
296    pub policy_id: PolicyID,
297    /// More details about the attribute-access error
298    pub attribute_access: AttributeAccess,
299}
300
301impl Diagnostic for UnsafeOptionalAttributeAccess {
302    impl_diagnostic_from_source_loc_opt_field!(source_loc);
303
304    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
305        Some(Box::new(format!(
306            "try testing for the attribute with `{} && ..`",
307            self.attribute_access.suggested_has_guard()
308        )))
309    }
310}
311
312/// Structure containing details about an undefined function error.
313#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
314#[error("for policy `{policy_id}`, undefined extension function: {name}")]
315pub struct UndefinedFunction {
316    /// Source location
317    pub source_loc: Option<Loc>,
318    /// Policy ID where the error occurred
319    pub policy_id: PolicyID,
320    /// Name of the undefined function
321    pub name: String,
322}
323
324impl Diagnostic for UndefinedFunction {
325    impl_diagnostic_from_source_loc_opt_field!(source_loc);
326}
327
328/// Structure containing details about a wrong number of arguments error.
329#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
330#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
331pub struct WrongNumberArguments {
332    /// Source location
333    pub source_loc: Option<Loc>,
334    /// Policy ID where the error occurred
335    pub policy_id: PolicyID,
336    /// Expected number of arguments
337    pub expected: usize,
338    /// Actual number of arguments
339    pub actual: usize,
340}
341
342impl Diagnostic for WrongNumberArguments {
343    impl_diagnostic_from_source_loc_opt_field!(source_loc);
344}
345
346/// Structure containing details about a function argument validation error.
347#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
348#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
349pub struct FunctionArgumentValidation {
350    /// Source location
351    pub source_loc: Option<Loc>,
352    /// Policy ID where the error occurred
353    pub policy_id: PolicyID,
354    /// Error message
355    pub msg: String,
356}
357
358impl Diagnostic for FunctionArgumentValidation {
359    impl_diagnostic_from_source_loc_opt_field!(source_loc);
360}
361
362/// Structure containing details about a hierarchy not respected error
363#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
364#[error("for policy `{policy_id}`, operands to `in` do not respect the entity hierarchy")]
365pub struct HierarchyNotRespected {
366    /// Source location
367    pub source_loc: Option<Loc>,
368    /// Policy ID where the error occurred
369    pub policy_id: PolicyID,
370    /// LHS (descendant) of the hierarchy relationship
371    pub in_lhs: Option<EntityType>,
372    /// RHS (ancestor) of the hierarchy relationship
373    pub in_rhs: Option<EntityType>,
374}
375
376impl Diagnostic for HierarchyNotRespected {
377    impl_diagnostic_from_source_loc_opt_field!(source_loc);
378
379    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
380        match (&self.in_lhs, &self.in_rhs) {
381            (Some(in_lhs), Some(in_rhs)) => Some(Box::new(format!(
382                "`{in_lhs}` cannot be a descendant of `{in_rhs}`"
383            ))),
384            _ => None,
385        }
386    }
387}
388
389/// The policy uses an empty set literal in a way that is forbidden
390#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
391#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
392pub struct EmptySetForbidden {
393    /// Source location
394    pub source_loc: Option<Loc>,
395    /// Policy ID where the error occurred
396    pub policy_id: PolicyID,
397}
398
399impl Diagnostic for EmptySetForbidden {
400    impl_diagnostic_from_source_loc_opt_field!(source_loc);
401}
402
403/// The policy passes a non-literal to an extension constructor, which is
404/// forbidden in strict validation
405#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
406#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
407pub struct NonLitExtConstructor {
408    /// Source location
409    pub source_loc: Option<Loc>,
410    /// Policy ID where the error occurred
411    pub policy_id: PolicyID,
412}
413
414impl Diagnostic for NonLitExtConstructor {
415    impl_diagnostic_from_source_loc_opt_field!(source_loc);
416
417    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
418        Some(Box::new(
419            "consider applying extension constructors inside attribute values when constructing entity or context data"
420        ))
421    }
422}
423
424/// Contains more detailed information about an attribute access when it occurs
425/// on an entity type expression or on the `context` variable. Track a `Vec` of
426/// attributes rather than a single attribute so that on `principal.foo.bar` can
427/// report that the record attribute `foo` of an entity type (e.g., `User`)
428/// needs attributes `bar` instead of giving up when the immediate target of the
429/// attribute access is not a entity.
430#[derive(Debug, Clone, Hash, Eq, PartialEq)]
431pub enum AttributeAccess {
432    /// The attribute access is some sequence of attributes accesses eventually
433    /// targeting an [`EntityLUB`].
434    EntityLUB(EntityLUB, Vec<SmolStr>),
435    /// The attribute access is some sequence of attributes accesses eventually
436    /// targeting the `context` variable. The context being accessed is identified
437    /// by the [`EntityUID`] for the associated action.
438    Context(EntityUID, Vec<SmolStr>),
439    /// Other cases where we do not attempt to give more information about the
440    /// access. This includes any access on the `AnyEntity` type and on record
441    /// types other than the `context` variable.
442    Other(Vec<SmolStr>),
443}
444
445impl AttributeAccess {
446    /// Construct an `AttributeAccess` access from a `GetAttr` expression `expr.attr`.
447    pub(crate) fn from_expr(
448        req_env: &RequestEnv<'_>,
449        mut expr: &Expr<Option<Type>>,
450        attr: SmolStr,
451    ) -> AttributeAccess {
452        let mut attrs: Vec<SmolStr> = vec![attr];
453        loop {
454            if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
455                return AttributeAccess::EntityLUB(lub.clone(), attrs);
456            } else if let ExprKind::Var(Var::Context) = expr.expr_kind() {
457                return match req_env.action_entity_uid() {
458                    Some(action) => AttributeAccess::Context(action.clone(), attrs),
459                    None => AttributeAccess::Other(attrs),
460                };
461            } else if let ExprKind::GetAttr {
462                expr: sub_expr,
463                attr,
464            } = expr.expr_kind()
465            {
466                expr = sub_expr;
467                attrs.push(attr.clone());
468            } else {
469                return AttributeAccess::Other(attrs);
470            }
471        }
472    }
473
474    pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
475        match self {
476            AttributeAccess::EntityLUB(_, attrs) => attrs,
477            AttributeAccess::Context(_, attrs) => attrs,
478            AttributeAccess::Other(attrs) => attrs,
479        }
480    }
481
482    /// Construct a `has` expression that we can use to suggest a fix after an
483    /// unsafe optional attribute access.
484    pub(crate) fn suggested_has_guard(&self) -> String {
485        // We know if this is an access directly on `context`, so we can suggest
486        // specifically `context has ..`. Otherwise, we just use a generic `e`.
487        let base_expr = match self {
488            AttributeAccess::Context(_, _) => "context".into(),
489            _ => "e".into(),
490        };
491
492        let (safe_attrs, err_attr) = match self.attrs().split_first() {
493            Some((first, rest)) => (rest, first.clone()),
494            // We should always have a least one attribute stored, so this
495            // shouldn't be possible. If it does happen, just use a placeholder
496            // attribute name `f` since we'd rather avoid panicking.
497            None => (&[] as &[SmolStr], "f".into()),
498        };
499
500        let full_expr = std::iter::once(&base_expr)
501            .chain(safe_attrs.iter().rev())
502            .join(".");
503        format!("{full_expr} has {err_attr}")
504    }
505}
506
507impl Display for AttributeAccess {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        let attrs_str = self.attrs().iter().rev().join(".");
510        match self {
511            AttributeAccess::EntityLUB(lub, _) => write!(
512                f,
513                "`{attrs_str}` for entity type{}",
514                match lub.get_single_entity() {
515                    Some(single) => format!(" {}", single),
516                    _ => format!("s {}", lub.iter().join(", ")),
517                },
518            ),
519            AttributeAccess::Context(action, _) => {
520                write!(f, "`{attrs_str}` in context for {action}",)
521            }
522            AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
523        }
524    }
525}
526
527// These tests all assume that the typechecker found an error while checking the
528// outermost `GetAttr` in the expressions. If the attribute didn't exist at all,
529// only the primary message would included in the final error. If it was an
530// optional attribute without a guard, then the help message is also printed.
531#[cfg(test)]
532mod test_attr_access {
533    use cedar_policy_core::ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var};
534
535    use super::AttributeAccess;
536    use crate::types::{OpenTag, RequestEnv, Type};
537
538    // PANIC SAFETY: testing
539    #[allow(clippy::panic)]
540    #[track_caller]
541    fn assert_message_and_help(
542        attr_access: &Expr<Option<Type>>,
543        msg: impl AsRef<str>,
544        help: impl AsRef<str>,
545    ) {
546        let env = RequestEnv::DeclaredAction {
547            principal: &"Principal".parse().unwrap(),
548            action: &EntityUID::with_eid_and_type(
549                cedar_policy_core::ast::ACTION_ENTITY_TYPE,
550                "action",
551            )
552            .unwrap(),
553            resource: &"Resource".parse().unwrap(),
554            context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
555            principal_slot: None,
556            resource_slot: None,
557        };
558
559        let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
560            panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
561        };
562
563        let access = AttributeAccess::from_expr(&env, expr, attr.clone());
564        assert_eq!(
565            access.to_string().as_str(),
566            msg.as_ref(),
567            "Error message did not match expected"
568        );
569        assert_eq!(
570            access.suggested_has_guard().as_str(),
571            help.as_ref(),
572            "Suggested has guard did not match expected"
573        );
574    }
575
576    #[test]
577    fn context_access() {
578        // We have to build the Expr manually because the `EntityLUB` case
579        // requires type annotations, even though the other cases ignore them.
580        let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
581        assert_message_and_help(
582            &e,
583            "`foo` in context for Action::\"action\"",
584            "context has foo",
585        );
586        let e = ExprBuilder::new().get_attr(e, "bar".into());
587        assert_message_and_help(
588            &e,
589            "`foo.bar` in context for Action::\"action\"",
590            "context.foo has bar",
591        );
592        let e = ExprBuilder::new().get_attr(e, "baz".into());
593        assert_message_and_help(
594            &e,
595            "`foo.bar.baz` in context for Action::\"action\"",
596            "context.foo.bar has baz",
597        );
598    }
599
600    #[test]
601    fn entity_access() {
602        let e = ExprBuilder::new().get_attr(
603            ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
604                .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
605            "foo".into(),
606        );
607        assert_message_and_help(&e, "`foo` for entity type User", "e has foo");
608        let e = ExprBuilder::new().get_attr(e, "bar".into());
609        assert_message_and_help(&e, "`foo.bar` for entity type User", "e.foo has bar");
610        let e = ExprBuilder::new().get_attr(e, "baz".into());
611        assert_message_and_help(
612            &e,
613            "`foo.bar.baz` for entity type User",
614            "e.foo.bar has baz",
615        );
616    }
617
618    #[test]
619    fn entity_type_attr_access() {
620        let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
621            .get_attr(
622                ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
623                    .var(Var::Principal),
624                "thing".into(),
625            );
626        assert_message_and_help(&e, "`thing` for entity type User", "e has thing");
627        let e = ExprBuilder::new().get_attr(e, "bar".into());
628        assert_message_and_help(&e, "`bar` for entity type Thing", "e has bar");
629        let e = ExprBuilder::new().get_attr(e, "baz".into());
630        assert_message_and_help(&e, "`bar.baz` for entity type Thing", "e.bar has baz");
631    }
632
633    #[test]
634    fn other_access() {
635        let e = ExprBuilder::new().get_attr(
636            ExprBuilder::new().ite(
637                ExprBuilder::new().val(true),
638                ExprBuilder::new().record([]).unwrap(),
639                ExprBuilder::new().record([]).unwrap(),
640            ),
641            "foo".into(),
642        );
643        assert_message_and_help(&e, "`foo`", "e has foo");
644        let e = ExprBuilder::new().get_attr(e, "bar".into());
645        assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
646        let e = ExprBuilder::new().get_attr(e, "baz".into());
647        assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
648    }
649}