1use std::collections::HashMap;
14
15use openusd::{sdf, tf, usd};
16
17use crate::error::Error;
18use crate::model::{API_SCHEMA_BASE, Class, Library, Property, Reflection, SCHEMA_BASE, Shape};
19
20#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum Violation {
28 #[error("inherits from `{name}`, which no layer declares")]
30 MissingBase {
31 name: tf::Token,
33 },
34
35 #[error("inherits from itself through {chain}")]
38 CyclicInheritance {
39 chain: String,
41 },
42
43 #[error("inherits from {count} classes; a schema has at most one base")]
46 MultipleBases {
47 count: usize,
49 },
50
51 #[error("an applied API schema must inherit directly from APISchemaBase, not from `{base}`")]
59 AppliedApiNotRooted {
60 base: tf::Token,
62 },
63
64 #[error("a non-applied API schema inherits APISchemaBase or another non-applied API, not `{base}`")]
68 NonAppliedApiNotRooted {
69 base: tf::Token,
71 },
72
73 #[error("inherits from `{base}`, which is the same schema family")]
76 InheritsWithinFamily {
77 base: tf::Token,
79 },
80
81 #[error("unknown apiSchemaType `{spelling}`")]
83 UnknownApiSchemaType {
84 spelling: String,
86 },
87
88 #[error("{key} does not apply to a {kind} schema")]
90 KindMismatch {
91 key: &'static str,
94 kind: &'static str,
96 },
97
98 #[error("a concrete schema must inherit from Typed")]
101 ConcreteNotTyped,
102
103 #[error("a typed schema cannot declare apiSchemaType")]
111 ApiSchemaTypeOnTyped,
112
113 #[error("`{identifier}` is not an allowed schema identifier")]
116 DisallowedIdentifier {
117 identifier: tf::Token,
119 },
120
121 #[error("typeName `{authored}` is not the schema's own identifier")]
124 TypeNameMismatch {
125 authored: tf::Token,
127 },
128
129 #[error("apiSchemas must be authored as a prepend")]
136 ApiSchemasNotPrepended,
137
138 #[error("`{first}` and `{second}` both reach `{name}` in the schematics")]
145 DuplicateSchematicsName {
146 name: tf::Token,
148 first: tf::Token,
150 second: tf::Token,
152 },
153
154 #[error("property `{property}` has unknown type `{type_name}`")]
157 UnknownPropertyType {
158 property: tf::Token,
160 type_name: tf::Token,
162 },
163
164 #[error("a multiple-apply schema needs propertyNamespacePrefix exactly when it has properties")]
168 MultipleApplyPrefix,
169
170 #[error("`{field}` is not a field a schema may declare")]
173 DisallowedField {
174 field: String,
176 },
177
178 #[error("`{property}` is an API schema override here, but `{class}` declares it outright")]
186 OverridesADeclaration {
187 property: tf::Token,
189 class: tf::Token,
191 },
192
193 #[error("`{property}` is {} on `{class}`", redeclared_as(.ours, .theirs))]
203 IncompatibleRedeclaration {
204 property: tf::Token,
206 class: tf::Token,
208 theirs: Box<Shape>,
211 ours: Box<Shape>,
213 },
214
215 #[error("token `{id}` would hold both \"{first}\" and \"{second}\"")]
218 TokenValueCollision {
219 id: String,
221 first: String,
223 second: String,
225 },
226
227 #[error("`{first}` and `{second}` both reach the name {name}")]
233 RustNameCollision {
234 name: String,
236 first: String,
238 second: String,
240 },
241
242 #[error("`{first}` and `{second}` both reach the method {method}")]
245 MethodCollision {
246 method: String,
248 first: tf::Token,
250 second: tf::Token,
252 },
253
254 #[error("`{name}` is not an identifier, so nothing generated can be called it")]
257 NotAnIdentifier {
258 name: String,
260 },
261
262 #[error(
266 "`{class}` is inherited from or reflected but not generated; declare it in the root layer or in a library of its own"
267 )]
268 Ungenerated {
269 class: tf::Token,
271 },
272
273 #[error("reflects `{schema}`, which no layer declares")]
275 UnknownReflectedSchema {
276 schema: tf::Token,
278 },
279
280 #[error("`{schema}` is reflected but not applied; add it to `prepend apiSchemas`")]
288 ReflectedNotApplied {
289 schema: tf::Token,
291 },
292
293 #[error("`{class}` belongs to the {library} library; name where its views live with Builder::extern_library")]
297 UnknownLibrary {
298 library: String,
300 class: tf::Token,
302 },
303
304 #[error("the {library} library's views were placed at `{path}`, which is no Rust path")]
307 UnreadableLibraryPath {
308 library: String,
310 path: String,
312 },
313
314 #[error("unknown apiGetImplementation `{spelling}`")]
316 UnknownApiGetImplementation {
317 spelling: String,
319 },
320
321 #[error("`{property}` asks for a custom accessor but generates none")]
324 CustomGetWithoutAccessor {
325 property: tf::Token,
327 },
328
329 #[error("`{property}` is a `{type_name}`, which has no value-type constant to declare it with")]
332 UnnameableType {
333 property: tf::Token,
335 type_name: tf::Token,
337 },
338}
339
340pub fn check(library: &Library) -> Result<Vec<String>, Error> {
343 let mut warnings = Vec::new();
344
345 for class in &library.classes {
346 check_identifier(class)?;
347 check_kind(class)?;
348 check_inheritance(class)?;
349 check_fields(class)?;
350 check_properties(class)?;
351 check_reflection(class, library, &mut warnings)?;
352 warnings.extend(conventions(class));
353 }
354
355 Ok(warnings)
356}
357
358fn check_identifier(class: &Class) -> Result<(), Error> {
360 if !usd::SchemaRegistry::is_allowed_schema_identifier(class.identifier.as_str()) {
361 return Err(class.violation(Violation::DisallowedIdentifier {
362 identifier: class.identifier.clone(),
363 }));
364 }
365
366 if let Some(authored) = &class.authored_type_name
367 && authored != &class.identifier
368 {
369 return Err(class.violation(Violation::TypeNameMismatch {
370 authored: authored.clone(),
371 }));
372 }
373
374 Ok(())
375}
376
377fn check_kind(class: &Class) -> Result<(), Error> {
379 let kind = class.kind;
380 let metadata = &class.metadata;
381
382 let multiple_apply = kind == usd::SchemaKind::MultipleApplyApi;
385 for (mismatched, key) in [
386 (
387 kind != usd::SchemaKind::SingleApplyApi && !metadata.auto_apply_to.is_empty(),
388 "apiSchemaAutoApplyTo",
389 ),
390 (
391 !kind.is_applied_api_schema() && !metadata.can_only_apply_to.is_empty(),
392 "apiSchemaCanOnlyApplyTo",
393 ),
394 (
395 !multiple_apply && !metadata.allowed_instance_names.is_empty(),
396 "apiSchemaAllowedInstanceNames",
397 ),
398 (
399 !multiple_apply && !metadata.instance_restrictions.is_empty(),
400 "apiSchemaInstances",
401 ),
402 (
403 kind != usd::SchemaKind::ConcreteTyped && !metadata.fallback_types.is_empty(),
404 "fallbackTypes",
405 ),
406 (
411 multiple_apply
412 && class
413 .authored_fields
414 .iter()
415 .any(|field| field == sdf::FieldKey::PropertyOrder.as_str()),
416 sdf::FieldKey::PropertyOrder.as_str(),
417 ),
418 ] {
419 if mismatched {
420 return Err(class.violation(Violation::KindMismatch {
421 key,
422 kind: kind_name(kind),
423 }));
424 }
425 }
426
427 if kind.is_api_schema() && (class.is_typed || class.authored_type_name.is_some()) {
428 return Err(class.violation(Violation::ApiSchemaTypeOnTyped));
429 }
430
431 if multiple_apply {
432 let has_properties = class.local_properties().next().is_some();
433 if has_properties != metadata.property_namespace_prefix.is_some() {
434 return Err(class.violation(Violation::MultipleApplyPrefix));
435 }
436 } else if metadata.property_namespace_prefix.is_some() {
437 return Err(class.violation(Violation::KindMismatch {
438 key: "propertyNamespacePrefix",
439 kind: kind_name(kind),
440 }));
441 }
442
443 if kind == usd::SchemaKind::ConcreteTyped && !class.is_typed {
446 return Err(class.violation(Violation::ConcreteNotTyped));
447 }
448
449 Ok(())
450}
451
452fn check_inheritance(class: &Class) -> Result<(), Error> {
454 if class.authored_base_count > 1 {
455 return Err(class.violation(Violation::MultipleBases {
456 count: class.authored_base_count,
457 }));
458 }
459
460 for base in &class.bases {
463 let (family, _) = usd::SchemaRegistry::parse_schema_family_and_version(&base.identifier);
464 if family == class.family {
465 return Err(class.violation(Violation::InheritsWithinFamily {
466 base: base.identifier.clone(),
467 }));
468 }
469 }
470
471 if !class.kind.is_api_schema() {
472 return Ok(());
473 }
474
475 let base = class.direct_base.clone().unwrap_or_else(|| tf::Token::new(SCHEMA_BASE));
480 let inherits_root = base.as_str() == API_SCHEMA_BASE;
481
482 if class.kind.is_applied_api_schema() && !inherits_root {
483 return Err(class.violation(Violation::AppliedApiNotRooted { base }));
484 }
485
486 if class.kind == usd::SchemaKind::NonAppliedApi && !inherits_root {
491 let base_is_non_applied = class
492 .bases
493 .first()
494 .is_some_and(|base| base.kind == usd::SchemaKind::NonAppliedApi);
495 if !base_is_non_applied {
496 return Err(class.violation(Violation::NonAppliedApiNotRooted { base }));
497 }
498 }
499
500 Ok(())
501}
502
503fn check_fields(class: &Class) -> Result<(), Error> {
508 if let Some(field) = disallowed_field(&class.authored_fields) {
509 return Err(class.violation(Violation::DisallowedField { field: field.clone() }));
510 }
511
512 if let Some(op) = &class.api_schemas_op
513 && (op.explicit
514 || !op.explicit_items.is_empty()
515 || !op.appended_items.is_empty()
516 || !op.added_items.is_empty()
517 || !op.deleted_items.is_empty()
518 || !op.ordered_items.is_empty())
519 {
520 return Err(class.violation(Violation::ApiSchemasNotPrepended));
521 }
522
523 Ok(())
524}
525
526fn check_properties(class: &Class) -> Result<(), Error> {
528 let mut seen: HashMap<&tf::Token, &tf::Token> = HashMap::new();
529 for property in &class.properties {
530 if let Some(first) = seen.insert(&property.schematics_name, &property.name) {
531 return Err(property.violation(Violation::DuplicateSchematicsName {
532 name: property.schematics_name.clone(),
533 first: first.clone(),
534 second: property.name.clone(),
535 }));
536 }
537 }
538
539 for property in &class.properties {
540 if let Some(field) = disallowed_field(property.fields.keys()) {
541 return Err(property.violation(Violation::DisallowedField { field: field.clone() }));
542 }
543
544 if property.shape.spec_type == sdf::SpecType::Attribute && property.type_name().is_none() {
545 let declared = property
546 .fields
547 .get(sdf::FieldKey::TypeName.as_str())
548 .and_then(|value| value.clone().try_as_token())
549 .unwrap_or_default();
550 return Err(property.violation(Violation::UnknownPropertyType {
551 property: property.name.clone(),
552 type_name: declared,
553 }));
554 }
555
556 if property.api.custom_get && !property.has_accessor() {
557 return Err(property.violation(Violation::CustomGetWithoutAccessor {
558 property: property.name.clone(),
559 }));
560 }
561
562 if property.is_override()
568 && let Some(site) = property.sites.iter().find(|site| !site.is_override)
569 {
570 return Err(Error::Definition {
571 origin: site.origin.describe(),
572 violation: Violation::OverridesADeclaration {
573 property: property.name.clone(),
574 class: site.class.clone(),
575 },
576 });
577 }
578
579 if let Some((mine, weaker)) = property.sites.split_first()
584 && mine.class == class.identifier
585 && let Some(site) = weaker.iter().find(|site| site.shape != mine.shape)
586 {
587 return Err(Error::Definition {
588 origin: mine.origin.describe(),
589 violation: Violation::IncompatibleRedeclaration {
590 property: property.name.clone(),
591 class: site.class.clone(),
592 theirs: Box::new(site.shape.clone()),
593 ours: Box::new(mine.shape.clone()),
594 },
595 });
596 }
597 }
598
599 Ok(())
600}
601
602fn check_reflection(class: &Class, library: &Library, warnings: &mut Vec<String>) -> Result<(), Error> {
610 for reflection in class.reflections(library) {
611 match reflection {
612 Reflection::Taken { schema, shadowed, .. } => {
613 for (property, first) in shadowed {
614 warnings.push(format!(
615 "{}: `{}` of `{}` is not reflected; `{first}` already declares it",
616 class.origin.describe(),
617 property.name,
618 schema.identifier
619 ));
620 }
621 }
622 Reflection::NotSingleApply(schema) => warnings.push(format!(
623 "{}: `{}` is a {} schema, so it is not reflected; only a single-apply API schema can be",
624 class.origin.describe(),
625 schema.identifier,
626 kind_name(schema.kind)
627 )),
628 Reflection::NotApplied(schema) => {
631 return Err(class.violation(Violation::ReflectedNotApplied {
632 schema: schema.identifier.clone(),
633 }));
634 }
635 }
636 }
637 Ok(())
638}
639
640fn conventions(class: &Class) -> Vec<String> {
642 let mut warnings = Vec::new();
643 let ends_in_api = class.family.as_str().ends_with("API");
644 let is_api = class.kind.is_api_schema();
645
646 if is_api && !ends_in_api {
648 warnings.push(format!(
649 "{}: an API schema's name conventionally ends in `API`",
650 class.origin.describe()
651 ));
652 }
653 if !is_api && ends_in_api {
654 warnings.push(format!(
655 "{}: only an API schema's name conventionally ends in `API`",
656 class.origin.describe()
657 ));
658 }
659 warnings
660}
661
662fn disallowed_field<'a>(fields: impl IntoIterator<Item = &'a String>) -> Option<&'a String> {
672 let input = [
673 sdf::FieldKey::InheritPaths.as_str(),
674 sdf::FieldKey::CustomData.as_str(),
675 sdf::FieldKey::Specifier.as_str(),
676 sdf::ChildrenKey::PrimChildren.as_str(),
677 sdf::ChildrenKey::PropertyChildren.as_str(),
678 ];
679 fields
680 .into_iter()
681 .find(|field| !input.contains(&field.as_str()) && usd::SchemaRegistry::is_disallowed_field(field))
682}
683
684fn redeclared_as(ours: &Shape, theirs: &Shape) -> String {
688 spelled(ours)
689 .into_iter()
690 .zip(spelled(theirs))
691 .find(|(ours, theirs)| ours != theirs)
692 .map_or_else(
693 || "as it was".to_owned(),
694 |(ours, theirs)| format!("{ours} here and {theirs}"),
695 )
696}
697
698fn spelled(shape: &Shape) -> [String; 4] {
700 [
701 match shape.spec_type {
702 sdf::SpecType::Relationship => "a relationship",
703 _ => "an attribute",
704 }
705 .to_owned(),
706 shape
707 .type_name
708 .as_ref()
709 .map_or_else(|| "of no type".to_owned(), |name| format!("`{name}`")),
710 match shape.variability {
711 sdf::Variability::Uniform => "uniform",
712 sdf::Variability::Varying => "varying",
713 }
714 .to_owned(),
715 match shape.custom {
716 true => "custom",
717 false => "not custom",
718 }
719 .to_owned(),
720 ]
721}
722
723fn kind_name(kind: usd::SchemaKind) -> &'static str {
725 match kind {
726 usd::SchemaKind::AbstractBase => "abstract base",
727 usd::SchemaKind::AbstractTyped => "abstract typed",
728 usd::SchemaKind::ConcreteTyped => "concrete typed",
729 usd::SchemaKind::NonAppliedApi => "non-applied API",
730 usd::SchemaKind::SingleApplyApi => "single-apply API",
731 usd::SchemaKind::MultipleApplyApi => "multiple-apply API",
732 }
733}
734
735impl Class {
736 pub(crate) fn violation(&self, violation: Violation) -> Error {
738 Error::Definition {
739 origin: self.origin.describe(),
740 violation,
741 }
742 }
743}
744
745impl Property {
746 pub(crate) fn violation(&self, violation: Violation) -> Error {
748 Error::Definition {
749 origin: self.origin.describe(),
750 violation,
751 }
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use crate::tests::{TAG_API, read_fixture, read_source, schema};
759
760 fn violation(name: &str) -> Violation {
762 match read_fixture(name) {
763 Err(Error::Definition { violation, .. }) => violation,
764 Err(other) => panic!("{name} was rejected, but not by a rule: {other}"),
765 Ok(_) => panic!("{name} was accepted"),
766 }
767 }
768
769 #[test]
772 fn multiple_apply_prefix() {
773 assert!(matches!(violation("schemaFail12.usda"), Violation::MultipleApplyPrefix));
774 assert!(matches!(violation("schemaFail13.usda"), Violation::MultipleApplyPrefix));
775 }
776
777 #[test]
779 fn kind_metadata_mismatch() {
780 for (fixture, key) in [
781 ("schemaFail14.usda", "propertyNamespacePrefix"),
782 ("schemaFail15.usda", "fallbackTypes"),
783 ("schemaFail16.usda", "fallbackTypes"),
784 ("schemaFail17.usda", "apiSchemaAutoApplyTo"),
785 ("schemaFail18.usda", "apiSchemaAutoApplyTo"),
786 ("schemaFail24.usda", "propertyOrder"),
787 ] {
788 match violation(fixture) {
789 Violation::KindMismatch { key: found, .. } => assert_eq!(found, key, "{fixture}"),
790 other => panic!("{fixture}: {other}"),
791 }
792 }
793 }
794
795 #[test]
797 fn api_schemas_mode() {
798 assert!(matches!(
799 violation("schemaFail19.usda"),
800 Violation::ApiSchemasNotPrepended
801 ));
802 assert!(matches!(
803 violation("schemaFail20.usda"),
804 Violation::ApiSchemasNotPrepended
805 ));
806 }
807
808 #[test]
811 fn same_family_base() {
812 assert!(matches!(
813 violation("schemaFail21.usda"),
814 Violation::InheritsWithinFamily { .. }
815 ));
816 }
817
818 #[test]
821 fn registry_would_refuse() {
822 assert!(matches!(
823 violation("schemaFail22.usda"),
824 Violation::DisallowedIdentifier { .. }
825 ));
826 match violation("schemaFail23.usda") {
827 Violation::DisallowedField { field } => assert_eq!(field, "kind"),
828 other => panic!("{other}"),
829 }
830 }
831
832 #[test]
837 fn variant_set_refused() {
838 let dir = tempfile::tempdir().expect("tempdir");
839 let error = read_source(
840 dir.path(),
841 r#"#usda 1.0
842
843def "GLOBAL" (
844 customData = {
845 string libraryName = "testVariants"
846 }
847)
848{
849}
850
851class "Typed" {}
852
853class "Varied" (
854 inherits = </Typed>
855) {
856 double plain = 1
857
858 variantSet "shading" = {
859 "red" {
860 double onlyInVariant = 2
861 }
862 }
863}
864"#,
865 )
866 .expect_err("a schema may not vary");
867
868 match error {
869 Error::Definition {
870 violation: Violation::DisallowedField { field },
871 ..
872 } => assert_eq!(field, sdf::ChildrenKey::VariantSetChildren.as_str()),
873 other => panic!("{other}"),
874 }
875 }
876
877 #[test]
881 fn composition_arc_refused() {
882 let dir = tempfile::tempdir().expect("tempdir");
883 let error = read_source(
884 dir.path(),
885 r#"#usda 1.0
886
887def "GLOBAL" (
888 customData = {
889 string libraryName = "testArc"
890 }
891)
892{
893}
894
895class "Typed" {}
896
897class "Specializing" (
898 inherits = </Typed>
899 specializes = </Typed>
900) {}
901"#,
902 )
903 .expect_err("a schema may not compose");
904
905 match error {
906 Error::Definition {
907 violation: Violation::DisallowedField { field },
908 ..
909 } => assert_eq!(field, "specializes"),
910 other => panic!("{other}"),
911 }
912 }
913
914 const OVERRIDES: &str = r#"#usda 1.0
917
918def "GLOBAL" (
919 customData = {
920 string libraryName = "testOverrides"
921 }
922)
923{
924}
925
926class "Typed" {}
927
928class "Base" (
929 inherits = </Typed>
930) {
931 int plain = 1
932}
933
934class "Derived" (
935 inherits = </Base>
936) {
937 int plain = 2 (
938 customData = {
939 bool apiSchemaOverride = true
940 }
941 )
942}
943"#;
944
945 #[test]
949 fn override_of_a_declaration() {
950 let dir = tempfile::tempdir().expect("tempdir");
951 let error = read_source(dir.path(), OVERRIDES).expect_err("a class may not delete what it inherits");
952
953 match error {
954 Error::Definition {
955 violation: Violation::OverridesADeclaration { property, class },
956 ..
957 } => {
958 assert_eq!(property.as_str(), "plain");
959 assert_eq!(class.as_str(), "Base");
960 }
961 other => panic!("{other}"),
962 }
963 }
964
965 #[test]
969 fn declaration_over_an_override() {
970 let dir = tempfile::tempdir().expect("tempdir");
971 let library = read_source(
972 dir.path(),
973 r#"#usda 1.0
974
975def "GLOBAL" (
976 customData = {
977 string libraryName = "testTakenOn"
978 }
979)
980{
981}
982
983class "Typed" {}
984
985class "Base" (
986 inherits = </Typed>
987) {
988 int plain = 1 (
989 customData = {
990 bool apiSchemaOverride = true
991 }
992 )
993}
994
995class "Derived" (
996 inherits = </Base>
997) {
998 int plain = 2
999}
1000"#,
1001 )
1002 .expect("a class may take an inherited override on");
1003
1004 let derived = library
1005 .classes
1006 .iter()
1007 .find(|class| class.identifier.as_str() == "Derived")
1008 .expect("the derived class");
1009 assert_eq!(derived.override_properties().count(), 0);
1010 }
1011
1012 #[test]
1016 fn override_reaches_a_silent_class() {
1017 let dir = tempfile::tempdir().expect("tempdir");
1018 let library = read_source(
1019 dir.path(),
1020 r#"#usda 1.0
1021
1022def "GLOBAL" (
1023 customData = {
1024 string libraryName = "testInheritedOverride"
1025 }
1026)
1027{
1028}
1029
1030class "Typed" {}
1031
1032class "Base" (
1033 inherits = </Typed>
1034) {
1035 int plain = 1 (
1036 customData = {
1037 bool apiSchemaOverride = true
1038 }
1039 )
1040}
1041
1042class "Derived" (
1043 inherits = </Base>
1044) {}
1045"#,
1046 )
1047 .expect("resolves");
1048
1049 for identifier in ["Base", "Derived"] {
1050 let class = library
1051 .classes
1052 .iter()
1053 .find(|class| class.identifier.as_str() == identifier)
1054 .expect("the class");
1055 let names: Vec<&str> = class.override_properties().map(|p| p.name.as_str()).collect();
1056 assert_eq!(names, vec!["plain"], "{identifier}");
1057 }
1058 }
1059
1060 fn redeclaring(base: &str, derived: &str) -> String {
1063 schema(
1064 "testRedeclare",
1065 &format!(
1066 r#"class "Base" (
1067 inherits = </Typed>
1068) {{
1069 {base}
1070}}
1071
1072class "Derived" (
1073 inherits = </Base>
1074) {{
1075 {derived}
1076}}"#
1077 ),
1078 )
1079 }
1080
1081 fn incompatible(base: &str, derived: &str) -> (String, Shape, Shape) {
1084 let dir = tempfile::tempdir().expect("tempdir");
1085 match read_source(dir.path(), &redeclaring(base, derived)).expect_err("the redeclaration changes the property")
1086 {
1087 Error::Definition {
1088 origin,
1089 violation:
1090 Violation::IncompatibleRedeclaration {
1091 class, theirs, ours, ..
1092 },
1093 } => {
1094 assert!(
1095 origin.ends_with("/Derived.plain"),
1096 "reported against the redeclaration: {origin}"
1097 );
1098 (class.to_string(), *theirs, *ours)
1099 }
1100 other => panic!("{other}"),
1101 }
1102 }
1103
1104 #[test]
1107 fn redeclared_type_changes() {
1108 let (class, theirs, ours) = incompatible("int plain = 1", "double plain = 2");
1109 assert_eq!(class, "Base");
1110 assert_eq!(theirs.type_name, Some(tf::Token::new("int")));
1111 assert_eq!(ours.type_name, Some(tf::Token::new("double")));
1112 }
1113
1114 #[test]
1117 fn redeclared_adds_uniform() {
1118 let (_, theirs, ours) = incompatible(r#"token plain = "a""#, r#"uniform token plain = "b""#);
1119 assert_eq!(theirs.variability, sdf::Variability::Varying);
1120 assert_eq!(ours.variability, sdf::Variability::Uniform);
1121 }
1122
1123 #[test]
1125 fn redeclared_adds_custom() {
1126 let (_, theirs, ours) = incompatible("int plain = 1", "custom int plain = 2");
1127 assert!(!theirs.custom);
1128 assert!(ours.custom);
1129 }
1130
1131 #[test]
1134 fn redeclared_spelled() {
1135 let base = Shape {
1136 spec_type: sdf::SpecType::Attribute,
1137 type_name: Some(tf::Token::new("int")),
1138 variability: sdf::Variability::Varying,
1139 custom: false,
1140 };
1141 let typed = Shape {
1142 type_name: Some(tf::Token::new("double")),
1143 ..base.clone()
1144 };
1145 let uniform = Shape {
1146 variability: sdf::Variability::Uniform,
1147 ..base.clone()
1148 };
1149 let custom = Shape {
1150 custom: true,
1151 ..base.clone()
1152 };
1153 let related = Shape {
1154 spec_type: sdf::SpecType::Relationship,
1155 type_name: None,
1156 ..base.clone()
1157 };
1158
1159 assert_eq!(redeclared_as(&typed, &base), "`double` here and `int`");
1160 assert_eq!(redeclared_as(&uniform, &base), "uniform here and varying");
1161 assert_eq!(redeclared_as(&custom, &base), "custom here and not custom");
1162 assert_eq!(redeclared_as(&related, &base), "a relationship here and an attribute");
1163 }
1164
1165 #[test]
1169 fn redeclared_as_rel() {
1170 let dir = tempfile::tempdir().expect("tempdir");
1171 let error = read_source(dir.path(), &redeclaring("int plain = 1", "rel plain"))
1172 .expect_err("the specs disagree on what the property is");
1173 assert!(
1174 matches!(&error, Error::Composition { diagnostic, .. } if diagnostic.contains("inconsistent spec types")),
1175 "{error}"
1176 );
1177 }
1178
1179 #[test]
1182 fn redeclared_renamed_rejected() {
1183 let (_, theirs, ours) = incompatible(
1184 "int plain = 1",
1185 r#"double plain = 2 (
1186 customData = { string apiName = "other" }
1187 )"#,
1188 );
1189 assert_eq!(theirs.type_name, Some(tf::Token::new("int")));
1190 assert_eq!(ours.type_name, Some(tf::Token::new("double")));
1191 }
1192
1193 #[test]
1195 fn redeclared_fallback_only() {
1196 let dir = tempfile::tempdir().expect("tempdir");
1197 read_source(
1198 dir.path(),
1199 &redeclaring("double3 plain = (1, 1, 1)", "double3 plain = (2, 2, 2)"),
1200 )
1201 .expect("a fallback may change");
1202 }
1203
1204 #[test]
1208 fn redeclared_omits_uniform() {
1209 let dir = tempfile::tempdir().expect("tempdir");
1210 let library = read_source(
1211 dir.path(),
1212 &redeclaring(r#"uniform token plain = "a""#, r#"token plain = "b""#),
1213 )
1214 .expect("omitting the keyword changes nothing");
1215
1216 let derived = library
1217 .classes
1218 .iter()
1219 .find(|class| class.identifier.as_str() == "Derived")
1220 .expect("the derived class");
1221 let plain = derived
1222 .local_properties()
1223 .find(|property| property.name.as_str() == "plain")
1224 .expect("the redeclared property");
1225 assert_eq!(plain.shape.variability, sdf::Variability::Uniform);
1226 }
1227
1228 fn reflecting(classes: &str) -> String {
1230 schema("testReflect", classes)
1231 }
1232
1233 #[test]
1236 fn reflected_unknown() {
1237 let dir = tempfile::tempdir().expect("tempdir");
1238 let error = read_source(
1239 dir.path(),
1240 &reflecting(
1241 r#"class "Thing" (
1242 inherits = </Typed>
1243 customData = { token[] reflectedAPISchemas = ["NowhereAPI"] }
1244) {}"#,
1245 ),
1246 )
1247 .expect_err("nothing declares it");
1248
1249 match error {
1250 Error::Definition {
1251 violation: Violation::UnknownReflectedSchema { schema },
1252 ..
1253 } => assert_eq!(schema.as_str(), "NowhereAPI"),
1254 other => panic!("{other}"),
1255 }
1256 }
1257
1258 #[test]
1261 fn reflected_not_applied() {
1262 let dir = tempfile::tempdir().expect("tempdir");
1263 let error = read_source(
1264 dir.path(),
1265 &reflecting(&format!(
1266 r#"{TAG_API}
1267
1268class "Thing" (
1269 inherits = </Typed>
1270 customData = {{ token[] reflectedAPISchemas = ["TagAPI"] }}
1271) {{}}"#
1272 )),
1273 )
1274 .expect_err("the schema is not applied");
1275
1276 match error {
1277 Error::Definition {
1278 violation: Violation::ReflectedNotApplied { schema },
1279 ..
1280 } => assert_eq!(schema.as_str(), "TagAPI"),
1281 other => panic!("{other}"),
1282 }
1283 }
1284
1285 #[test]
1289 fn reflected_multi_apply_warns() {
1290 let dir = tempfile::tempdir().expect("tempdir");
1291 let library = read_source(
1292 dir.path(),
1293 &reflecting(
1294 r#"class "MultiAPI" (
1295 inherits = </APISchemaBase>
1296 customData = {
1297 token apiSchemaType = "multipleApply"
1298 token propertyNamespacePrefix = "multi"
1299 }
1300) {
1301 int depth = 0
1302}
1303
1304class "Thing" (
1305 inherits = </Typed>
1306 prepend apiSchemas = ["MultiAPI:foo"]
1307 customData = { token[] reflectedAPISchemas = ["MultiAPI"] }
1308) {}"#,
1309 ),
1310 )
1311 .expect("set aside, not refused");
1312 let warnings = check(&library).expect("set aside, not refused");
1313
1314 assert!(
1315 warnings
1316 .iter()
1317 .any(|warning| warning.contains("`MultiAPI` is a multiple-apply API schema, so it is not reflected")),
1318 "{warnings:?}"
1319 );
1320 let thing = library.find(&tf::Token::new("Thing")).expect("the class");
1321 assert!(matches!(
1322 thing.reflections(&library).as_slice(),
1323 [Reflection::NotSingleApply(schema)] if schema.identifier.as_str() == "MultiAPI"
1324 ));
1325 }
1326
1327 #[test]
1330 fn reflected_duplicate_warns() {
1331 let dir = tempfile::tempdir().expect("tempdir");
1332 let library = read_source(
1333 dir.path(),
1334 &reflecting(&format!(
1335 r#"{TAG_API}
1336
1337class "LabelAPI" (
1338 inherits = </APISchemaBase>
1339 customData = {{ token apiSchemaType = "singleApply" }}
1340) {{
1341 string label = ""
1342 int dup = 0
1343}}
1344
1345class "Thing" (
1346 inherits = </Typed>
1347 prepend apiSchemas = ["TagAPI", "LabelAPI"]
1348 customData = {{ token[] reflectedAPISchemas = ["TagAPI", "LabelAPI"] }}
1349) {{}}"#
1350 )),
1351 )
1352 .expect("the first keeps the name");
1353 let warnings = check(&library).expect("the first keeps the name");
1354
1355 assert!(
1356 warnings
1357 .iter()
1358 .any(|warning| warning.contains("`dup` of `LabelAPI` is not reflected; `TagAPI` already declares it")),
1359 "{warnings:?}"
1360 );
1361
1362 let thing = library.find(&tf::Token::new("Thing")).expect("the class");
1364 let taken: Vec<String> = thing
1365 .reflections(&library)
1366 .iter()
1367 .map(|reflection| match reflection {
1368 Reflection::Taken {
1369 schema,
1370 properties,
1371 shadowed,
1372 } => {
1373 let names: Vec<&str> = properties.iter().map(|p| p.name.as_str()).collect();
1374 let shadowed: Vec<String> = shadowed
1375 .iter()
1376 .map(|(p, first)| format!("{} by {first}", p.name))
1377 .collect();
1378 format!("{} takes {names:?}, shadowed {shadowed:?}", schema.identifier)
1379 }
1380 other => panic!("{other:?}"),
1381 })
1382 .collect();
1383 assert_eq!(
1384 taken,
1385 vec![
1386 r#"TagAPI takes ["tag", "dup"], shadowed []"#,
1387 r#"LabelAPI takes ["label"], shadowed ["dup by TagAPI"]"#,
1388 ]
1389 );
1390 }
1391
1392 #[test]
1394 fn type_name_mismatch() {
1395 assert!(matches!(
1396 violation("schemaFail5.usda"),
1397 Violation::TypeNameMismatch { .. }
1398 ));
1399 }
1400
1401 #[test]
1404 fn concrete_needs_typed() {
1405 assert!(matches!(violation("schemaFail6.usda"), Violation::ConcreteNotTyped));
1406 assert!(matches!(violation("schemaFail7.usda"), Violation::ConcreteNotTyped));
1407 }
1408
1409 #[test]
1417 fn applied_api_base() {
1418 for fixture in [
1419 "schemaFail3.usda",
1420 "schemaFail4.usda",
1421 "schemaFail8.usda",
1422 "schemaFail10.usda",
1423 "schemaFail11.usda",
1424 ] {
1425 assert!(
1426 matches!(violation(fixture), Violation::AppliedApiNotRooted { .. }),
1427 "{fixture}"
1428 );
1429 }
1430 }
1431
1432 #[test]
1435 fn non_applied_base() {
1436 assert!(matches!(
1437 violation("schemaFail9.usda"),
1438 Violation::NonAppliedApiNotRooted { .. }
1439 ));
1440 }
1441
1442 #[test]
1446 fn typed_cannot_be_api() {
1447 let dir = tempfile::tempdir().expect("tempdir");
1448 let error = read_source(
1449 dir.path(),
1450 r#"#usda 1.0
1451
1452over "GLOBAL" (
1453 customData = {
1454 string libraryName = "testTypedApi"
1455 }
1456)
1457{
1458}
1459
1460class "Typed" {}
1461
1462class Shape "Shape" (
1463 inherits = </Typed>
1464 customData = { token apiSchemaType = "singleApply" }
1465) {}
1466"#,
1467 )
1468 .expect_err("a schema cannot be both");
1469 assert!(
1470 matches!(
1471 error,
1472 Error::Definition {
1473 violation: Violation::ApiSchemaTypeOnTyped,
1474 ..
1475 }
1476 ),
1477 "{error}"
1478 );
1479 }
1480
1481 #[test]
1485 fn silent_base_is_not_non_applied() {
1486 let dir = tempfile::tempdir().expect("tempdir");
1487 let error = read_source(
1488 dir.path(),
1489 r#"#usda 1.0
1490
1491over "GLOBAL" (
1492 customData = {
1493 string libraryName = "testSilentBase"
1494 }
1495)
1496{
1497}
1498
1499class "APISchemaBase" {}
1500
1501class "SilentAPI" (
1502 inherits = </APISchemaBase>
1503) {}
1504
1505class "NonAppliedAPI" (
1506 inherits = </SilentAPI>
1507 customData = { token apiSchemaType = "nonApplied" }
1508) {}
1509"#,
1510 )
1511 .expect_err("the base is single-apply by default");
1512 assert!(
1513 matches!(
1514 error,
1515 Error::Definition {
1516 violation: Violation::NonAppliedApiNotRooted { .. },
1517 ..
1518 }
1519 ),
1520 "{error}"
1521 );
1522 }
1523
1524 #[test]
1528 fn schematics_names_collide() {
1529 let dir = tempfile::tempdir().expect("tempdir");
1530 let error = read_source(
1531 dir.path(),
1532 r#"#usda 1.0
1533
1534over "GLOBAL" (
1535 customData = {
1536 string libraryName = "testCollide"
1537 }
1538)
1539{
1540}
1541
1542class "APISchemaBase" {}
1543
1544class "CollideAPI" (
1545 inherits = </APISchemaBase>
1546 customData = {
1547 token apiSchemaType = "multipleApply"
1548 token propertyNamespacePrefix = "test"
1549 }
1550) {
1551 int foo
1552 int test:__INSTANCE_NAME__:foo
1553}
1554"#,
1555 )
1556 .expect_err("both properties reach one schematics name");
1557 assert!(
1558 matches!(
1559 error,
1560 Error::Definition {
1561 violation: Violation::DuplicateSchematicsName { .. },
1562 ..
1563 }
1564 ),
1565 "{error}"
1566 );
1567 }
1568
1569 #[test]
1578 fn accepted_upstream_failures() {
1579 for fixture in ["schemaFail.usda", "schemaFail2.usda"] {
1580 assert!(
1581 read_fixture(fixture).is_ok(),
1582 "{fixture} is now rejected; move it to a rule test and drop it from this list"
1583 );
1584 }
1585 }
1586
1587 #[test]
1594 fn suffix_is_a_warning() {
1595 let dir = tempfile::tempdir().expect("tempdir");
1596 let library = read_source(
1597 dir.path(),
1598 r#"#usda 1.0
1599
1600over "GLOBAL" (
1601 customData = {
1602 string libraryName = "testConvention"
1603 }
1604)
1605{
1606}
1607
1608class "APISchemaBase" {}
1609
1610class "Applied" (
1611 inherits = </APISchemaBase>
1612 customData = { token apiSchemaType = "singleApply" }
1613) {}
1614"#,
1615 )
1616 .expect("nothing here breaks a rule");
1617 let warnings = check(&library).expect("nothing here breaks a rule");
1618
1619 assert!(
1620 warnings
1621 .iter()
1622 .any(|warning| warning.contains("conventionally ends in")),
1623 "{warnings:?}"
1624 );
1625 }
1626}