Skip to main content

openusd_build/
validate.rs

1//! The rules a schema has to satisfy to be generated from.
2//!
3//! Two lists, kept apart because they answer different questions. A schema
4//! rule says the definition would not mean what it says — a stage composed
5//! against our schematics would behave differently from one composed against
6//! C++'s. A generator rule says the Rust we would emit does not compile or is
7//! ambiguous. Every rule states that harm; that upstream rejects something is
8//! not on its own a reason to.
9//!
10//! Anything a schema author would want to know that does not make the output
11//! wrong is a warning instead, returned for a build script to print.
12
13use 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/// A rule a schema broke.
21///
22/// One variant per rule enforced, not one per upstream check: two upstream
23/// checks that mean one thing here share a variant, and a check not kept has
24/// none. Tests match variants rather than message text.
25#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum Violation {
28    /// A base no layer in the stack declares.
29    #[error("inherits from `{name}`, which no layer declares")]
30    MissingBase {
31        /// The name that could not be found.
32        name: tf::Token,
33    },
34
35    /// An inheritance chain that returns to where it started, which has no
36    /// order to generate in.
37    #[error("inherits from itself through {chain}")]
38    CyclicInheritance {
39        /// The cycle, in the order it was walked.
40        chain: String,
41    },
42
43    /// More than one base. A schema is a single chain, and the registry
44    /// resolves `is_a` along one.
45    #[error("inherits from {count} classes; a schema has at most one base")]
46    MultipleBases {
47        /// How many were authored.
48        count: usize,
49    },
50
51    /// An applied API schema that does not inherit directly from
52    /// `APISchemaBase`.
53    ///
54    /// Applying a schema brings its own properties and the API schemas it
55    /// lists, not its bases' — an API schema's inheritance is not walked the
56    /// way a typed schema's is — so a chain here would promise properties that
57    /// applying never delivers.
58    #[error("an applied API schema must inherit directly from APISchemaBase, not from `{base}`")]
59    AppliedApiNotRooted {
60        /// The base it inherits instead.
61        base: tf::Token,
62    },
63
64    /// A non-applied API schema on a base that is neither `APISchemaBase` nor
65    /// another non-applied API schema, whose properties it would appear to
66    /// offer and could never carry.
67    #[error("a non-applied API schema inherits APISchemaBase or another non-applied API, not `{base}`")]
68    NonAppliedApiNotRooted {
69        /// The base it inherits instead.
70        base: tf::Token,
71    },
72
73    /// A schema inheriting inside its own family, which the registry reads as
74    /// one schema at two versions rather than two schemas.
75    #[error("inherits from `{base}`, which is the same schema family")]
76    InheritsWithinFamily {
77        /// The base sharing this schema's family.
78        base: tf::Token,
79    },
80
81    /// An `apiSchemaType` that is none of the three kinds.
82    #[error("unknown apiSchemaType `{spelling}`")]
83    UnknownApiSchemaType {
84        /// What the schema wrote.
85        spelling: String,
86    },
87
88    /// Metadata that contradicts the kind of schema it sits on.
89    #[error("{key} does not apply to a {kind} schema")]
90    KindMismatch {
91        /// The key that does not belong, whether the schema wrote it as prim
92        /// metadata or under `customData`.
93        key: &'static str,
94        /// The kind it was found on.
95        kind: &'static str,
96    },
97
98    /// A concrete schema that is not typed, so a stage could never instantiate
99    /// it.
100    #[error("a concrete schema must inherit from Typed")]
101    ConcreteNotTyped,
102
103    /// A schema that is typed, or carries a type name, and also declares
104    /// itself an API schema.
105    ///
106    /// The two cannot both hold: `apiSchemaType` is what makes a schema an API
107    /// schema, and an API schema is never a prim type. Left alone the API
108    /// reading wins and the type name never reaches the schematics, so a stage
109    /// could no longer instantiate the prim type the schema meant to declare.
110    #[error("a typed schema cannot declare apiSchemaType")]
111    ApiSchemaTypeOnTyped,
112
113    /// An identifier the registry would refuse to register, and so could never
114    /// look up.
115    #[error("`{identifier}` is not an allowed schema identifier")]
116    DisallowedIdentifier {
117        /// The identifier as declared.
118        identifier: tf::Token,
119    },
120
121    /// A `typeName` that is not the class's own identifier. It reaches the
122    /// schematics, so a stage would compose the wrong type.
123    #[error("typeName `{authored}` is not the schema's own identifier")]
124    TypeNameMismatch {
125        /// The authored type name.
126        authored: tf::Token,
127    },
128
129    /// An `apiSchemas` opinion that is not a prepend. Only a prepend composes
130    /// as the class's built-in applied API schemas.
131    ///
132    /// Which kinds may declare built-ins at all is deliberately not a rule
133    /// here: the upstream corpus's own `PublicMultipleApplyAPI` declares them
134    /// on a multiple-apply schema, so the shape is legal however it reads.
135    #[error("apiSchemas must be authored as a prepend")]
136    ApiSchemasNotPrepended,
137
138    /// Two properties that reach one name in the schematics, where one spec
139    /// cannot hold both.
140    ///
141    /// The check is on the resolved name: a multiple-apply schema's property
142    /// picks up its namespace prefix and the instance-name template on the way
143    /// there, so two names distinct at the source can arrive as one.
144    #[error("`{first}` and `{second}` both reach `{name}` in the schematics")]
145    DuplicateSchematicsName {
146        /// The name they collided on.
147        name: tf::Token,
148        /// The first property to reach it.
149        first: tf::Token,
150        /// The second.
151        second: tf::Token,
152    },
153
154    /// An attribute whose type no registered value type names, leaving its
155    /// accessor nothing to read.
156    #[error("property `{property}` has unknown type `{type_name}`")]
157    UnknownPropertyType {
158        /// The property.
159        property: tf::Token,
160        /// The type as declared.
161        type_name: tf::Token,
162    },
163
164    /// A multiple-apply schema with properties but no namespace prefix, or a
165    /// prefix and no properties. The prefix is what keeps one instance's
166    /// properties apart from another's.
167    #[error("a multiple-apply schema needs propertyNamespacePrefix exactly when it has properties")]
168    MultipleApplyPrefix,
169
170    /// A field the registry would refuse to carry, so it would be silently
171    /// dropped rather than mean anything.
172    #[error("`{field}` is not a field a schema may declare")]
173    DisallowedField {
174        /// The field name.
175        field: String,
176    },
177
178    /// A property this class records as an API schema override that an ancestor
179    /// declares outright.
180    ///
181    /// An override reaches a prim definition only where a built-in API schema
182    /// supplies the property, so this would delete the ancestor's property from
183    /// the class wherever none does. The other direction is allowed: a class may
184    /// make an inherited override a declaration of its own.
185    #[error("`{property}` is an API schema override here, but `{class}` declares it outright")]
186    OverridesADeclaration {
187        /// The property.
188        property: tf::Token,
189        /// The ancestor declaring it outright.
190        class: tf::Token,
191    },
192
193    /// A redeclaration that changes what a property is, rather than what it
194    /// falls back to.
195    ///
196    /// An accessor is generated where a property is introduced and every class
197    /// below reaches that one, so its creator authors the type, variability and
198    /// `custom` composed there. A redeclaration changing any of them would have
199    /// that creator author a property the redeclaring class's own definition
200    /// contradicts. A fallback is not on the list: changing one is what a
201    /// redeclaration is for.
202    #[error("`{property}` is {} on `{class}`", redeclared_as(.ours, .theirs))]
203    IncompatibleRedeclaration {
204        /// The property.
205        property: tf::Token,
206        /// The ancestor it disagrees with.
207        class: tf::Token,
208        /// What the property is at the ancestor. Boxed with `ours`, so the
209        /// error a build stops on stays as small as every other.
210        theirs: Box<Shape>,
211        /// What this class made it.
212        ours: Box<Shape>,
213    },
214
215    /// Two token sources reaching one identifier with different values, where
216    /// one constant would have to hold both strings.
217    #[error("token `{id}` would hold both \"{first}\" and \"{second}\"")]
218    TokenValueCollision {
219        /// The identifier they collided on.
220        id: String,
221        /// The string the first source gave it.
222        first: String,
223        /// The string the second gave it.
224        second: String,
225    },
226
227    /// Two classes of one library reaching one Rust name, where one module
228    /// cannot hold both.
229    ///
230    /// The scope is the module: two libraries may each declare a `Sphere`, and
231    /// nothing stops them.
232    #[error("`{first}` and `{second}` both reach the name {name}")]
233    RustNameCollision {
234        /// The name they collided on.
235        name: String,
236        /// The first schema to reach it.
237        first: String,
238        /// The second.
239        second: String,
240    },
241
242    /// Two properties reaching one method name, where two methods of one name
243    /// — or two traits offering one — make every call ambiguous.
244    #[error("`{first}` and `{second}` both reach the method {method}")]
245    MethodCollision {
246        /// The method they collided on.
247        method: String,
248        /// The first property to reach it.
249        first: tf::Token,
250        /// The second.
251        second: tf::Token,
252    },
253
254    /// A name a schema chose — through `className` or `apiName` — that is not a
255    /// Rust identifier, so nothing could be called it.
256    #[error("`{name}` is not an identifier, so nothing generated can be called it")]
257    NotAnIdentifier {
258        /// The name as the schema wrote it.
259        name: String,
260    },
261
262    /// A class this run does not generate and that belongs to no other library,
263    /// so no view of it exists for a descendant to derive from or for a class
264    /// to reflect.
265    #[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        /// The class that has no views.
270        class: tf::Token,
271    },
272
273    /// A reflected API schema no layer in the stack declares.
274    #[error("reflects `{schema}`, which no layer declares")]
275    UnknownReflectedSchema {
276        /// The name that could not be found.
277        schema: tf::Token,
278    },
279
280    /// A reflected API schema the class does not apply.
281    ///
282    /// Reflecting says every prim of this type carries the schema's properties,
283    /// which only applying the schema makes true; a reflected accessor would
284    /// otherwise reach a property the prim's definition does not have. A
285    /// multiple-apply class records its applied schemas as instance-name
286    /// templates, which no reflected name matches, so it cannot reflect.
287    #[error("`{schema}` is reflected but not applied; add it to `prepend apiSchemas`")]
288    ReflectedNotApplied {
289        /// The schema reflected without being applied.
290        schema: tf::Token,
291    },
292
293    /// A class in a library this run does not generate and was not told where
294    /// to find, so its views cannot be named: a base to derive from, or a
295    /// schema to reflect.
296    #[error("`{class}` belongs to the {library} library; name where its views live with Builder::extern_library")]
297    UnknownLibrary {
298        /// The library declaring the class.
299        library: String,
300        /// The class that could not be reached.
301        class: tf::Token,
302    },
303
304    /// A library was told where its views live, but not in a spelling Rust can
305    /// read, so nothing can be named through it.
306    #[error("the {library} library's views were placed at `{path}`, which is no Rust path")]
307    UnreadableLibraryPath {
308        /// The library the path was given for.
309        library: String,
310        /// What was given.
311        path: String,
312    },
313
314    /// An `apiGetImplementation` that is neither spelling.
315    #[error("unknown apiGetImplementation `{spelling}`")]
316    UnknownApiGetImplementation {
317        /// What the schema wrote.
318        spelling: String,
319    },
320
321    /// A custom read accessor asked for on a property that has no accessor,
322    /// which cannot both be true.
323    #[error("`{property}` asks for a custom accessor but generates none")]
324    CustomGetWithoutAccessor {
325        /// The property.
326        property: tf::Token,
327    },
328
329    /// An attribute whose type the core reads but gives no constant to declare
330    /// it with, so nothing could author the property.
331    #[error("`{property}` is a `{type_name}`, which has no value-type constant to declare it with")]
332    UnnameableType {
333        /// The property.
334        property: tf::Token,
335        /// The type it was declared as.
336        type_name: tf::Token,
337    },
338}
339
340/// Checks every rule against a resolved library, returning what a schema
341/// author should know but that does not make the output wrong.
342pub 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
358/// The identifier and the type name that reach the registry.
359fn 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
377/// Metadata that has to agree with the kind of schema carrying it.
378fn check_kind(class: &Class) -> Result<(), Error> {
379    let kind = class.kind;
380    let metadata = &class.metadata;
381
382    // Auto-apply names one schema to apply, which a multiple-apply schema
383    // cannot be without an instance name to apply it under.
384    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        // An order names properties as the schema declared them, and a
407        // multiple-apply schema's reach the schematics under an instance-name
408        // template instead — so the order it asked for would name properties
409        // the schema data does not have.
410        (
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    // A concrete schema is one a stage instantiates, which only a typed schema
444    // can be.
445    if kind == usd::SchemaKind::ConcreteTyped && !class.is_typed {
446        return Err(class.violation(Violation::ConcreteNotTyped));
447    }
448
449    Ok(())
450}
451
452/// The shape of the inheritance chain.
453fn 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    // Any ancestor of this schema's own family makes the chain read as one
461    // schema at two versions, not as two schemas.
462    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    // A class inheriting nothing is rooted at SchemaBase, which every API
476    // schema needs APISchemaBase below. Only the root every schema derives from
477    // has no base at all, and naming it is what makes the diagnostic read for a
478    // class that claims to be an API schema anyway.
479    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    // A non-applied API schema may sit on APISchemaBase or on another
487    // non-applied one, wherever that base is declared. Only a base that is one
488    // counts: an API schema that declares no kind at all is single-apply by
489    // default, so silence is not permission.
490    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
503/// Fields a schematics carries, on the class prim and on every property.
504///
505/// `inheritPaths`, `customData` and `specifier` are the generator's own input
506/// rather than schema data, so they are expected here and never written out.
507fn 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
526/// Every property's type, name and accessor metadata.
527fn 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        // An override reaches a prim definition only where a built-in API
563        // schema supplies the property, so a class that turns an inherited
564        // declaration into one deletes it from itself wherever none does. The
565        // other direction is fine, and a class may make an inherited override
566        // a declaration of its own.
567        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        // A redeclaration may change what the property falls back to and
580        // nothing else: the accessor a caller reaches is the introducing
581        // class's, whose creator authors the shape composed there. Reported
582        // against this class's own declaration, which is the one to fix.
583        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
602/// What comes of the API schemas a class names to reflect: what a schema
603/// author should know about the ones set aside, and the one that is wrong.
604///
605/// The model decides each schema's fate ([`Class::reflections`]); this only
606/// phrases it. A schema set aside is a warning, as upstream prints and goes on,
607/// and the corpus relies on that: it reflects a multiple-apply schema it
608/// applies under an instance name.
609fn 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            // Reflecting says every prim of this type carries the schema's
629            // properties, which only applying the schema makes true.
630            Reflection::NotApplied(schema) => {
631                return Err(class.violation(Violation::ReflectedNotApplied {
632                    schema: schema.identifier.clone(),
633                }));
634            }
635        }
636    }
637    Ok(())
638}
639
640/// What a schema author should know, where the output is still correct.
641fn 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    // The registry never reads this, so it is a convention rather than a rule.
647    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
662/// The first field a schematics would refuse to carry, if any.
663///
664/// The generator's own input is expected and never written out: what a class
665/// inherits, its `customData`, its specifier, and the two children keys naming
666/// the prims and properties it declares.
667///
668/// The other children keys are not on that list. A class prim carrying a
669/// variant set is a shape the schematics has no way to record, and letting the
670/// key through would drop the variant's content without a word.
671fn 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
684/// How a redeclaration reads in its diagnostic: the first thing `ours` and
685/// `theirs` disagree on, this class's reading first, as in "`double` here and
686/// `int`".
687fn 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
698/// Each thing a redeclaration is held to, as a diagnostic reads it.
699fn 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
723/// How a schema kind reads in a diagnostic.
724fn 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    /// This class's declaration, with the rule it broke.
737    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    /// This property's declaration, with the rule it broke.
747    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    /// The rule a fixture is rejected for.
761    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    /// A multiple-apply schema needs its namespace prefix exactly when it has
770    /// properties to put under it.
771    #[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    /// Metadata that contradicts the kind of schema carrying it.
778    #[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    /// Only a prepend composes as a class's built-in applied API schemas.
796    #[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    /// A schema inheriting inside its own family reads as one schema at two
809    /// versions rather than two schemas.
810    #[test]
811    fn same_family_base() {
812        assert!(matches!(
813            violation("schemaFail21.usda"),
814            Violation::InheritsWithinFamily { .. }
815        ));
816    }
817
818    /// An identifier the registry would refuse, and a field a schematics does
819    /// not carry.
820    #[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    /// A variant set on a class prim is a shape the schematics cannot record,
833    /// and the children key naming it is not the generator input the two keys
834    /// beside it are — so it is refused rather than quietly dropped along with
835    /// everything the variant declares.
836    #[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    /// A composition arc is a field the registry refuses too, and it has to be
878    /// caught on the class prim rather than on the composed result: flattening
879    /// is what resolves an arc away, so by then there is nothing left to see.
880    #[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    /// The two override schemas the corpus keeps commented out, because
915    /// upstream rejects them for the reason this rule states.
916    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    /// A class may not turn a property an ancestor declares outright into an
946    /// API schema override, which would delete it from this class wherever no
947    /// built-in API schema supplies it.
948    #[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    /// The reverse is allowed, and is what the corpus's own
966    /// `overrideBaseTrueDerivedFalse` does: a class may make an inherited
967    /// override a declaration of its own.
968    #[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    /// An override travels to a class that says nothing about the property:
1013    /// the strongest declaration decides, and for a class that redeclares
1014    /// nothing that is the ancestor's.
1015    #[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    /// A base declaring one property, and a class deriving from it that
1061    /// redeclares the same property.
1062    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    /// What a redeclaration was refused for: the ancestor, what the property is
1082    /// there, and what this class made it.
1083    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    /// A redeclaration that changes the type, which the introducing class's
1105    /// creator would go on authoring as it was.
1106    #[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    /// Spelling `uniform` on a property the base left varying changes what
1115    /// composes, and so is refused.
1116    #[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    /// So does making an inherited property `custom`.
1124    #[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    /// The diagnostic spells the first thing the two shapes disagree on, this
1132    /// class's reading first.
1133    #[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    /// Redeclaring an attribute as a relationship never reaches the rule:
1166    /// composition refuses a property whose specs disagree on what kind it is
1167    /// before anything is resolved from the stack.
1168    #[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    /// A different accessor name does not excuse the change: the ancestor's
1180    /// creator is still reachable on the derived view.
1181    #[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    /// Changing the fallback is what a redeclaration is for.
1194    #[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    /// A redeclaration that leaves `uniform` unspelled composes to uniform
1205    /// through the base, so the property is what it was and is accepted (see
1206    /// [`Shape`]).
1207    #[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    /// A library of the roots and whatever `classes` declare.
1229    fn reflecting(classes: &str) -> String {
1230        schema("testReflect", classes)
1231    }
1232
1233    /// Reflecting a name nothing declares is a mistake the stack reports, not
1234    /// a schema quietly left out.
1235    #[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    /// Reflecting promises the schema's properties on every prim of the type,
1259    /// which only applying the schema keeps.
1260    #[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    /// A multiple-apply schema has no one set of properties to take, so it is
1286    /// set aside with a word rather than refused: the corpus does exactly
1287    /// this, applying it under an instance name.
1288    #[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    /// Two reflected schemas declaring one name: the first keeps it, the
1328    /// second's is set aside with a word, and the class takes the rest.
1329    #[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        // Each schema, with the names taken from it and the ones set aside.
1363        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    /// A `typeName` reaches the schematics, so it has to be the schema's own.
1393    #[test]
1394    fn type_name_mismatch() {
1395        assert!(matches!(
1396            violation("schemaFail5.usda"),
1397            Violation::TypeNameMismatch { .. }
1398        ));
1399    }
1400
1401    /// Only a typed schema can be instantiated, so only a typed schema can be
1402    /// concrete.
1403    #[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    /// An applied API schema inherits `APISchemaBase` and nothing else, since
1410    /// applying one brings no base's properties with it.
1411    ///
1412    /// Upstream rejects `schemaFail3` and `schemaFail4` for declaring a
1413    /// property twice, and `schemaFail8` for a family name not ending in
1414    /// `API`. All three are rejected here as well, by this rule instead: none
1415    /// of them inherits `APISchemaBase`.
1416    #[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    /// A non-applied API schema cannot inherit an applied one, whose
1433    /// properties it would appear to offer and could never carry.
1434    #[test]
1435    fn non_applied_base() {
1436        assert!(matches!(
1437            violation("schemaFail9.usda"),
1438            Violation::NonAppliedApiNotRooted { .. }
1439        ));
1440    }
1441
1442    /// A typed schema that also calls itself an API schema is rejected rather
1443    /// than quietly generated as the API schema, which would drop the prim
1444    /// type it meant to declare.
1445    #[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    /// A base that declares no kind is single-apply, so a non-applied schema
1482    /// may not sit on it — saying nothing must not buy what saying
1483    /// `singleApply` is refused.
1484    #[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    /// Two properties of a multiple-apply schema can be distinct at the source
1525    /// and arrive as one name in the schematics, where one spec cannot hold
1526    /// both and the second would silently replace the first.
1527    #[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    /// The upstream failure files this crate accepts, and why.
1570    ///
1571    /// Both turn on a name two tokens would reach once converted to a Rust
1572    /// identifier: an `allowedTokens` value colliding with a library token.
1573    /// That is a generator rule, which the name stage owes and which nothing
1574    /// here can check yet. The list is the decision on the record: a file
1575    /// leaving it means a rule arrived, and a file joining it needs a reason
1576    /// written here.
1577    #[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    /// A schema whose name breaks the API-suffix convention is still
1588    /// generated, and says so. The registry never reads that convention, so
1589    /// nothing about the output is wrong.
1590    ///
1591    /// The upstream corpus keeps the convention throughout, and the one file
1592    /// that breaks it breaks a rule as well, so this writes its own schema.
1593    #[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}