kotlin_codegen/validate.rs
1//! Checking a [`KtFile`] before it is written.
2//!
3//! A *program* builds the declaration model, so the mistakes are program
4//! mistakes: a name derived from a Rust field that happens to be `object`, two
5//! generated classes that mangle to the same name. Without a check here those
6//! surface much later, when the Kotlin compiler runs — often in a different
7//! build, against a generated file that gives no hint about what produced it.
8//!
9//! Two rules bound what belongs here:
10//!
11//! 1. **Only what the model proves.** No type resolution, no parsing of the raw
12//! text inside [`KtCode`](super::KtCode) bodies. Whether `io.zenoh.ZSession`
13//! exists is the compiler's question, not ours.
14//! 2. **No false positives.** A check that occasionally fires on correct output
15//! is worse than no check, because the first false alarm teaches people to
16//! switch the checks off.
17//!
18//! Validation is a **separate read-only pass**: the model stays a plain data
19//! structure, the builders stay infallible, and
20//! [`render`](super::KtFile::render) stays infallible too — rendering a model
21//! you already know is broken is exactly what you want while debugging a
22//! generator. The checks run on the *write* path instead
23//! ([`merge_files`](super::merge_files), [`write_files`](super::write_files)).
24
25use std::collections::{BTreeMap, BTreeSet};
26
27use super::{
28 ident::{is_valid_kotlin_package, is_writable_kotlin_ident},
29 model::{
30 KtBody, KtClass, KtClassKind, KtClassModifier, KtCompanion, KtCtorParam, KtDecl, KtFile,
31 KtFun, KtParam,
32 },
33 slot::KtPropertyValue,
34};
35
36/// Which check produced a [`Diagnostic`].
37///
38/// Consumers match on this to override a severity, so it is the stable name of
39/// a check rather than its message.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[non_exhaustive]
42pub enum Check {
43 /// A declared name is not something Kotlin can write.
44 InvalidIdentifier,
45 /// A file's package is not a legal Kotlin package path.
46 InvalidPackage,
47 /// Two type declarations in one scope share a name — classes, `fun
48 /// interface`s and type aliases all live in Kotlin's *classifier*
49 /// namespace.
50 DuplicateType,
51 /// Two value declarations in one scope share a name — properties and
52 /// `val`/`var` constructor parameters.
53 DuplicateValue,
54 /// Two functions in one scope share a name *and* a parameter type list.
55 /// Same-named functions with different parameters are overloads and pass.
56 DuplicateFunction,
57 /// Two `Raw` blocks in one scope share a name. Identical ones are merged
58 /// rather than reported (see [`merge_files`](super::merge_files)), so this
59 /// means two different blocks are claiming one identity.
60 DuplicateRaw,
61 /// A property has no type, no value and no accessors — `val x` alone,
62 /// which is never legal.
63 PropertyWithoutTypeOrValue,
64 /// An `enum class` declares primary-constructor parameters, but an entry
65 /// passes no arguments.
66 EnumEntryMissingArguments,
67 /// A function has no body somewhere a bodiless function is not allowed.
68 FunctionWithoutBody,
69 /// Two distinct FQNs referenced from raw text share a simple name, so the
70 /// text cannot be made to refer to both.
71 ImportCollision,
72}
73
74impl Check {
75 /// Every check, so a policy can be built over all of them. Adding a check
76 /// extends this list, which is what keeps
77 /// [`ValidationPolicy::warn_all`] correct as the set grows.
78 pub const ALL: &'static [Check] = &[
79 Check::InvalidIdentifier,
80 Check::InvalidPackage,
81 Check::DuplicateType,
82 Check::DuplicateValue,
83 Check::DuplicateFunction,
84 Check::DuplicateRaw,
85 Check::PropertyWithoutTypeOrValue,
86 Check::EnumEntryMissingArguments,
87 Check::FunctionWithoutBody,
88 Check::ImportCollision,
89 ];
90
91 /// The stable, greppable name of this check.
92 pub fn name(self) -> &'static str {
93 match self {
94 Check::InvalidIdentifier => "invalid-identifier",
95 Check::InvalidPackage => "invalid-package",
96 Check::DuplicateType => "duplicate-type",
97 Check::DuplicateValue => "duplicate-value",
98 Check::DuplicateFunction => "duplicate-function",
99 Check::DuplicateRaw => "duplicate-raw",
100 Check::PropertyWithoutTypeOrValue => "property-without-type-or-value",
101 Check::EnumEntryMissingArguments => "enum-entry-missing-arguments",
102 Check::FunctionWithoutBody => "function-without-body",
103 Check::ImportCollision => "import-collision",
104 }
105 }
106}
107
108impl std::fmt::Display for Check {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.write_str(self.name())
111 }
112}
113
114/// How much a diagnostic matters. A [`Check`] set to neither is off.
115#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
116pub enum Severity {
117 /// Reported, but does not stop generation.
118 Warning,
119 /// Stops generation.
120 Error,
121}
122
123impl std::fmt::Display for Severity {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.write_str(match self {
126 Severity::Warning => "warning",
127 Severity::Error => "error",
128 })
129 }
130}
131
132/// One problem found in a [`KtFile`].
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Diagnostic {
135 pub check: Check,
136 pub severity: Severity,
137 /// Where the problem is, as a path through the declaration tree —
138 /// `io.zenoh.jni.session/ZSession/Companion`. The model has no source
139 /// positions, so this is how a diagnostic is located.
140 pub scope: String,
141 pub message: String,
142}
143
144impl std::fmt::Display for Diagnostic {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(
147 f,
148 "{} [{}] in `{}`: {}",
149 self.severity, self.check, self.scope, self.message
150 )
151 }
152}
153
154/// Per-check severity, so a consumer can downgrade or disable a check instead
155/// of pinning an old version of this crate when one misfires.
156///
157/// Every check is an error by default.
158///
159/// ```
160/// use kotlin_codegen::{Check, Severity, ValidationPolicy};
161/// let policy = ValidationPolicy::new().warn(Check::DuplicateType);
162/// assert_eq!(
163/// policy.severity_of(Check::DuplicateType),
164/// Some(Severity::Warning)
165/// );
166/// ```
167#[derive(Clone, Debug, Default)]
168pub struct ValidationPolicy {
169 /// Only the overrides; anything absent is [`Severity::Error`].
170 overrides: BTreeMap<Check, Option<Severity>>,
171}
172
173impl ValidationPolicy {
174 /// Every check an error.
175 pub fn new() -> Self {
176 Self::default()
177 }
178
179 /// Report `check` but keep generating.
180 pub fn warn(mut self, check: Check) -> Self {
181 self.overrides.insert(check, Some(Severity::Warning));
182 self
183 }
184
185 /// Stop generating on `check` (the default).
186 pub fn deny(mut self, check: Check) -> Self {
187 self.overrides.insert(check, Some(Severity::Error));
188 self
189 }
190
191 /// Downgrade *every* check to a warning.
192 ///
193 /// The way to adopt validation in a generator that already produces
194 /// output: run it in warning mode first, look at what comes back, and
195 /// switch to the default once it is quiet. Reaching a clean run and then
196 /// dropping this call is much less disruptive than having a build start
197 /// failing on output that was fine yesterday.
198 ///
199 /// ```
200 /// use kotlin_codegen::{Check, Severity, ValidationPolicy};
201 /// let policy = ValidationPolicy::warn_all();
202 /// for check in Check::ALL {
203 /// assert_eq!(policy.severity_of(*check), Some(Severity::Warning));
204 /// }
205 /// ```
206 pub fn warn_all() -> Self {
207 let mut policy = Self::new();
208 for check in Check::ALL {
209 policy = policy.warn(*check);
210 }
211 policy
212 }
213
214 /// Turn `check` off entirely.
215 pub fn allow(mut self, check: Check) -> Self {
216 self.overrides.insert(check, None);
217 self
218 }
219
220 /// The effective severity of `check`, or `None` when it is off.
221 pub fn severity_of(&self, check: Check) -> Option<Severity> {
222 self.overrides
223 .get(&check)
224 .copied()
225 .unwrap_or(Some(Severity::Error))
226 }
227}
228
229/// A child scope path. The root package is the empty string, so joining
230/// naively would yield a leading `/` and make diagnostics inconsistent between
231/// the root package and every other one.
232fn scope_join(scope: &str, child: &str) -> String {
233 if scope.is_empty() {
234 child.to_string()
235 } else {
236 format!("{scope}/{child}")
237 }
238}
239
240/// Collects diagnostics under the active policy, dropping the ones that are
241/// switched off so no check pays for a report nobody wants.
242pub(crate) struct Diagnostics<'a> {
243 policy: &'a ValidationPolicy,
244 out: Vec<Diagnostic>,
245}
246
247impl<'a> Diagnostics<'a> {
248 pub(crate) fn new(policy: &'a ValidationPolicy) -> Self {
249 Self {
250 policy,
251 out: Vec::new(),
252 }
253 }
254
255 pub(crate) fn push(&mut self, check: Check, scope: &str, message: String) {
256 if let Some(severity) = self.policy.severity_of(check) {
257 self.out.push(Diagnostic {
258 check,
259 severity,
260 scope: scope.to_string(),
261 message,
262 });
263 }
264 }
265
266 pub(crate) fn finish(self) -> Vec<Diagnostic> {
267 self.out
268 }
269}
270
271impl KtFile {
272 /// Every problem in this file, with each check at its default severity
273 /// (error).
274 ///
275 /// An empty result means the file is as good as the *model* can prove:
276 /// names, redeclarations and declaration shapes are checked, and [`Check`]
277 /// enumerates exactly which. Anything needing type resolution, or reading
278 /// the raw text inside [`KtCode`](super::KtCode) bodies, stays the Kotlin
279 /// compiler's job.
280 pub fn validate(&self) -> Vec<Diagnostic> {
281 self.validate_with(&ValidationPolicy::new())
282 }
283
284 /// [`KtFile::validate`] with per-check severities.
285 pub fn validate_with(&self, policy: &ValidationPolicy) -> Vec<Diagnostic> {
286 let mut d = Diagnostics::new(policy);
287 check_identifiers(self, &mut d);
288 check_scope(&self.decls, &[], None, &self.package, &mut d);
289 check_shapes(&self.decls, Container::File, &self.package, &mut d);
290 check_extra_imports(self, &mut d);
291 d.finish()
292 }
293}
294
295/// The names declared in one scope must not collide — but Kotlin keeps three
296/// *separate* namespaces, so what counts as a collision depends on the kind.
297///
298/// A scope is one declaration container: the file, a class body, a companion
299/// body. Nested classes and companions are walked recursively, which is where
300/// most generated declarations actually live.
301///
302/// | Namespace | Holds | Rule |
303/// |---|---|---|
304/// | types | classes, `fun interface`s, type aliases | unique by name |
305/// | values | properties, `val`/`var` constructor parameters | unique by name |
306/// | functions | functions | unique by name **and** parameter types |
307///
308/// Keeping types and values apart is what allows `class Foo` and `val Foo` to
309/// coexist, which Kotlin permits and this check used to reject.
310///
311/// **Limitation.** There is no type resolver here, so a function's parameter
312/// types — and its extension receiver — are compared *as written*: `io.p.Foo`
313/// and `Foo` are different keys even when they name the same type, and
314/// `fun <T> f(x: T)` does not collide with `fun <R> f(x: R)`. The check
315/// therefore misses some real duplicates. It is a net, not a proof — and a net
316/// that never catches a fish it shouldn't.
317fn check_scope<'a>(
318 decls: &'a [KtDecl],
319 ctor_params: &'a [KtCtorParam],
320 companion: Option<&'a KtCompanion>,
321 scope: &str,
322 d: &mut Diagnostics<'_>,
323) {
324 let mut types: BTreeSet<&str> = BTreeSet::new();
325 let mut values: BTreeSet<&str> = BTreeSet::new();
326 let mut funs: BTreeSet<String> = BTreeSet::new();
327 let mut raws: BTreeSet<&str> = BTreeSet::new();
328
329 // `val`/`var` constructor parameters are properties of the class, so they
330 // share the value namespace with its members. A plain (non-property)
331 // parameter is constructor-local and declares nothing.
332 for p in ctor_params.iter().filter(|p| p.prop.is_some()) {
333 if !p.name.is_empty() && !values.insert(&p.name) {
334 d.push(
335 Check::DuplicateValue,
336 scope,
337 format!("duplicate value `{}` (constructor property)", p.name),
338 );
339 }
340 }
341
342 for decl in decls {
343 match decl {
344 KtDecl::Class(c) => {
345 declare_name(&mut types, &c.name, Check::DuplicateType, "type", scope, d);
346 check_class_scope(c, scope, d);
347 }
348 KtDecl::FunInterface(i) => {
349 declare_name(&mut types, &i.name, Check::DuplicateType, "type", scope, d);
350 }
351 KtDecl::TypeAlias { name, .. } => {
352 declare_name(&mut types, name, Check::DuplicateType, "type", scope, d);
353 }
354 KtDecl::Property(p) => {
355 declare_name(
356 &mut values,
357 &p.name,
358 Check::DuplicateValue,
359 "value",
360 scope,
361 d,
362 );
363 }
364 KtDecl::Fun(f) => {
365 if f.name.is_empty() {
366 continue;
367 }
368 let sig = fun_signature(f);
369 if !funs.insert(sig.clone()) {
370 d.push(
371 Check::DuplicateFunction,
372 scope,
373 format!("duplicate function `{sig}`"),
374 );
375 }
376 }
377 KtDecl::Raw { name, .. } => {
378 declare_name(&mut raws, name, Check::DuplicateRaw, "raw block", scope, d);
379 }
380 }
381 }
382
383 // A *named* companion object is a classifier nested in this class, so its
384 // name shares the type namespace with the class's other type members —
385 // `class Factory` alongside `companion object Factory` is a redeclaration.
386 //
387 // The implicit `Companion` is deliberately left out: that name is one this
388 // crate supplies rather than one the model declares, so treating it as a
389 // declaration could fire on a generator that manages the collision itself
390 // by renaming the companion.
391 if let Some(name) = companion.and_then(|c| c.name.as_deref()) {
392 if !name.is_empty() && !types.insert(name) {
393 d.push(
394 Check::DuplicateType,
395 scope,
396 format!("companion object `{name}` collides with another type of that name"),
397 );
398 }
399 }
400}
401
402/// A function's overload identity, as it reads in a diagnostic:
403/// `f(Int)`, or `Foo.f(Int)` for an extension function.
404///
405/// The receiver is part of the identity because Kotlin dispatches an extension
406/// on it — `Foo.f()` and `Bar.f()` are two declarations in one package, not a
407/// redeclaration. It is parenthesized on the same rule the renderer uses, so
408/// the diagnostic reads as the syntax it is describing.
409fn fun_signature(f: &KtFun) -> String {
410 let params = param_signature(&f.params);
411 match &f.receiver {
412 Some(r) if r.needs_receiver_parens() => format!("({r}).{}({params})", f.name),
413 Some(r) => format!("{r}.{}({params})", f.name),
414 None => format!("{}({params})", f.name),
415 }
416}
417
418/// A function's parameter types as written, which is the only signature the
419/// model can offer — see the note on [`check_scope`].
420fn param_signature(params: &[KtParam]) -> String {
421 params
422 .iter()
423 .map(|p| p.ty.to_string())
424 .collect::<Vec<_>>()
425 .join(", ")
426}
427
428fn declare_name<'a>(
429 set: &mut BTreeSet<&'a str>,
430 name: &'a str,
431 check: Check,
432 what: &str,
433 scope: &str,
434 d: &mut Diagnostics<'_>,
435) {
436 if name.is_empty() {
437 return;
438 }
439 if !set.insert(name) {
440 d.push(check, scope, format!("duplicate {what} `{name}`"));
441 }
442}
443
444/// A class body is its own scope, and so is its companion's.
445fn check_class_scope(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
446 let inner = scope_join(scope, &c.name);
447 // The companion is part of this class's scope, not a member of it — its
448 // name is declared here, while its body is a scope of its own.
449 check_scope(
450 &c.members,
451 c.ctor_params(),
452 c.companion.as_deref(),
453 &inner,
454 d,
455 );
456 if let Some(comp) = &c.companion {
457 let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
458 check_scope(&comp.members, &[], None, &cscope, d);
459 }
460}
461
462/// Extra imports carry pre-shortened raw-text references, so a simple-name
463/// collision between two distinct FQNs cannot be repaired by qualifying a use
464/// site the way a modelled type's can — the raw text already says `Foo`.
465///
466/// Lowercase simple names are exempt: those are top-level *function* imports,
467/// which Kotlin allows to overload across packages.
468fn check_extra_imports(file: &KtFile, d: &mut Diagnostics<'_>) {
469 let mut by_simple: BTreeMap<&str, &str> = BTreeMap::new();
470 for imp in &file.extra_imports {
471 let simple = imp.rsplit_once('.').map(|(_, s)| s).unwrap_or(imp.as_str());
472 if simple.chars().next().is_some_and(|c| c.is_lowercase()) {
473 continue;
474 }
475 // First registration owns the simple name — the same rule `ImportSet`
476 // uses — so with three or more colliding FQNs every diagnostic points
477 // at the one owner rather than at whichever was seen most recently.
478 let owner = *by_simple.entry(simple).or_insert(imp.as_str());
479 if owner != imp.as_str() {
480 d.push(
481 Check::ImportCollision,
482 &file.package,
483 format!("import simple-name collision: `{owner}` and `{imp}`"),
484 );
485 }
486 }
487}
488
489/// Every name the model will write out is a legal Kotlin identifier.
490///
491/// Deliberately *not* checked, because they are free-form fragments rather than
492/// plain identifiers and checking them would produce false positives: generic
493/// parameter lists (`out R`, `T : Comparable<T>`), annotations, modifier
494/// keywords, and a `Raw` block's name — that last one is a merge identity, not
495/// something rendered.
496fn check_identifiers(file: &KtFile, d: &mut Diagnostics<'_>) {
497 if !is_valid_kotlin_package(&file.package) {
498 d.push(
499 Check::InvalidPackage,
500 &file.package,
501 format!("`{}` is not a valid Kotlin package path", file.package),
502 );
503 }
504 for decl in &file.decls {
505 check_decl_identifiers(decl, &file.package, d);
506 }
507}
508
509fn check_ident(name: &str, what: &str, scope: &str, d: &mut Diagnostics<'_>) {
510 if !is_writable_kotlin_ident(name) {
511 d.push(
512 Check::InvalidIdentifier,
513 scope,
514 format!("{what} name `{name}` is not a valid Kotlin identifier"),
515 );
516 }
517}
518
519fn check_decl_identifiers(decl: &KtDecl, scope: &str, d: &mut Diagnostics<'_>) {
520 match decl {
521 KtDecl::Class(c) => check_class_identifiers(c, scope, d),
522 KtDecl::Fun(f) => {
523 check_ident(&f.name, "function", scope, d);
524 let inner = scope_join(scope, &f.name);
525 for p in &f.params {
526 check_ident(&p.name, "parameter", &inner, d);
527 }
528 }
529 KtDecl::FunInterface(i) => {
530 check_ident(&i.name, "fun interface", scope, d);
531 let inner = scope_join(scope, &i.name);
532 check_ident(&i.method.name, "function", &inner, d);
533 let method = scope_join(&inner, &i.method.name);
534 for p in &i.method.params {
535 check_ident(&p.name, "parameter", &method, d);
536 }
537 }
538 KtDecl::Property(p) => check_ident(&p.name, "property", scope, d),
539 KtDecl::TypeAlias { name, .. } => check_ident(name, "type alias", scope, d),
540 // A `Raw` block's name is its merge identity, never rendered.
541 KtDecl::Raw { .. } => {}
542 }
543}
544
545fn check_class_identifiers(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
546 check_ident(&c.name, "class", scope, d);
547 let inner = scope_join(scope, &c.name);
548 for p in c.ctor_params() {
549 check_ident(&p.name, "constructor parameter", &inner, d);
550 }
551 for e in c.kind.entries() {
552 check_ident(&e.name, "enum entry", &inner, d);
553 }
554 for m in &c.members {
555 check_decl_identifiers(m, &inner, d);
556 }
557 if let Some(comp) = &c.companion {
558 if let Some(name) = &comp.name {
559 check_ident(name, "companion object", &inner, d);
560 }
561 let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
562 for m in &comp.members {
563 check_decl_identifiers(m, &cscope, d);
564 }
565 }
566}
567
568/// What encloses a declaration. A bodiless function is legal or not depending
569/// on where it sits, which no type in the model can capture — so this is the
570/// context the shape checks need.
571#[derive(Clone, Copy, PartialEq, Eq)]
572enum Container {
573 /// Top level of a file.
574 File,
575 /// An `interface` or `sealed interface`: members without a body are
576 /// abstract by position, no keyword required.
577 Interface,
578 /// An `abstract` or `sealed` class: a member may be abstract, but must say
579 /// so.
580 Abstract,
581 /// Anything else — every function needs a body.
582 Concrete,
583}
584
585impl Container {
586 fn of(c: &KtClass) -> Self {
587 match &c.kind {
588 KtClassKind::Interface | KtClassKind::SealedInterface => Container::Interface,
589 KtClassKind::Class {
590 modifier: Some(KtClassModifier::Abstract) | Some(KtClassModifier::Sealed),
591 ..
592 } => Container::Abstract,
593 _ => Container::Concrete,
594 }
595 }
596}
597
598/// The shapes that [Part A](https://github.com/milyin/kotlin-codegen/pull/6)
599/// could not make unrepresentable, because each depends on a relation between
600/// fields or on where a declaration sits rather than on one field's type.
601fn check_shapes(decls: &[KtDecl], container: Container, scope: &str, d: &mut Diagnostics<'_>) {
602 for decl in decls {
603 match decl {
604 KtDecl::Property(p) => {
605 // Only the all-absent case is unambiguously wrong. A type alone
606 // is an abstract property; a value alone infers its type.
607 if p.ty.is_none()
608 && matches!(p.value, KtPropertyValue::None)
609 && p.accessors.is_none()
610 {
611 d.push(
612 Check::PropertyWithoutTypeOrValue,
613 scope,
614 format!(
615 "property `{}` has no type, no value and no accessors",
616 p.name
617 ),
618 );
619 }
620 }
621 KtDecl::Fun(f) => {
622 if !matches!(f.body, KtBody::None) {
623 continue;
624 }
625 let allowed = match container {
626 Container::Interface => true,
627 Container::Abstract => f
628 .modifiers
629 .iter()
630 .any(|m| m.split(' ').any(|w| w == "abstract")),
631 Container::File | Container::Concrete => false,
632 };
633 if !allowed {
634 d.push(
635 Check::FunctionWithoutBody,
636 scope,
637 format!(
638 "function `{}` has no body; only an interface member, or an \
639 `abstract` member of an abstract class, may omit one",
640 f.name
641 ),
642 );
643 }
644 }
645 KtDecl::Class(c) => check_class_shapes(c, scope, d),
646 KtDecl::FunInterface(_) | KtDecl::TypeAlias { .. } | KtDecl::Raw { .. } => {}
647 }
648 }
649}
650
651fn check_class_shapes(c: &KtClass, scope: &str, d: &mut Diagnostics<'_>) {
652 let inner = scope_join(scope, &c.name);
653 // Every entry of an enum with a primary constructor must call it.
654 if let KtClassKind::Enum { ctor, entries } = &c.kind {
655 if !ctor.is_empty() {
656 for e in entries.iter().filter(|e| e.args.is_none()) {
657 d.push(
658 Check::EnumEntryMissingArguments,
659 &inner,
660 format!(
661 "entry `{}` passes no arguments, but the enum declares {} \
662 constructor parameter(s)",
663 e.name,
664 ctor.len()
665 ),
666 );
667 }
668 }
669 }
670 check_shapes(&c.members, Container::of(c), &inner, d);
671 if let Some(comp) = &c.companion {
672 let cscope = scope_join(&inner, comp.name.as_deref().unwrap_or("Companion"));
673 // A companion object is concrete: its members need bodies.
674 check_shapes(&comp.members, Container::Concrete, &cscope, d);
675 }
676}