1use super::{
58 CommandBody, CommandIndex, CommandNode, Constraint, Globals, OptionShape, OptionSpec,
59 Positional, Ref, Tool, ToolMiddleware, ToolMiddlewareScope,
60};
61use crate::schema::graph::{RefResolutionError, SchemaGraph};
62use crate::schema::metadata::TypeId;
63use crate::schema::schema_type::SchemaType;
64use crate::schema::schema_value::SchemaValue;
65use crate::schema::validation::value::{ValueError, validate_value};
66use crate::schema::validation::well_formedness::{SchemaError, validate_graph, validate_root_type};
67use std::collections::HashSet;
68use std::fmt::{self, Display, Formatter};
69
70pub fn is_valid_identifier(s: &str) -> bool {
72 s.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
73 && s.split('-').all(|part| {
74 !part.is_empty()
75 && part
76 .bytes()
77 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
78 })
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum ToolValidationError {
84 EmptyVersion,
86 EmptyCommandTree,
88 CommandIndexOutOfBounds { index: i32, len: usize },
90 InvalidIdentifier { kind: &'static str, value: String },
92 DuplicateSubcommandName { parent: String, name: String },
94 DuplicateName { command: String, name: String },
97 DuplicateShort { command: String, short: char },
99 UnresolvedConstraintRef { command: String, name: String },
101 ValueIsTypeMismatch { command: String, name: String },
104 DefaultTypeMismatch { command: String, name: String },
108 RepeatableMapTypeNotMap { command: String, name: String },
111 UnresolvedTypeRef {
114 command: String,
115 position: String,
116 id: String,
117 },
118 IllFormedSchema {
122 command: String,
123 position: String,
124 detail: String,
125 },
126 DuplicateTypeId { id: String },
129 UnresolvedDefaultFormatter { command: String, formatter: String },
131 VerbatimWithoutSeparator { command: String, positional: String },
133 VariantInInputPosition { command: String, position: String },
136 UnreachableCommandNode { index: i32 },
138 CommandTreeCycle { index: i32 },
140 DuplicateCommandParent { index: i32 },
143}
144
145impl Display for ToolValidationError {
146 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
147 match self {
148 ToolValidationError::EmptyVersion => write!(f, "the tool version is empty"),
149 ToolValidationError::EmptyCommandTree => {
150 write!(f, "the command tree is empty")
151 }
152 ToolValidationError::CommandIndexOutOfBounds { index, len } => {
153 write!(
154 f,
155 "command index {index} is out of bounds (tree has {len} nodes)"
156 )
157 }
158 ToolValidationError::InvalidIdentifier { kind, value } => {
159 write!(f, "invalid {kind}: {value:?}")
160 }
161 ToolValidationError::DuplicateSubcommandName { parent, name } => {
162 write!(
163 f,
164 "duplicate subcommand name {name:?} under command {parent:?}"
165 )
166 }
167 ToolValidationError::DuplicateName { command, name } => {
168 write!(f, "duplicate name {name:?} in command {command:?}")
169 }
170 ToolValidationError::DuplicateShort { command, short } => {
171 write!(f, "duplicate short form {short:?} in command {command:?}")
172 }
173 ToolValidationError::UnresolvedConstraintRef { command, name } => {
174 write!(
175 f,
176 "constraint references unknown name {name:?} in command {command:?}"
177 )
178 }
179 ToolValidationError::ValueIsTypeMismatch { command, name } => {
180 write!(
181 f,
182 "value-is literal for {name:?} is not valid for its type in command {command:?}"
183 )
184 }
185 ToolValidationError::DefaultTypeMismatch { command, name } => {
186 write!(
187 f,
188 "default literal for {name:?} is not valid for its type in command {command:?}"
189 )
190 }
191 ToolValidationError::RepeatableMapTypeNotMap { command, name } => {
192 write!(
193 f,
194 "repeatable-map option {name:?} in command {command:?} does not collect into a map type"
195 )
196 }
197 ToolValidationError::UnresolvedTypeRef {
198 command,
199 position,
200 id,
201 } => {
202 write!(
203 f,
204 "type reference {id:?} at position {position:?} in command {command:?} does not resolve to a definition in the tool schema"
205 )
206 }
207 ToolValidationError::IllFormedSchema {
208 command,
209 position,
210 detail,
211 } => {
212 write!(
213 f,
214 "type at position {position:?} in command {command:?} is not well-formed: {detail}"
215 )
216 }
217 ToolValidationError::DuplicateTypeId { id } => {
218 write!(f, "duplicate schema definition id {id:?}")
219 }
220 ToolValidationError::UnresolvedDefaultFormatter { command, formatter } => {
221 write!(
222 f,
223 "default-formatter {formatter:?} is not declared in command {command:?}"
224 )
225 }
226 ToolValidationError::VerbatimWithoutSeparator {
227 command,
228 positional,
229 } => {
230 write!(
231 f,
232 "tail positional {positional:?} in command {command:?} is verbatim but has no separator"
233 )
234 }
235 ToolValidationError::VariantInInputPosition { command, position } => {
236 write!(
237 f,
238 "a variant type is reachable from input position {position:?} in command {command:?}"
239 )
240 }
241 ToolValidationError::UnreachableCommandNode { index } => {
242 write!(f, "command node {index} is not reachable from the root")
243 }
244 ToolValidationError::CommandTreeCycle { index } => {
245 write!(f, "the command tree contains a cycle at node {index}")
246 }
247 ToolValidationError::DuplicateCommandParent { index } => {
248 write!(f, "command node {index} has more than one parent")
249 }
250 }
251 }
252}
253
254impl std::error::Error for ToolValidationError {}
255
256pub fn validate_tool(tool: &Tool) -> Result<(), Vec<ToolValidationError>> {
258 let mut ctx = Validator::new(tool);
259 if tool.version.trim().is_empty() {
260 ctx.errors.push(ToolValidationError::EmptyVersion);
261 }
262 ctx.run();
263 if ctx.errors.is_empty() {
264 Ok(())
265 } else {
266 Err(ctx.errors)
267 }
268}
269
270#[derive(Clone, Debug, PartialEq, Eq)]
272pub enum ToolMiddlewareValidationError {
273 EmptyVersion,
274 InvalidIdentifier { kind: &'static str, value: String },
275 DuplicateIdentity { value: String },
276 InvalidParameterSchema(Vec<SchemaError>),
277 ForbiddenParameterType(&'static str),
278 InvalidPresentedTool(Vec<ToolValidationError>),
279 InvalidExpectedTool(Vec<ToolValidationError>),
280}
281
282impl Display for ToolMiddlewareValidationError {
283 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
284 match self {
285 Self::EmptyVersion => write!(f, "the tool middleware version is empty"),
286 Self::InvalidIdentifier { kind, value } => write!(f, "invalid {kind}: {value:?}"),
287 Self::DuplicateIdentity { value } => {
288 write!(f, "duplicate tool middleware name or alias: {value:?}")
289 }
290 Self::InvalidParameterSchema(errors) => {
291 write!(f, "invalid tool middleware parameter schema: {errors:?}")
292 }
293 Self::ForbiddenParameterType(kind) => {
294 write!(
295 f,
296 "tool middleware parameter schema contains forbidden type {kind}"
297 )
298 }
299 Self::InvalidPresentedTool(errors) => {
300 write!(f, "invalid presented tool descriptor: {errors:?}")
301 }
302 Self::InvalidExpectedTool(errors) => {
303 write!(f, "invalid expected tool descriptor: {errors:?}")
304 }
305 }
306 }
307}
308
309impl std::error::Error for ToolMiddlewareValidationError {}
310
311pub fn validate_tool_middleware(
313 middleware: &ToolMiddleware,
314) -> Result<(), Vec<ToolMiddlewareValidationError>> {
315 let mut errors = Vec::new();
316 if middleware.version.trim().is_empty() {
317 errors.push(ToolMiddlewareValidationError::EmptyVersion);
318 }
319 let mut identities = HashSet::new();
320 for (kind, identity) in std::iter::once(("tool middleware name", &middleware.name)).chain(
321 middleware
322 .aliases
323 .iter()
324 .map(|alias| ("tool middleware alias", alias)),
325 ) {
326 if !is_valid_identifier(identity) {
327 errors.push(ToolMiddlewareValidationError::InvalidIdentifier {
328 kind,
329 value: identity.clone(),
330 });
331 }
332 if !identities.insert(identity) {
333 errors.push(ToolMiddlewareValidationError::DuplicateIdentity {
334 value: identity.clone(),
335 });
336 }
337 }
338
339 if let Err(schema_errors) = validate_graph(&middleware.parameter_schema) {
340 errors.push(ToolMiddlewareValidationError::InvalidParameterSchema(
341 schema_errors,
342 ));
343 }
344 for ty in std::iter::once(&middleware.parameter_schema.root)
345 .chain(middleware.parameter_schema.defs.iter().map(|def| &def.body))
346 {
347 collect_forbidden_parameter_types(ty, &mut errors);
348 }
349
350 if let ToolMiddlewareScope::Monomorphic(scope) = &middleware.scope {
351 if let Err(tool_errors) = validate_tool(&scope.presented) {
352 errors.push(ToolMiddlewareValidationError::InvalidPresentedTool(
353 tool_errors,
354 ));
355 }
356 if let Some(expected) = &scope.expected
357 && let Err(tool_errors) = validate_tool(expected)
358 {
359 errors.push(ToolMiddlewareValidationError::InvalidExpectedTool(
360 tool_errors,
361 ));
362 }
363 }
364
365 if errors.is_empty() {
366 Ok(())
367 } else {
368 Err(errors)
369 }
370}
371
372fn collect_forbidden_parameter_types(
373 ty: &SchemaType,
374 errors: &mut Vec<ToolMiddlewareValidationError>,
375) {
376 match ty {
377 SchemaType::Secret { .. } => errors.push(
378 ToolMiddlewareValidationError::ForbiddenParameterType("secret"),
379 ),
380 SchemaType::QuotaToken { .. } => errors.push(
381 ToolMiddlewareValidationError::ForbiddenParameterType("quota-token"),
382 ),
383 SchemaType::PermissionCard { .. } => errors.push(
384 ToolMiddlewareValidationError::ForbiddenParameterType("permission-card"),
385 ),
386 SchemaType::Future { .. } => errors.push(
387 ToolMiddlewareValidationError::ForbiddenParameterType("future"),
388 ),
389 SchemaType::Stream { .. } => errors.push(
390 ToolMiddlewareValidationError::ForbiddenParameterType("stream"),
391 ),
392 SchemaType::Record { fields, .. } => fields
393 .iter()
394 .for_each(|field| collect_forbidden_parameter_types(&field.body, errors)),
395 SchemaType::Variant { cases, .. } => cases.iter().for_each(|case| {
396 if let Some(payload) = &case.payload {
397 collect_forbidden_parameter_types(payload, errors);
398 }
399 }),
400 SchemaType::Tuple { elements, .. } => elements
401 .iter()
402 .for_each(|element| collect_forbidden_parameter_types(element, errors)),
403 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
404 collect_forbidden_parameter_types(element, errors)
405 }
406 SchemaType::Map { key, value, .. } => {
407 collect_forbidden_parameter_types(key, errors);
408 collect_forbidden_parameter_types(value, errors);
409 }
410 SchemaType::Option { inner, .. } => collect_forbidden_parameter_types(inner, errors),
411 SchemaType::Result { spec, .. } => {
412 if let Some(ok) = &spec.ok {
413 collect_forbidden_parameter_types(ok, errors);
414 }
415 if let Some(err) = &spec.err {
416 collect_forbidden_parameter_types(err, errors);
417 }
418 }
419 SchemaType::Union { spec, .. } => spec
420 .branches
421 .iter()
422 .for_each(|branch| collect_forbidden_parameter_types(&branch.body, errors)),
423 SchemaType::Ref { .. }
424 | SchemaType::Bool { .. }
425 | SchemaType::S8 { .. }
426 | SchemaType::S16 { .. }
427 | SchemaType::S32 { .. }
428 | SchemaType::S64 { .. }
429 | SchemaType::U8 { .. }
430 | SchemaType::U16 { .. }
431 | SchemaType::U32 { .. }
432 | SchemaType::U64 { .. }
433 | SchemaType::F32 { .. }
434 | SchemaType::F64 { .. }
435 | SchemaType::Char { .. }
436 | SchemaType::String { .. }
437 | SchemaType::Enum { .. }
438 | SchemaType::Flags { .. }
439 | SchemaType::Text { .. }
440 | SchemaType::Binary { .. }
441 | SchemaType::Path { .. }
442 | SchemaType::Url { .. }
443 | SchemaType::Uuid { .. }
444 | SchemaType::Datetime { .. }
445 | SchemaType::Duration { .. }
446 | SchemaType::Quantity { .. } => {}
447 }
448}
449
450struct InputRoot<'a> {
452 command: String,
453 position: String,
454 ty: &'a SchemaType,
455}
456
457struct Validator<'a> {
458 tool: &'a Tool,
459 errors: Vec<ToolValidationError>,
460 input_roots: Vec<InputRoot<'a>>,
463 used_ref_ids: HashSet<TypeId>,
470}
471
472#[derive(Clone, Copy, PartialEq, Eq)]
476enum ValueIsMode {
477 WholeOrOnePeel,
482 Exact,
487}
488
489#[derive(Clone, Copy)]
493enum ValueComparand<'a> {
494 Type(&'a SchemaType, ValueIsMode),
497 BlockedByTypeError,
502}
503
504enum ValueIsOutcome {
506 Compatible,
509 Mismatch,
512 BlockedByDanglingRef,
516}
517
518#[derive(Default)]
521struct NameScope<'a> {
522 names: HashSet<String>,
525 typed: std::collections::HashMap<String, ValueComparand<'a>>,
530}
531
532impl<'a> Validator<'a> {
533 fn new(tool: &'a Tool) -> Self {
534 Self {
535 tool,
536 errors: Vec::new(),
537 input_roots: Vec::new(),
538 used_ref_ids: HashSet::new(),
539 }
540 }
541
542 fn run(&mut self) {
543 self.check_duplicate_type_ids();
544
545 if self.tool.commands.nodes.is_empty() {
546 self.errors.push(ToolValidationError::EmptyCommandTree);
547 }
548
549 if !self.tool.commands.nodes.is_empty() {
550 let mut visited = HashSet::new();
551 let mut on_stack = HashSet::new();
552 self.visit_command(CommandIndex(0), &[], &mut visited, &mut on_stack);
553 for i in 0..self.tool.commands.nodes.len() {
554 if !visited.contains(&(i as i32)) {
555 self.errors
556 .push(ToolValidationError::UnreachableCommandNode { index: i as i32 });
557 }
558 }
559 }
560
561 self.check_no_variant_in_input();
562 self.check_def_refs();
563 }
564
565 fn command_node(&self, index: CommandIndex) -> Option<&'a CommandNode> {
566 index
567 .as_usize()
568 .and_then(|i| self.tool.commands.nodes.get(i))
569 }
570
571 fn check_identifier(&mut self, kind: &'static str, value: &str) {
572 if !is_valid_identifier(value) {
573 self.errors.push(ToolValidationError::InvalidIdentifier {
574 kind,
575 value: value.to_string(),
576 });
577 }
578 }
579
580 fn visit_command(
583 &mut self,
584 index: CommandIndex,
585 ancestor_globals: &[&'a Globals],
586 visited: &mut HashSet<i32>,
587 on_stack: &mut HashSet<i32>,
588 ) {
589 if on_stack.contains(&index.0) {
590 self.errors
591 .push(ToolValidationError::CommandTreeCycle { index: index.0 });
592 return;
593 }
594 if !visited.insert(index.0) {
595 self.errors
598 .push(ToolValidationError::DuplicateCommandParent { index: index.0 });
599 return;
600 }
601 let Some(node) = self.command_node(index) else {
602 return;
603 };
604 on_stack.insert(index.0);
605
606 self.check_identifier("command name", &node.name);
607 for alias in &node.aliases {
608 self.check_identifier("command alias", alias);
609 }
610 self.check_globals(&node.name, &node.globals);
611 self.check_global_scope_uniqueness(&node.name, ancestor_globals, &node.globals);
612
613 let mut in_scope: Vec<&'a Globals> = ancestor_globals.to_vec();
616 in_scope.push(&node.globals);
617
618 if let Some(body) = &node.body {
619 self.check_body(&node.name, body, &in_scope);
620 }
621
622 self.check_subcommand_uniqueness(node);
623
624 for sub in &node.subcommands {
625 let len = self.tool.commands.nodes.len();
626 if sub.as_usize().filter(|i| *i < len).is_none() {
627 self.errors
628 .push(ToolValidationError::CommandIndexOutOfBounds { index: sub.0, len });
629 continue;
630 }
631 self.visit_command(*sub, &in_scope, visited, on_stack);
632 }
633
634 on_stack.remove(&index.0);
635 }
636
637 fn check_globals(&mut self, command: &str, globals: &'a Globals) {
638 for opt in &globals.options {
639 self.check_option_identifiers(opt);
640 self.check_repeatable_map(command, opt);
641 self.check_option_default(command, opt);
642 self.check_type_well_formed(command, &opt.long, option_input_type(opt));
643 self.input_roots.push(InputRoot {
647 command: command.to_string(),
648 position: opt.long.clone(),
649 ty: option_input_type(opt),
650 });
651 }
652 for flag in &globals.flags {
653 self.check_identifier("flag long name", &flag.long);
654 for alias in &flag.aliases {
655 self.check_identifier("flag alias", alias);
656 }
657 }
658 }
659
660 fn check_global_scope_uniqueness(
665 &mut self,
666 command: &str,
667 ancestors: &[&'a Globals],
668 own: &Globals,
669 ) {
670 let mut names: HashSet<String> = HashSet::new();
671 let mut shorts: HashSet<char> = HashSet::new();
672 for globals in ancestors {
673 for opt in &globals.options {
674 names.insert(opt.long.clone());
675 names.extend(opt.aliases.iter().cloned());
676 if let Some(short) = opt.short {
677 shorts.insert(short);
678 }
679 }
680 for flag in &globals.flags {
681 names.insert(flag.long.clone());
682 names.extend(flag.aliases.iter().cloned());
683 if let Some(short) = flag.short {
684 shorts.insert(short);
685 }
686 }
687 }
688
689 let mut add_name = |this: &mut Self, name: &str| {
690 if !names.insert(name.to_string()) {
691 this.errors.push(ToolValidationError::DuplicateName {
692 command: command.to_string(),
693 name: name.to_string(),
694 });
695 }
696 };
697 for opt in &own.options {
698 add_name(self, &opt.long);
699 for alias in &opt.aliases {
700 add_name(self, alias);
701 }
702 if let Some(short) = opt.short
703 && !shorts.insert(short)
704 {
705 self.errors.push(ToolValidationError::DuplicateShort {
706 command: command.to_string(),
707 short,
708 });
709 }
710 }
711 for flag in &own.flags {
712 add_name(self, &flag.long);
713 for alias in &flag.aliases {
714 add_name(self, alias);
715 }
716 if let Some(short) = flag.short
717 && !shorts.insert(short)
718 {
719 self.errors.push(ToolValidationError::DuplicateShort {
720 command: command.to_string(),
721 short,
722 });
723 }
724 }
725 }
726
727 fn check_option_identifiers(&mut self, opt: &OptionSpec) {
728 self.check_identifier("option long name", &opt.long);
729 for alias in &opt.aliases {
730 self.check_identifier("option alias", alias);
731 }
732 }
733
734 fn check_repeatable_map(&mut self, command: &str, opt: &OptionSpec) {
737 if let OptionShape::RepeatableMap(shape) = &opt.shape {
738 let not_a_map = match self.tool.schema.resolve_ref(&shape.map_type) {
747 Ok(SchemaType::Map { .. }) => false,
748 Ok(_) | Err(RefResolutionError::RecursiveRef(_)) => true,
749 Err(RefResolutionError::DanglingRef(_)) => false,
750 };
751 if not_a_map {
752 self.errors
753 .push(ToolValidationError::RepeatableMapTypeNotMap {
754 command: command.to_string(),
755 name: opt.long.clone(),
756 });
757 }
758 }
759 }
760
761 fn check_option_default(&mut self, command: &str, opt: &OptionSpec) {
767 let Some(default) = &opt.default else {
768 return;
769 };
770 let graph = &self.tool.schema;
771 let result = match &opt.shape {
775 OptionShape::Scalar(ty) | OptionShape::OptionalScalar(ty) => {
776 validate_value(graph, ty, default)
777 }
778 OptionShape::RepeatableList(shape) => {
779 let list_ty = SchemaType::list(shape.item_type.clone());
780 validate_value(graph, &list_ty, default)
781 }
782 OptionShape::RepeatableMap(shape) => validate_value(graph, &shape.map_type, default),
783 };
784 if let Err(errors) = result
785 && !value_mismatch_is_only_dangling(&errors)
786 {
787 self.errors.push(ToolValidationError::DefaultTypeMismatch {
788 command: command.to_string(),
789 name: opt.long.clone(),
790 });
791 }
792 }
793
794 fn check_positional_default(&mut self, command: &str, positional: &Positional) {
797 if let Some(default) = &positional.default
798 && let Err(errors) = validate_value(&self.tool.schema, &positional.type_, default)
799 && !value_mismatch_is_only_dangling(&errors)
802 {
803 self.errors.push(ToolValidationError::DefaultTypeMismatch {
804 command: command.to_string(),
805 name: positional.name.clone(),
806 });
807 }
808 }
809
810 fn check_type_well_formed(&mut self, command: &str, position: &str, ty: &SchemaType) {
824 collect_structural_ref_ids(ty, &mut self.used_ref_ids);
825 if let Err(errors) = validate_root_type(&self.tool.schema, ty) {
826 self.errors.extend(map_schema_errors(
827 command.to_string(),
828 position.to_string(),
829 errors,
830 ));
831 }
832 }
833
834 fn check_duplicate_type_ids(&mut self) {
839 let mut seen: HashSet<String> = HashSet::new();
840 for def in &self.tool.schema.defs {
841 if !seen.insert(def.id.to_string()) {
842 self.errors.push(ToolValidationError::DuplicateTypeId {
843 id: def.id.to_string(),
844 });
845 }
846 }
847 }
848
849 fn check_def_refs(&mut self) {
854 let mut seeds = self.used_ref_ids.clone();
865 for def in &self.tool.schema.defs {
866 if !matches!(def.body, SchemaType::Ref { .. }) {
867 collect_structural_ref_ids(&def.body, &mut seeds);
868 }
869 }
870 let covered = pure_alias_closure(&self.tool.schema, &seeds);
871 let mut seen_ids: HashSet<&TypeId> = HashSet::new();
877 let mut duplicate_ids: HashSet<&TypeId> = HashSet::new();
878 for def in &self.tool.schema.defs {
879 if !seen_ids.insert(&def.id) {
880 duplicate_ids.insert(&def.id);
881 }
882 }
883 let mut reported: Vec<ToolValidationError> = Vec::new();
884 for def in &self.tool.schema.defs {
885 if covered.contains(&def.id)
889 && matches!(def.body, SchemaType::Ref { .. })
890 && !duplicate_ids.contains(&def.id)
891 {
892 continue;
893 }
894 if let Err(errors) = validate_root_type(&self.tool.schema, &def.body) {
895 let def_id = def.id.to_string();
896 reported.extend(map_schema_errors(format!("<def {def_id}>"), def_id, errors));
897 }
898 }
899 self.errors.extend(reported);
900 }
901
902 fn check_subcommand_uniqueness(&mut self, node: &'a CommandNode) {
903 let mut seen: HashSet<&str> = HashSet::new();
904 for sub in &node.subcommands {
905 let Some(child) = self.command_node(*sub) else {
906 continue;
907 };
908 for token in std::iter::once(&child.name).chain(child.aliases.iter()) {
909 if !seen.insert(token.as_str()) {
910 self.errors
911 .push(ToolValidationError::DuplicateSubcommandName {
912 parent: node.name.clone(),
913 name: token.clone(),
914 });
915 }
916 }
917 }
918 }
919
920 fn check_body(&mut self, command: &str, body: &'a CommandBody, in_scope: &[&'a Globals]) {
921 let graph: &'a SchemaGraph = &self.tool.schema;
926 let mut global_names: HashSet<String> = HashSet::new();
929 let mut global_shorts: HashSet<char> = HashSet::new();
930 for globals in in_scope {
931 for opt in &globals.options {
932 global_names.insert(opt.long.clone());
933 global_names.extend(opt.aliases.iter().cloned());
934 if let Some(short) = opt.short {
935 global_shorts.insert(short);
936 }
937 }
938 for flag in &globals.flags {
939 global_names.insert(flag.long.clone());
940 global_names.extend(flag.aliases.iter().cloned());
941 if let Some(short) = flag.short {
942 global_shorts.insert(short);
943 }
944 }
945 }
946
947 let mut names = global_names.clone();
950 let mut shorts = global_shorts.clone();
951 let mut add_name = |this: &mut Self, name: &str| {
952 if !names.insert(name.to_string()) {
953 this.errors.push(ToolValidationError::DuplicateName {
954 command: command.to_string(),
955 name: name.to_string(),
956 });
957 }
958 };
959
960 let mut scope = NameScope::default();
963 for globals in in_scope {
964 for opt in &globals.options {
965 register_option(graph, &mut scope, opt);
966 }
967 for flag in &globals.flags {
968 scope.names.insert(flag.long.clone());
969 scope.names.extend(flag.aliases.iter().cloned());
970 }
971 }
972
973 for opt in &body.options {
974 self.check_option_identifiers(opt);
975 add_name(self, &opt.long);
976 for alias in &opt.aliases {
977 add_name(self, alias);
978 }
979 if let Some(short) = opt.short
980 && !shorts.insert(short)
981 {
982 self.errors.push(ToolValidationError::DuplicateShort {
983 command: command.to_string(),
984 short,
985 });
986 }
987 register_option(graph, &mut scope, opt);
988 self.check_repeatable_map(command, opt);
989 self.check_option_default(command, opt);
990 self.check_type_well_formed(command, &opt.long, option_input_type(opt));
991 self.input_roots.push(InputRoot {
994 command: command.to_string(),
995 position: opt.long.clone(),
996 ty: option_input_type(opt),
997 });
998 }
999
1000 for flag in &body.flags {
1001 self.check_identifier("flag long name", &flag.long);
1002 for alias in &flag.aliases {
1003 self.check_identifier("flag alias", alias);
1004 }
1005 add_name(self, &flag.long);
1006 for alias in &flag.aliases {
1007 add_name(self, alias);
1008 }
1009 if let Some(short) = flag.short
1010 && !shorts.insert(short)
1011 {
1012 self.errors.push(ToolValidationError::DuplicateShort {
1013 command: command.to_string(),
1014 short,
1015 });
1016 }
1017 scope.names.insert(flag.long.clone());
1018 scope.names.extend(flag.aliases.iter().cloned());
1019 }
1020
1021 for positional in &body.positionals.fixed {
1022 self.check_identifier("positional name", &positional.name);
1023 add_name(self, &positional.name);
1024 scope.names.insert(positional.name.clone());
1025 scope.typed.insert(
1026 positional.name.clone(),
1027 ValueComparand::Type(&positional.type_, ValueIsMode::WholeOrOnePeel),
1028 );
1029 self.check_positional_default(command, positional);
1030 self.check_type_well_formed(command, &positional.name, &positional.type_);
1031 self.input_roots.push(InputRoot {
1032 command: command.to_string(),
1033 position: positional.name.clone(),
1034 ty: &positional.type_,
1035 });
1036 }
1037
1038 if let Some(tail) = &body.positionals.tail {
1039 self.check_identifier("positional name", &tail.name);
1040 add_name(self, &tail.name);
1041 scope.names.insert(tail.name.clone());
1042 scope.typed.insert(
1046 tail.name.clone(),
1047 ValueComparand::Type(&tail.item_type, ValueIsMode::Exact),
1048 );
1049 self.check_type_well_formed(command, &tail.name, &tail.item_type);
1050 self.input_roots.push(InputRoot {
1051 command: command.to_string(),
1052 position: tail.name.clone(),
1053 ty: &tail.item_type,
1054 });
1055 if tail.verbatim && tail.separator.is_none() {
1056 self.errors
1057 .push(ToolValidationError::VerbatimWithoutSeparator {
1058 command: command.to_string(),
1059 positional: tail.name.clone(),
1060 });
1061 }
1062 }
1063
1064 for constraint in &body.constraints {
1065 self.check_constraint(command, constraint, &scope);
1066 }
1067
1068 if let Some(result) = &body.result {
1069 for formatter in &result.formatters {
1070 self.check_identifier("formatter name", &formatter.name);
1071 }
1072 let resolved = result
1073 .formatters
1074 .iter()
1075 .any(|f| f.name == result.default_formatter);
1076 if !resolved {
1077 self.errors
1078 .push(ToolValidationError::UnresolvedDefaultFormatter {
1079 command: command.to_string(),
1080 formatter: result.default_formatter.clone(),
1081 });
1082 }
1083 self.check_type_well_formed(command, "result", &result.type_);
1084 }
1085
1086 for error_case in &body.errors {
1087 self.check_identifier("error-case name", &error_case.name);
1088 if let Some(payload) = &error_case.payload {
1089 self.check_type_well_formed(command, &error_case.name, payload);
1090 }
1091 }
1092 }
1093
1094 fn check_constraint(&mut self, command: &str, constraint: &Constraint, scope: &NameScope) {
1095 let refs = collect_refs(constraint);
1096 for r in refs {
1097 match r {
1098 Ref::Present(name) => {
1099 if !scope.names.contains(name) {
1100 self.errors
1101 .push(ToolValidationError::UnresolvedConstraintRef {
1102 command: command.to_string(),
1103 name: name.clone(),
1104 });
1105 }
1106 }
1107 Ref::ValueIs(value_is) => {
1108 if !scope.names.contains(&value_is.name) {
1109 self.errors
1110 .push(ToolValidationError::UnresolvedConstraintRef {
1111 command: command.to_string(),
1112 name: value_is.name.clone(),
1113 });
1114 continue;
1115 }
1116 match scope.typed.get(&value_is.name) {
1117 Some(ValueComparand::Type(declared, mode)) => {
1118 match self.value_is_outcome(declared, *mode, &value_is.value) {
1119 ValueIsOutcome::Compatible => {}
1120 ValueIsOutcome::Mismatch => {
1121 self.errors.push(ToolValidationError::ValueIsTypeMismatch {
1122 command: command.to_string(),
1123 name: value_is.name.clone(),
1124 });
1125 }
1126 ValueIsOutcome::BlockedByDanglingRef => {
1127 }
1132 }
1133 }
1134 Some(ValueComparand::BlockedByTypeError) => {
1135 }
1141 None => {
1142 self.errors.push(ToolValidationError::ValueIsTypeMismatch {
1145 command: command.to_string(),
1146 name: value_is.name.clone(),
1147 });
1148 }
1149 }
1150 }
1151 }
1152 }
1153 }
1154
1155 fn value_is_outcome(
1171 &self,
1172 declared: &SchemaType,
1173 mode: ValueIsMode,
1174 value: &SchemaValue,
1175 ) -> ValueIsOutcome {
1176 let graph = &self.tool.schema;
1177 let direct = validate_value(graph, declared, value);
1178 if direct.is_ok() {
1179 return ValueIsOutcome::Compatible;
1180 }
1181
1182 if mode == ValueIsMode::Exact {
1185 return match direct {
1186 Err(errors) if value_mismatch_is_only_dangling(&errors) => {
1187 ValueIsOutcome::BlockedByDanglingRef
1188 }
1189 _ => ValueIsOutcome::Mismatch,
1190 };
1191 }
1192
1193 if let Ok(mut peeled) = graph.resolve_ref(declared) {
1199 while let SchemaType::Option { inner, .. } = peeled {
1200 match graph.resolve_ref(inner) {
1201 Ok(next) => peeled = next,
1202 Err(RefResolutionError::DanglingRef(_)) => {
1207 return ValueIsOutcome::BlockedByDanglingRef;
1208 }
1209 Err(RefResolutionError::RecursiveRef(_)) => {
1212 return ValueIsOutcome::Mismatch;
1213 }
1214 }
1215 }
1216 let relaxed = match peeled {
1217 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1218 Some(element.as_ref())
1219 }
1220 SchemaType::Map { value: v, .. } => Some(v.as_ref()),
1221 _ => None,
1222 };
1223 if let Some(inner_ty) = relaxed {
1224 return match validate_value(graph, inner_ty, value) {
1225 Ok(()) => ValueIsOutcome::Compatible,
1226 Err(errors) if value_mismatch_is_only_dangling(&errors) => {
1227 ValueIsOutcome::BlockedByDanglingRef
1228 }
1229 Err(_) => ValueIsOutcome::Mismatch,
1230 };
1231 }
1232 }
1233
1234 match direct {
1236 Err(errors) if value_mismatch_is_only_dangling(&errors) => {
1237 ValueIsOutcome::BlockedByDanglingRef
1238 }
1239 _ => ValueIsOutcome::Mismatch,
1240 }
1241 }
1242
1243 fn check_no_variant_in_input(&mut self) {
1244 let graph = &self.tool.schema;
1245 let mut errors = Vec::new();
1246 for root in &self.input_roots {
1247 let mut visited: HashSet<String> = HashSet::new();
1248 if type_reaches_variant(graph, root.ty, &mut visited) {
1249 errors.push(ToolValidationError::VariantInInputPosition {
1250 command: root.command.clone(),
1251 position: root.position.clone(),
1252 });
1253 }
1254 }
1255 self.errors.extend(errors);
1256 }
1257}
1258
1259fn option_value_type<'a>(graph: &'a SchemaGraph, opt: &'a OptionSpec) -> Option<&'a SchemaType> {
1271 match &opt.shape {
1272 OptionShape::Scalar(t) | OptionShape::OptionalScalar(t) => Some(t),
1273 OptionShape::RepeatableList(shape) => Some(&shape.item_type),
1274 OptionShape::RepeatableMap(shape) => match graph.resolve_ref(&shape.map_type).ok()? {
1275 SchemaType::Map { value, .. } => Some(value),
1276 _ => None,
1277 },
1278 }
1279}
1280
1281fn option_input_type(opt: &OptionSpec) -> &SchemaType {
1287 match &opt.shape {
1288 OptionShape::Scalar(t) | OptionShape::OptionalScalar(t) => t,
1289 OptionShape::RepeatableList(shape) => &shape.item_type,
1290 OptionShape::RepeatableMap(shape) => &shape.map_type,
1291 }
1292}
1293
1294fn register_option<'a>(graph: &'a SchemaGraph, scope: &mut NameScope<'a>, opt: &'a OptionSpec) {
1295 scope.names.insert(opt.long.clone());
1296 scope.names.extend(opt.aliases.iter().cloned());
1297 let comparand = match option_value_type(graph, opt) {
1304 Some(ty) => ValueComparand::Type(ty, option_value_is_mode(opt)),
1305 None => ValueComparand::BlockedByTypeError,
1306 };
1307 scope.typed.insert(opt.long.clone(), comparand);
1308 for alias in &opt.aliases {
1309 scope.typed.insert(alias.clone(), comparand);
1310 }
1311}
1312
1313fn option_value_is_mode(opt: &OptionSpec) -> ValueIsMode {
1319 match &opt.shape {
1320 OptionShape::Scalar(_) | OptionShape::OptionalScalar(_) => ValueIsMode::WholeOrOnePeel,
1321 OptionShape::RepeatableList(_) | OptionShape::RepeatableMap(_) => ValueIsMode::Exact,
1322 }
1323}
1324
1325fn type_reaches_variant(
1329 graph: &SchemaGraph,
1330 ty: &SchemaType,
1331 visited: &mut HashSet<String>,
1332) -> bool {
1333 if let SchemaType::Ref { id, .. } = ty
1334 && !visited.insert(id.to_string())
1335 {
1336 return false;
1337 }
1338 let Ok(resolved) = graph.resolve_ref(ty) else {
1339 return false;
1340 };
1341 match resolved {
1342 SchemaType::Variant { .. } => true,
1343 SchemaType::Record { fields, .. } => fields
1344 .iter()
1345 .any(|f| type_reaches_variant(graph, &f.body, visited)),
1346 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1347 type_reaches_variant(graph, element, visited)
1348 }
1349 SchemaType::Option { inner, .. } => type_reaches_variant(graph, inner, visited),
1350 SchemaType::Map { key, value, .. } => {
1351 type_reaches_variant(graph, key, visited) || type_reaches_variant(graph, value, visited)
1352 }
1353 SchemaType::Tuple { elements, .. } => elements
1354 .iter()
1355 .any(|e| type_reaches_variant(graph, e, visited)),
1356 SchemaType::Result { spec, .. } => {
1357 spec.ok
1358 .as_deref()
1359 .is_some_and(|t| type_reaches_variant(graph, t, visited))
1360 || spec
1361 .err
1362 .as_deref()
1363 .is_some_and(|t| type_reaches_variant(graph, t, visited))
1364 }
1365 SchemaType::Union { spec, .. } => spec
1366 .branches
1367 .iter()
1368 .any(|b| type_reaches_variant(graph, &b.body, visited)),
1369 SchemaType::Future { inner: Some(t), .. } | SchemaType::Stream { inner: Some(t), .. } => {
1370 type_reaches_variant(graph, t, visited)
1371 }
1372 _ => false,
1373 }
1374}
1375
1376fn value_mismatch_is_only_dangling(errors: &[ValueError]) -> bool {
1383 !errors.is_empty()
1384 && errors
1385 .iter()
1386 .all(|e| matches!(e, ValueError::DanglingRef { .. }))
1387}
1388
1389fn map_schema_errors(
1402 command: String,
1403 position: String,
1404 errors: Vec<SchemaError>,
1405) -> Vec<ToolValidationError> {
1406 let mut seen_chain_targets: HashSet<TypeId> = HashSet::new();
1414 let mut mapped = Vec::with_capacity(errors.len());
1415 for error in errors {
1416 match error {
1417 SchemaError::DanglingRef(id) => {
1418 if seen_chain_targets.insert(id.clone()) {
1419 mapped.push(ToolValidationError::UnresolvedTypeRef {
1420 command: command.clone(),
1421 position: position.clone(),
1422 id: id.to_string(),
1423 });
1424 }
1425 }
1426 SchemaError::RecursiveAlias(id) => {
1427 if seen_chain_targets.insert(id.clone()) {
1428 mapped.push(ToolValidationError::IllFormedSchema {
1429 command: command.clone(),
1430 position: position.clone(),
1431 detail: SchemaError::RecursiveAlias(id).to_string(),
1432 });
1433 }
1434 }
1435 other => mapped.push(ToolValidationError::IllFormedSchema {
1436 command: command.clone(),
1437 position: position.clone(),
1438 detail: other.to_string(),
1439 }),
1440 }
1441 }
1442 mapped
1443}
1444
1445fn collect_structural_ref_ids(ty: &SchemaType, out: &mut HashSet<TypeId>) {
1450 match ty {
1451 SchemaType::Ref { id, .. } => {
1452 out.insert(id.clone());
1453 }
1454 SchemaType::Record { fields, .. } => {
1455 for field in fields {
1456 collect_structural_ref_ids(&field.body, out);
1457 }
1458 }
1459 SchemaType::Variant { cases, .. } => {
1460 for case in cases {
1461 if let Some(payload) = &case.payload {
1462 collect_structural_ref_ids(payload, out);
1463 }
1464 }
1465 }
1466 SchemaType::Tuple { elements, .. } => {
1467 for element in elements {
1468 collect_structural_ref_ids(element, out);
1469 }
1470 }
1471 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1472 collect_structural_ref_ids(element, out);
1473 }
1474 SchemaType::Map { key, value, .. } => {
1475 collect_structural_ref_ids(key, out);
1476 collect_structural_ref_ids(value, out);
1477 }
1478 SchemaType::Option { inner, .. } => collect_structural_ref_ids(inner, out),
1479 SchemaType::Result { spec, .. } => {
1480 if let Some(ok) = &spec.ok {
1481 collect_structural_ref_ids(ok, out);
1482 }
1483 if let Some(err) = &spec.err {
1484 collect_structural_ref_ids(err, out);
1485 }
1486 }
1487 SchemaType::Union { spec, .. } => {
1488 for branch in &spec.branches {
1489 collect_structural_ref_ids(&branch.body, out);
1490 }
1491 }
1492 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1493 if let Some(inner) = inner {
1494 collect_structural_ref_ids(inner, out);
1495 }
1496 }
1497 _ => {}
1498 }
1499}
1500
1501fn pure_alias_closure(graph: &SchemaGraph, seeds: &HashSet<TypeId>) -> HashSet<TypeId> {
1510 let mut covered = HashSet::new();
1511 for seed in seeds {
1512 let mut current = seed.clone();
1513 while let Some(def) = graph.lookup(¤t) {
1514 let SchemaType::Ref { id, .. } = &def.body else {
1515 break;
1516 };
1517 if !covered.insert(current.clone()) {
1518 break;
1519 }
1520 current = id.clone();
1521 }
1522 }
1523 covered
1524}
1525
1526fn collect_refs(constraint: &Constraint) -> Vec<&Ref> {
1528 match constraint {
1529 Constraint::RequiresAll(refs)
1530 | Constraint::AllOrNone(refs)
1531 | Constraint::RequiresAny(refs) => refs.iter().collect(),
1532 Constraint::MutexGroups(groups) => groups.iter().flat_map(|g| g.refs.iter()).collect(),
1533 Constraint::Implies(implies) => implies.lhs.iter().chain(implies.rhs.iter()).collect(),
1534 Constraint::Forbids(forbids) => forbids.lhs.iter().chain(forbids.rhs.iter()).collect(),
1535 }
1536}
1537
1538#[cfg(test)]
1539mod tests;