Skip to content

Commit d016d23

Browse files
Improve error message and add help text for LUB errors (#809)
Signed-off-by: John Kastner <[email protected]>
1 parent b842a2f commit d016d23

File tree

10 files changed

+279
-141
lines changed

10 files changed

+279
-141
lines changed

cedar-policy-core/src/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod cst_to_ast;
2424
pub mod err;
2525
/// implementations for formatting, like `Display`
2626
mod fmt;
27+
pub use fmt::join_with_conjunction;
2728
/// Source location struct
2829
mod loc;
2930
pub use loc::Loc;

cedar-policy-core/src/parser/fmt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ impl std::fmt::Display for Slot {
453453

454454
/// Format an iterator as a natural-language string, separating items with
455455
/// commas and a conjunction (e.g., "and", "or") between the last two items.
456-
pub(crate) fn join_with_conjunction<T, W: Write>(
456+
pub fn join_with_conjunction<T, W: Write>(
457457
f: &mut W,
458458
conjunction: &str,
459459
items: impl IntoIterator<Item = T>,

cedar-policy-validator/src/type_error.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use std::{collections::BTreeSet, fmt::Display};
2020

2121
use cedar_policy_core::ast::{CallStyle, EntityUID, Expr, ExprKind, Name, Var};
22-
use cedar_policy_core::parser::Loc;
22+
use cedar_policy_core::parser::{join_with_conjunction, Loc};
2323

2424
use crate::types::{EntityLUB, EntityRecordKind, RequestEnv};
2525

@@ -127,12 +127,19 @@ impl TypeError {
127127

128128
/// Construct a type error for when a least upper bound cannot be found for
129129
/// a collection of types.
130-
pub(crate) fn incompatible_types(on_expr: Expr, types: impl IntoIterator<Item = Type>) -> Self {
130+
pub(crate) fn incompatible_types(
131+
on_expr: Expr,
132+
types: impl IntoIterator<Item = Type>,
133+
hint: LubHelp,
134+
context: LubContext,
135+
) -> Self {
131136
Self {
132137
on_expr: Some(on_expr),
133138
source_loc: None,
134139
kind: TypeErrorKind::IncompatibleTypes(IncompatibleTypes {
135140
types: types.into_iter().collect::<BTreeSet<_>>(),
141+
hint,
142+
context,
136143
}),
137144
}
138145
}
@@ -241,7 +248,8 @@ pub enum TypeErrorKind {
241248
#[diagnostic(transparent)]
242249
UnexpectedType(UnexpectedType),
243250
/// The typechecker could not compute a least upper bound for `types`.
244-
#[error("unable to find upper bound for types: [{}]", .0.types.iter().join(","))]
251+
#[error(transparent)]
252+
#[diagnostic(transparent)]
245253
IncompatibleTypes(IncompatibleTypes),
246254
/// The typechecker detected an access to a record or entity attribute
247255
/// that it could not statically guarantee would be present.
@@ -331,9 +339,48 @@ pub(crate) enum UnexpectedTypeHelp {
331339
}
332340

333341
/// Structure containing details about an incompatible type error.
334-
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
342+
#[derive(Diagnostic, Error, Debug, Clone, Hash, Eq, PartialEq)]
343+
#[diagnostic(help("{context} must have compatible types. {hint}"))]
335344
pub struct IncompatibleTypes {
336345
pub(crate) types: BTreeSet<Type>,
346+
pub(crate) hint: LubHelp,
347+
pub(crate) context: LubContext,
348+
}
349+
350+
impl Display for IncompatibleTypes {
351+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352+
write!(f, "the types ")?;
353+
join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
354+
write!(f, " are not compatible")
355+
}
356+
}
357+
358+
#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
359+
pub(crate) enum LubHelp {
360+
#[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
361+
AttributeQualifier,
362+
#[error("Compatible record types must have exactly the same attributes")]
363+
RecordWidth,
364+
#[error("Different entity types are never compatible even when their attributes would be compatible")]
365+
EntityType,
366+
#[error("Entity and record types are never compatible even when their attributes would be compatible")]
367+
EntityRecord,
368+
#[error("Types must be exactly equal to be compatible")]
369+
None,
370+
}
371+
372+
#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
373+
pub(crate) enum LubContext {
374+
#[error("elements of a set")]
375+
Set,
376+
#[error("both branches of a conditional")]
377+
Conditional,
378+
#[error("both operands to a `==` expression")]
379+
Equality,
380+
#[error("elements of the first operand and the second operand to a `contains` expression")]
381+
Contains,
382+
#[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
383+
ContainsAnyAll,
337384
}
338385

339386
/// Structure containing details about a missing attribute error.

cedar-policy-validator/src/typecheck.rs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ use crate::{
4343
types::{
4444
AttributeType, Effect, EffectSet, EntityRecordKind, OpenTag, Primitive, RequestEnv, Type,
4545
},
46-
AttributeAccess, UnexpectedTypeHelp, ValidationMode, ValidationWarning, ValidationWarningKind,
46+
AttributeAccess, LubContext, UnexpectedTypeHelp, ValidationMode, ValidationWarning,
47+
ValidationWarningKind,
4748
};
4849

4950
use super::type_error::TypeError;
@@ -740,6 +741,7 @@ impl<'a> Typechecker<'a> {
740741
e,
741742
vec![typ_then.data().clone(), typ_else.data().clone()],
742743
type_errors,
744+
LubContext::Conditional,
743745
);
744746
let has_lub = lub_ty.is_some();
745747
let annot_expr = ExprBuilder::with_data(lub_ty)
@@ -1271,6 +1273,7 @@ impl<'a> Typechecker<'a> {
12711273
e,
12721274
elem_expr_types.iter().map(|ety| ety.data().clone()),
12731275
type_errors,
1276+
LubContext::Set,
12741277
);
12751278
match elem_lub {
12761279
_ if self.mode.is_strict() && exprs.is_empty() => {
@@ -1388,6 +1391,7 @@ impl<'a> Typechecker<'a> {
13881391
lhs_ty.data(),
13891392
rhs_ty.data(),
13901393
type_errors,
1394+
LubContext::Equality,
13911395
)
13921396
} else {
13931397
TypecheckAnswer::success(
@@ -1517,6 +1521,7 @@ impl<'a> Typechecker<'a> {
15171521
},
15181522
expr_ty_arg2.data(),
15191523
type_errors,
1524+
LubContext::Contains,
15201525
)
15211526
} else {
15221527
TypecheckAnswer::success(
@@ -1573,6 +1578,7 @@ impl<'a> Typechecker<'a> {
15731578
expr_ty_arg1.data(),
15741579
expr_ty_arg2.data(),
15751580
type_errors,
1581+
LubContext::ContainsAnyAll,
15761582
)
15771583
} else {
15781584
TypecheckAnswer::success(
@@ -1594,6 +1600,7 @@ impl<'a> Typechecker<'a> {
15941600
lhs_ty: &Option<Type>,
15951601
rhs_ty: &Option<Type>,
15961602
type_errors: &mut Vec<TypeError>,
1603+
context: LubContext,
15971604
) -> TypecheckAnswer<'b> {
15981605
match annotated_expr.data() {
15991606
Some(Type::False) => {
@@ -1603,20 +1610,25 @@ impl<'a> Typechecker<'a> {
16031610
TypecheckAnswer::success(ExprBuilder::with_data(Some(Type::True)).val(true))
16041611
}
16051612
_ => match (lhs_ty, rhs_ty) {
1606-
(Some(lhs_ty), Some(rhs_ty))
1607-
if Type::least_upper_bound(self.schema, lhs_ty, rhs_ty, self.mode)
1608-
.is_none() =>
1609-
{
1610-
type_errors.push(TypeError::incompatible_types(
1611-
unannotated_expr.clone(),
1612-
[lhs_ty.clone(), rhs_ty.clone()],
1613-
));
1614-
TypecheckAnswer::fail(annotated_expr)
1613+
(Some(lhs_ty), Some(rhs_ty)) => {
1614+
if let Err(lub_hint) =
1615+
Type::least_upper_bound(self.schema, lhs_ty, rhs_ty, self.mode)
1616+
{
1617+
type_errors.push(TypeError::incompatible_types(
1618+
unannotated_expr.clone(),
1619+
[lhs_ty.clone(), rhs_ty.clone()],
1620+
lub_hint,
1621+
context,
1622+
));
1623+
TypecheckAnswer::fail(annotated_expr)
1624+
} else {
1625+
// We had `Some` type for lhs and rhs and these types
1626+
// were compatible.
1627+
TypecheckAnswer::success(annotated_expr)
1628+
}
16151629
}
1616-
// Either we had `Some` type for lhs and rhs and these types
1617-
// were compatible, or we failed to a compute a type for either
1618-
// lhs or rhs, meaning we already failed typechecking for that
1619-
// expression.
1630+
// We failed to compute a type for either lhs or rhs, meaning
1631+
// we already failed typechecking for that expression.
16201632
_ => TypecheckAnswer::success(annotated_expr),
16211633
},
16221634
}
@@ -2383,6 +2395,7 @@ impl<'a> Typechecker<'a> {
23832395
expr: &Expr,
23842396
answers: impl IntoIterator<Item = Option<Type>>,
23852397
type_errors: &mut Vec<TypeError>,
2398+
context: LubContext,
23862399
) -> Option<Type> {
23872400
answers
23882401
.into_iter()
@@ -2393,17 +2406,22 @@ impl<'a> Typechecker<'a> {
23932406
.and_then(|typechecked_types| {
23942407
let lub =
23952408
Type::reduce_to_least_upper_bound(self.schema, &typechecked_types, self.mode);
2396-
if lub.is_none() {
2397-
// A type error is generated if we could not find a least
2398-
// upper bound for the types. The computed least upper bound
2399-
// will be None, so this function will correctly report this
2400-
// as a failure.
2401-
type_errors.push(TypeError::incompatible_types(
2402-
expr.clone(),
2403-
typechecked_types,
2404-
));
2409+
match lub {
2410+
Err(lub_hint) => {
2411+
// A type error is generated if we could not find a least
2412+
// upper bound for the types. The computed least upper bound
2413+
// will be None, so this function will correctly report this
2414+
// as a failure.
2415+
type_errors.push(TypeError::incompatible_types(
2416+
expr.clone(),
2417+
typechecked_types,
2418+
lub_hint,
2419+
context,
2420+
));
2421+
None
2422+
}
2423+
Ok(lub) => Some(lub),
24052424
}
2406-
lub
24072425
})
24082426
}
24092427

cedar-policy-validator/src/typecheck/test_expr.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use smol_str::SmolStr;
2727

2828
use crate::{
2929
type_error::TypeError, types::Type, AttributeAccess, AttributesOrContext, EntityType,
30-
NamespaceDefinition, SchemaFragment, UnexpectedTypeHelp, ValidationMode,
30+
LubContext, LubHelp, NamespaceDefinition, SchemaFragment, UnexpectedTypeHelp, ValidationMode,
3131
};
3232

3333
use super::test_utils::{
@@ -144,6 +144,8 @@ fn heterogeneous_set() {
144144
vec![TypeError::incompatible_types(
145145
set,
146146
vec![Type::singleton_boolean(true), Type::primitive_long()],
147+
LubHelp::None,
148+
LubContext::Set,
147149
)],
148150
);
149151
}
@@ -671,6 +673,8 @@ fn record_get_attr_lub_typecheck_fails() {
671673
)]),
672674
Type::primitive_long(),
673675
],
676+
LubHelp::None,
677+
LubContext::Conditional,
674678
)],
675679
);
676680
}
@@ -1025,6 +1029,8 @@ fn if_no_lub_error() {
10251029
vec![TypeError::incompatible_types(
10261030
if_expr,
10271031
vec![Type::primitive_long(), Type::primitive_string()],
1032+
LubHelp::None,
1033+
LubContext::Conditional,
10281034
)],
10291035
);
10301036
}
@@ -1038,6 +1044,8 @@ fn if_typecheck_fails() {
10381044
TypeError::incompatible_types(
10391045
if_expr,
10401046
vec![Type::primitive_long(), Type::primitive_string()],
1047+
LubHelp::None,
1048+
LubContext::Conditional,
10411049
),
10421050
TypeError::expected_type(
10431051
Expr::val("fail"),

cedar-policy-validator/src/typecheck/test_partial.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ mod fail_partial_schema {
653653
use std::str::FromStr;
654654

655655
use super::*;
656+
use crate::{LubContext, LubHelp};
656657

657658
#[test]
658659
fn error_on_declared_attr() {
@@ -694,6 +695,8 @@ mod fail_partial_schema {
694695
vec![TypeError::incompatible_types(
695696
Expr::from_str("if resource.foo then principal.age else (if resource.bar then principal.name else principal.unknown)").unwrap(),
696697
vec![Type::primitive_long(), Type::primitive_string()],
698+
LubHelp::None,
699+
LubContext::Conditional,
697700
)],
698701
);
699702
}

cedar-policy-validator/src/typecheck/test_policy.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ use crate::{
3838
type_error::TypeError,
3939
typecheck::{test_utils::static_to_template, PolicyCheck},
4040
types::{EntityLUB, Type},
41-
AttributeAccess, NamespaceDefinition, ValidationMode, ValidationWarningKind,
41+
AttributeAccess, LubContext, LubHelp, NamespaceDefinition, ValidationMode,
42+
ValidationWarningKind,
4243
};
4344

4445
fn simple_schema_file() -> NamespaceDefinition {
@@ -716,7 +717,9 @@ fn entity_record_lub_is_none() {
716717
[
717718
Type::closed_record_with_required_attributes([("name".into(), Type::primitive_string())]),
718719
Type::named_entity_reference_from_str("User"),
719-
]
720+
],
721+
LubHelp::EntityRecord,
722+
LubContext::Conditional,
720723
)
721724
],
722725
);
@@ -937,6 +940,8 @@ fn record_entity_lub_non_term() {
937940
)]),
938941
Type::named_entity_reference_from_str("U"),
939942
],
943+
LubHelp::EntityRecord,
944+
LubContext::Conditional,
940945
)],
941946
);
942947
}

0 commit comments

Comments
 (0)