diff --git a/core/src/schema/idl/grammar.rs b/core/src/schema/idl/grammar.rs index aa753f2..a1259c1 100644 --- a/core/src/schema/idl/grammar.rs +++ b/core/src/schema/idl/grammar.rs @@ -735,10 +735,19 @@ pub mod grammar { _close: (), } - /// `Name(key = value, key = value)` - a named call with keyword arguments. + /// `Name(key = value, key = value)` - a named call with keyword + /// arguments - or a bare `Name`, the same as `Name()`. #[derive(Debug, Clone)] pub struct AnnotationCall { pub name: Identifier, + #[rust_sitter::repeat(non_empty = false)] + pub call: Option, + } + + /// The `(key = value, ...)` half of an [`AnnotationCall`], factored out + /// so the call can leave it off (same shape as `AnnotationAssignment`). + #[derive(Debug, Clone)] + pub struct AnnotationCallArgs { #[rust_sitter::leaf(text = "(")] _open: (), #[rust_sitter::delimited( @@ -1592,6 +1601,13 @@ pub mod grammar { } } + impl AnnotationCall { + /// The call's arguments - none for a bare `Name`. + pub fn args(&self) -> &[AnnotationArg] { + self.call.as_ref().map(|c| c.args.as_slice()).unwrap_or_default() + } + } + impl AnnotationValue { /// Render back to canonical text. Scalars round-trip as written; a /// list normalises to `[Name(a = 1, b = 2), ...]`. @@ -1611,7 +1627,7 @@ pub mod grammar { .iter() .map(|call| { let args: Vec = call - .args + .args() .iter() .map(|a| { format!("{} = {}", a.name.text, a.value.as_text()) diff --git a/core/src/schema/ir/compiler/interpreter/incremental.rs b/core/src/schema/ir/compiler/interpreter/incremental.rs index 58b7936..ddeebf1 100644 --- a/core/src/schema/ir/compiler/interpreter/incremental.rs +++ b/core/src/schema/ir/compiler/interpreter/incremental.rs @@ -544,7 +544,7 @@ fn annotation_units(annotations: &[&Annotation]) -> Vec { units.push(FrozenUnit::ValidatorRef { name: call.name.text.clone(), args: call - .args + .args() .iter() .map(|arg| FrozenUnit::Property { name: arg.name.text.clone(), diff --git a/core/src/schema/ir/validation/validator.rs b/core/src/schema/ir/validation/validator.rs index fba6ea7..9a0a46b 100644 --- a/core/src/schema/ir/validation/validator.rs +++ b/core/src/schema/ir/validation/validator.rs @@ -25,6 +25,8 @@ const DUPLICATE_VALIDATOR_ARGUMENT: DiagnosticCode = DiagnosticCode::error("validation::duplicate-validator-argument"); const UNKNOWN_VALIDATOR_ARGUMENT: DiagnosticCode = DiagnosticCode::error("validation::unknown-validator-argument"); +const MISSING_VALIDATOR_ARGUMENT: DiagnosticCode = + DiagnosticCode::error("validation::missing-validator-argument"); const VALIDATOR_ARGUMENT_TYPE_MISMATCH: DiagnosticCode = DiagnosticCode::error("validation::validator-argument-type-mismatch"); const NOT_A_VALIDATOR: DiagnosticCode = DiagnosticCode::error("validation::not-a-validator"); @@ -436,6 +438,21 @@ pub fn validate_with( } } } + + // Every property without a default has to be given + // - a bare `Name` gives none. + let fields = validator_fields.get(vname.as_str()).copied().unwrap_or_default(); + for field in fields { + if let FrozenUnit::Field { name: prop, default: None, .. } = field { + if !seen.contains(&prop.as_str()) { + errors.push(Diagnostic { code: MISSING_VALIDATOR_ARGUMENT, help: None, secondary: None, severity_override: None, + message: format!("validator '{}' is missing argument '{}'", vname, prop), + context: field_ctx.clone(), + span: Some(*span), + }); + } + } + } } } Some(other) if other != SymbolType::Import => { diff --git a/core/tests/schema/ir/alias_validators.rs b/core/tests/schema/ir/alias_validators.rs index f3e97cc..cb89617 100644 --- a/core/tests/schema/ir/alias_validators.rs +++ b/core/tests/schema/ir/alias_validators.rs @@ -163,3 +163,38 @@ fn a_stand_in_import_is_not_judged_against_a_validator() { ["validator 'Positive' applies to `u32`, not `Name`"] ); } + +#[test] +fn a_bare_validator_name_is_a_call_with_no_arguments() { + let units = IncrementalInterpreter::from_source(&format!( + "{BOUNDS}@validators = [Bounds, Bounds(max = 5)]\ntype AccountName = string\n" + )); + let parameters = units + .iter() + .find_map(|u| match u { + FrozenUnit::TypeAlias { name, parameters, .. } if name == "AccountName" => Some(parameters), + _ => None, + }) + .expect("the alias"); + assert_eq!( + *parameters, + [ + FrozenUnit::ValidatorRef { name: "Bounds".to_string(), args: vec![] }, + FrozenUnit::ValidatorRef { + name: "Bounds".to_string(), + args: vec![FrozenUnit::Property { name: "max".to_string(), value: ConfigValue::Integer(5) }], + }, + ] + ); + assert!(validate(&units).is_ok()); +} + +#[test] +fn a_bare_validator_name_still_needs_its_required_arguments() { + let code = "validator Range for u32 {\n min: u32\n validate {\n \ + assert(value >= params.min, \"{value.name}\")\n }\n}\n\ + @validators = [Range]\ntype Count = u32\n"; + assert_eq!(messages(code), ["validator 'Range' is missing argument 'min'"]); + // Written out, it's fine - and so is leaving out one with a default. + assert!(messages(&code.replace("[Range]", "[Range(min = 1)]")).is_empty()); +}