157 lines
6.5 KiB
Rust
157 lines
6.5 KiB
Rust
use super::checked;
|
|
use dever::compiler::{check, model::Program};
|
|
use dever::syntax::{diagnostic::Diagnostic, source::Source};
|
|
|
|
pub(super) fn packages(files: &[(&str, &str)]) -> Result<Program, Diagnostic> {
|
|
let mut modules = Vec::new();
|
|
let mut identities = Vec::new();
|
|
for (index, (identity, text)) in files.iter().enumerate() {
|
|
let source = Source::new(index, format!("{identity}.dever"), (*text).into());
|
|
modules.push(dever::syntax::parse(&source)?);
|
|
identities.push((*identity).into());
|
|
}
|
|
check::check(modules, &identities)
|
|
}
|
|
|
|
#[test]
|
|
fn public_signatures_cannot_publish_private_types() {
|
|
for declaration in [
|
|
"public echo(value: Hidden) (answer: Hidden) { answer = value }",
|
|
"public echo(value: List<Hidden?>) (answer: List<Hidden?>) { answer = value }",
|
|
"public echo(value: Map<Text, List<Hidden>>) (answer: Map<Text, List<Hidden>>) { answer = value }",
|
|
"public echo(value: MapEntry<Text, Hidden>) (answer: MapEntry<Text, Hidden>) { answer = value }",
|
|
"public apply(action: handler(value: Hidden) (answer: Hidden)) () {}",
|
|
"public type Visible { nested: Hidden }",
|
|
"public type Visible { Wrapped(value: Hidden) }",
|
|
] {
|
|
let text = format!("type Hidden {{ value: Int }}\n{declaration}");
|
|
assert_eq!(checked(&text).unwrap_err().code, "C006", "{text}");
|
|
}
|
|
assert!(checked("public type Visible { value: Int }\npublic echo(value: List<Visible?>) (answer: List<Visible?>) { answer = value }").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn public_failures_cannot_leak_private_error_identity() {
|
|
for call in ["fail(Hidden.Failed(\"private\"))", "hidden()"] {
|
|
let text = format!(
|
|
"type Hidden {{ error Failed(message: Text) }}\nhidden() () {{ fail(Hidden.Failed(\"private\")) }}\npublic exposed() () {{ {call} }}"
|
|
);
|
|
let error = checked(&text).unwrap_err();
|
|
assert_eq!(error.code, "C006", "{error:?}");
|
|
}
|
|
assert!(checked("public type Problem { error Failed(message: Text) }\nprivateFailure() () { fail(Problem.Failed(\"public\")) }\npublic exposed() () { privateFailure() }").is_ok());
|
|
assert_eq!(
|
|
checked("public apply(action: handler(value: Map<Float, Int>) ()) () {}")
|
|
.unwrap_err()
|
|
.code,
|
|
"C005"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn package_cycles_are_rejected_without_rejecting_static_callbacks() {
|
|
for files in [
|
|
[
|
|
(
|
|
"left",
|
|
"public type A { value: Int }\npublic make() () { local: right.B? = null }",
|
|
),
|
|
(
|
|
"right",
|
|
"public type B { value: Int }\npublic make() () { local: left.A? = null }",
|
|
),
|
|
],
|
|
[
|
|
(
|
|
"left",
|
|
"public first() (answer: Int) { answer = right.second() }\npublic last() (answer: Int) { answer = 1 }",
|
|
),
|
|
(
|
|
"right",
|
|
"public second() (answer: Int) { answer = left.last() }",
|
|
),
|
|
],
|
|
[
|
|
(
|
|
"left",
|
|
"public type A { value: Int }\npublic fromRight(value: right.B) () {}",
|
|
),
|
|
(
|
|
"right",
|
|
"public type B { value: Int }\npublic fromLeft(value: left.A) () {}",
|
|
),
|
|
],
|
|
[
|
|
(
|
|
"left",
|
|
"public type A { value: Int }\npublic make() () { local = right.B { value = 1 } }",
|
|
),
|
|
(
|
|
"right",
|
|
"public type B { value: Int }\npublic make() () { local = left.A { value = 1 } }",
|
|
),
|
|
],
|
|
] {
|
|
assert_eq!(packages(&files).unwrap_err().code, "C007");
|
|
}
|
|
let files = [
|
|
(
|
|
"client",
|
|
"privateDouble(value: Int) (answer: Int) { answer = value * 2 }\nmain() (answer: Int) { answer = helper.apply(privateDouble, 4) }",
|
|
),
|
|
(
|
|
"helper",
|
|
"public apply(action: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = action(value) }",
|
|
),
|
|
];
|
|
assert!(packages(&files).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn cross_package_names_and_callbacks_obey_visibility() {
|
|
let helper = "type Hidden { value: Int }\nprivateRead(value: Int) (answer: Int) { answer = value }\npublic type Visible { value: Int }\npublic read(value: Int) (answer: Int) { answer = privateRead(value) }";
|
|
for caller in [
|
|
"main() (answer: Int) { answer = helper.privateRead(1) }",
|
|
"main() (answer: List<Int>) { answer = each(helper.privateRead, [1]) }",
|
|
"main(value: helper.Hidden) () {}",
|
|
"main() () { value = helper.Hidden { value = 1 } }",
|
|
] {
|
|
assert_eq!(
|
|
packages(&[("main", caller), ("helper", helper)])
|
|
.unwrap_err()
|
|
.code,
|
|
"C006"
|
|
);
|
|
}
|
|
assert!(packages(&[("main", "main() (answer: helper.Visible) { answer = helper.Visible { value = helper.read(3) } }"), ("helper", helper)]).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn private_fields_reject_foreign_reads_writes_and_construction() {
|
|
let vault = "public type Session { private key: Text\nlabel: Text }\npublic type Envelope { value: Session }";
|
|
for caller in [
|
|
"read(value: vault.Session) (answer: Text) { answer = value.key }",
|
|
"write(value: vault.Session) () { value.key = \"changed\" }",
|
|
"make() (answer: vault.Session) { answer = vault.Session { key = \"hidden\", label = \"name\" } }",
|
|
"make() (answer: vault.Session) { answer = vault.Session { label = \"name\" } }",
|
|
"read(value: vault.Envelope) (answer: Text) { answer = value.value.key }",
|
|
"write(value: vault.Envelope) () { value.value.key = \"changed\" }",
|
|
] {
|
|
let error = packages(&[("main", caller), ("vault", vault)]).unwrap_err();
|
|
assert_eq!(error.code, "C006", "{caller}: {error:?}");
|
|
assert!(error.message.contains("field is private"), "{error:?}");
|
|
}
|
|
assert!(packages(&[("main", "rename(value: vault.Session) (answer: vault.Session) { value.label = \"new\"\nanswer = value }"), ("vault", vault)]).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn private_record_fields_can_hide_types_but_choice_payloads_cannot() {
|
|
assert!(checked("type Hidden { value: Int }\npublic type Visible { private hidden: Hidden\nlabel: Text }\npublic make() (answer: Visible) { answer = Visible { hidden = Hidden { value = 7 }, label = \"name\" } }\npublic read(value: Visible) (answer: Int) { answer = value.hidden.value }").is_ok());
|
|
assert_eq!(
|
|
checked("public type Choice { Ready(private value: Int) }")
|
|
.unwrap_err()
|
|
.code,
|
|
"C005"
|
|
);
|
|
}
|