Skip to main content

i_slint_compiler/passes/
binding_analysis.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Compute binding analysis and attempt to find binding loops
5
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::rc::Rc;
9
10use by_address::ByAddress;
11
12use crate::diagnostics::{BuildDiagnostics, Spanned};
13use crate::expression_tree::{BindingExpression, BuiltinFunction, Expression};
14use crate::langtype::ElementType;
15use crate::layout::{LayoutItem, Orientation};
16use crate::namedreference::NamedReference;
17use crate::object_tree::{Document, ElementRc, PropertyAnimation, find_parent_element};
18use derive_more as dm;
19
20use crate::CompilerConfiguration;
21use crate::expression_tree::Callable;
22use smol_str::{SmolStr, ToSmolStr};
23
24/// Represent the kind of property for the DefaultFontSize based on the `default-font-size`` property of every `Window``
25#[derive(Debug, Clone, PartialEq, Default)]
26pub enum DefaultFontSize {
27    /// Not yet known/computed
28    #[default]
29    Unknown,
30    /// The default font size is set to a specific constant value in `px` (logical)
31    LogicalValue(f32),
32    /// The default font size is set to different, but always constant values
33    Const,
34    /// All windows are either using const value or unset
35    NotSet,
36    /// At least one `Window` has a non-constant default-font-size
37    Variable,
38}
39impl DefaultFontSize {
40    /// Returns true if the default font size is a constant value
41    pub fn is_const(&self) -> bool {
42        // Note that NotSet is considered const for now as the renderer won't change the default font size at runtime
43        matches!(self, Self::Const | Self::LogicalValue(_))
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Default)]
48pub struct GlobalAnalysis {
49    pub default_font_size: DefaultFontSize,
50    pub const_scale_factor: Option<f32>,
51    pub const_image_sizes: bool,
52}
53
54/// Maps the alias in the other direction than what the BindingExpression::two_way_binding does.
55/// So if binding for property A has B in its BindingExpression::two_way_binding, then
56/// ReverseAliases maps B to A.
57type ReverseAliases = HashMap<NamedReference, Vec<NamedReference>>;
58
59pub fn binding_analysis(
60    doc: &Document,
61    compiler_config: &CompilerConfiguration,
62    diag: &mut BuildDiagnostics,
63) -> GlobalAnalysis {
64    let mut global_analysis = GlobalAnalysis {
65        const_scale_factor: compiler_config.const_scale_factor,
66        const_image_sizes: compiler_config.const_image_sizes,
67        ..Default::default()
68    };
69    let mut reverse_aliases = Default::default();
70    mark_used_base_properties(doc);
71    propagate_is_set_on_aliases(doc, &mut reverse_aliases);
72    check_window_properties(doc, &mut global_analysis);
73    perform_binding_analysis(
74        doc,
75        &reverse_aliases,
76        &mut global_analysis,
77        compiler_config.error_on_binding_loop_with_window_layout,
78        diag,
79    );
80    global_analysis
81}
82/// A reference to a property which might be deep in a component path.
83/// eg: `foo.bar.baz.background`: `baz.background` is the `prop` and `foo` and `bar` are in elements
84#[derive(Hash, PartialEq, Eq, Clone)]
85struct PropertyPath {
86    elements: Vec<ByAddress<ElementRc>>,
87    prop: NamedReference,
88}
89
90impl std::fmt::Debug for PropertyPath {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        for e in &self.elements {
93            write!(f, "{}.", e.borrow().id)?;
94        }
95        self.prop.fmt(f)
96    }
97}
98
99impl PropertyPath {
100    /// Given a namedReference accessed by something on the same leaf component
101    /// as self, return a new PropertyPath that represent the property pointer
102    /// to by nr in the higher possible element
103    fn relative(&self, second: &PropertyPath) -> Self {
104        let mut element =
105            second.elements.first().map_or_else(|| second.prop.element(), |f| f.0.clone());
106        if element.borrow().enclosing_component.upgrade().unwrap().is_global() {
107            return second.clone();
108        }
109        fn check_that_element_is_in_the_component(
110            e: &ElementRc,
111            c: &Rc<crate::object_tree::Component>,
112        ) -> bool {
113            let enclosing = e.borrow().enclosing_component.upgrade().unwrap();
114            Rc::ptr_eq(c, &enclosing)
115                || enclosing
116                    .parent_element
117                    .borrow()
118                    .upgrade()
119                    .is_some_and(|e| check_that_element_is_in_the_component(&e, c))
120        }
121        let mut elements = self.elements.clone();
122        loop {
123            let enclosing = element.borrow().enclosing_component.upgrade().unwrap();
124            if enclosing.parent_element().is_some()
125                || !Rc::ptr_eq(&element, &enclosing.root_element)
126            {
127                break;
128            }
129
130            let Some(last) = elements.last() else {
131                break;
132            };
133            let last_component = last.borrow().base_type.as_component().clone();
134            if !check_that_element_is_in_the_component(&element, &last_component) {
135                // `element` is not inside `last`'s sub-component. The reverse holds
136                // instead — `last`'s component is enclosed by `element`'s — meaning
137                // `second` is rooted in an enclosing scope (e.g. a repeated cell's
138                // input bound to an outer property). There is no descent prefix to
139                // lift it through, so return it unchanged. Neither containment
140                // holding is a malformed path (asserted in debug builds).
141                debug_assert!(
142                    check_that_element_is_in_the_component(
143                        &last_component.root_element,
144                        &enclosing
145                    ),
146                    "The element is not in the component pointed at by the path ({self:?} / {second:?})"
147                );
148                return second.clone();
149            }
150            element = elements.pop().unwrap().0;
151        }
152        if second.elements.is_empty() {
153            debug_assert!(elements.last().is_none_or(|x| *x != ByAddress(second.prop.element())));
154            Self { elements, prop: NamedReference::new(&element, second.prop.name().clone()) }
155        } else {
156            elements.push(ByAddress(element));
157            elements.extend(second.elements.iter().skip(1).cloned());
158            Self { elements, prop: second.prop.clone() }
159        }
160    }
161}
162
163impl From<NamedReference> for PropertyPath {
164    fn from(prop: NamedReference) -> Self {
165        Self { elements: Vec::new(), prop }
166    }
167}
168
169struct AnalysisContext<'a> {
170    visited: HashSet<PropertyPath>,
171    /// The stack of properties that depends on each other
172    currently_analyzing: indexmap::IndexSet<PropertyPath>,
173    /// When set, one of the property in the `currently_analyzing` stack is the window layout property
174    /// And we should issue a warning if that's part of a loop instead of an error
175    window_layout_property: Option<PropertyPath>,
176    error_on_binding_loop_with_window_layout: bool,
177    global_analysis: &'a mut GlobalAnalysis,
178}
179
180fn perform_binding_analysis(
181    doc: &Document,
182    reverse_aliases: &ReverseAliases,
183    global_analysis: &mut GlobalAnalysis,
184    error_on_binding_loop_with_window_layout: bool,
185    diag: &mut BuildDiagnostics,
186) {
187    let mut context = AnalysisContext {
188        error_on_binding_loop_with_window_layout,
189        visited: HashSet::new(),
190        currently_analyzing: Default::default(),
191        window_layout_property: None,
192        global_analysis,
193    };
194    doc.visit_all_used_components(|component| {
195        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
196            component,
197            &(),
198            &mut |e, _| analyze_element(e, &mut context, reverse_aliases, diag),
199        )
200    });
201}
202
203fn analyze_element(
204    elem: &ElementRc,
205    context: &mut AnalysisContext,
206    reverse_aliases: &ReverseAliases,
207    diag: &mut BuildDiagnostics,
208) {
209    for (name, binding) in elem.borrow().real_bindings() {
210        if binding.borrow().analysis.is_some() {
211            continue;
212        }
213        analyze_binding(
214            &PropertyPath::from(NamedReference::new(elem, name.clone())),
215            context,
216            reverse_aliases,
217            diag,
218        );
219    }
220    for cb in elem.borrow().change_callbacks.values() {
221        for e in cb.borrow().iter() {
222            recurse_expression(elem, e, &mut |prop, r| {
223                process_property(prop, r, context, reverse_aliases, diag);
224            });
225        }
226    }
227    const P: ReadType = ReadType::PropertyRead;
228    for nr in elem.borrow().accessibility_props.0.values() {
229        process_property(&PropertyPath::from(nr.clone()), P, context, reverse_aliases, diag);
230    }
231    if let Some(g) = elem.borrow().geometry_props.as_ref() {
232        process_property(&g.x.clone().into(), P, context, reverse_aliases, diag);
233        process_property(&g.y.clone().into(), P, context, reverse_aliases, diag);
234        process_property(&g.width.clone().into(), P, context, reverse_aliases, diag);
235        process_property(&g.height.clone().into(), P, context, reverse_aliases, diag);
236    }
237
238    if let Some(component) = elem.borrow().enclosing_component.upgrade()
239        && Rc::ptr_eq(&component.root_element, elem)
240    {
241        for e in component.init_code.borrow().iter() {
242            recurse_expression(elem, e, &mut |prop, r| {
243                process_property(prop, r, context, reverse_aliases, diag);
244            });
245        }
246        component.root_constraints.borrow_mut().visit_named_references(&mut |nr| {
247            process_property(&nr.clone().into(), P, context, reverse_aliases, diag);
248        });
249        component.popup_windows.borrow().iter().for_each(|p| {
250            process_property(&p.x.clone().into(), P, context, reverse_aliases, diag);
251            process_property(&p.y.clone().into(), P, context, reverse_aliases, diag);
252        });
253        component.timers.borrow().iter().for_each(|t| {
254            process_property(&t.interval.clone().into(), P, context, reverse_aliases, diag);
255            process_property(&t.running.clone().into(), P, context, reverse_aliases, diag);
256            process_property(&t.triggered.clone().into(), P, context, reverse_aliases, diag);
257        });
258    }
259
260    if let Some(repeated) = &elem.borrow().repeated {
261        recurse_expression(elem, &repeated.model, &mut |prop, r| {
262            process_property(prop, r, context, reverse_aliases, diag);
263        });
264        if let Some(lv) = &repeated.is_listview {
265            process_property(&lv.content_y.clone().into(), P, context, reverse_aliases, diag);
266            if let Some(content_height) = &lv.content_height {
267                process_property(&content_height.clone().into(), P, context, reverse_aliases, diag);
268            }
269            if let Some(content_width) = &lv.content_width {
270                process_property(&content_width.clone().into(), P, context, reverse_aliases, diag);
271            }
272            process_property(&lv.listview_height.clone().into(), P, context, reverse_aliases, diag);
273            process_property(&lv.listview_width.clone().into(), P, context, reverse_aliases, diag);
274        }
275    }
276    // `layout_info_h_at_own_height` is deliberately not analyzed here. It exists
277    // on every component root that is a column flex, whether or not an instance
278    // reads it, and it reads `self.height` — on a root whose height an instance
279    // overrides, that read is a loop nobody takes. It is analyzed where it is
280    // actually read instead, through `visit_layout_items_dependencies`.
281    // `flexbox_column_wrap_width_override.slint` stops compiling if it is added.
282    if let Some((h, v)) = &elem.borrow().layout_info_prop {
283        process_property(&h.clone().into(), P, context, reverse_aliases, diag);
284        process_property(&v.clone().into(), P, context, reverse_aliases, diag);
285    }
286
287    for info in elem.borrow().debug.iter() {
288        if let Some(crate::layout::Layout::GridLayout(grid)) = &info.layout
289            && grid.uses_auto
290        {
291            for rowcol_prop_name in ["row", "col"] {
292                for it in grid.elems.iter() {
293                    let child = &it.item.element;
294                    if child
295                        .borrow()
296                        .property_analysis
297                        .borrow()
298                        .get(rowcol_prop_name)
299                        .is_some_and(|a| a.is_set || a.is_set_externally)
300                    {
301                        diag.push_error(
302                            format!("Cannot set property '{}' on '{}' because parent GridLayout uses auto-numbering",
303                                rowcol_prop_name, child.borrow().id),
304                                &child.borrow().to_source_location(), // not ideal, the location of the property being set would be better
305                        );
306                    }
307                }
308            }
309        }
310    }
311}
312
313#[derive(Copy, Clone, dm::BitAnd, dm::BitOr, dm::BitAndAssign, dm::BitOrAssign)]
314struct DependsOnExternal(bool);
315
316fn analyze_binding(
317    current: &PropertyPath,
318    context: &mut AnalysisContext,
319    reverse_aliases: &ReverseAliases,
320    diag: &mut BuildDiagnostics,
321) -> DependsOnExternal {
322    let mut depends_on_external = DependsOnExternal(false);
323    let element = current.prop.element();
324    let name = current.prop.name();
325    if (context.currently_analyzing.last() == Some(current))
326        && !element
327            .borrow()
328            .binding_cell_including_synthetic(name)
329            .unwrap()
330            .borrow()
331            .two_way_bindings
332            .is_empty()
333    {
334        let span = element
335            .borrow()
336            .binding_cell_including_synthetic(name)
337            .unwrap()
338            .borrow()
339            .span
340            .clone()
341            .unwrap_or_else(|| element.borrow().to_source_location());
342        diag.push_error(format!("Property '{name}' cannot refer to itself"), &span);
343        return depends_on_external;
344    }
345
346    if context.currently_analyzing.contains(current) {
347        let mut loop_description = String::new();
348        let mut has_window_layout = false;
349
350        fn push_prop(prop: &PropertyPath, out: &mut String) {
351            if !out.is_empty() {
352                out.push_str(" -> ");
353            }
354            let name = prop.prop.declared_name();
355            match prop.prop.element().borrow().id.as_str() {
356                "" => out.push_str(&name),
357                id => {
358                    out.push_str(id);
359                    out.push('.');
360                    out.push_str(&name);
361                }
362            }
363        }
364
365        // Build description by iterating in reverse (trigger direction: "A triggers B")
366        // and close the loop by prepending `current` at the start.
367        push_prop(current, &mut loop_description);
368        for it in context.currently_analyzing.iter().rev() {
369            if context.window_layout_property.as_ref().is_some_and(|p| p == it) {
370                has_window_layout = true;
371            }
372            push_prop(it, &mut loop_description);
373            if it == current {
374                break;
375            }
376        }
377
378        for it in context.currently_analyzing.iter().rev() {
379            let p = &it.prop;
380            let elem = p.element();
381            let elem = elem.borrow();
382            let binding = elem.binding_cell_including_synthetic(p.name()).unwrap().borrow();
383            if binding.analysis.as_ref().unwrap().is_in_binding_loop.replace(true) {
384                break;
385            }
386
387            let span = binding.span.clone().unwrap_or_else(|| elem.to_source_location());
388            // Skip the properties of synthetic elements (eg. the Flickable's content element):
389            // they have no location in the source. The rest of the loop is still reported.
390            if span.source_file.is_some() {
391                if !context.error_on_binding_loop_with_window_layout && has_window_layout {
392                    diag.push_warning(format!("The binding for the property '{}' is part of a binding loop ({loop_description}).\nThis was allowed in previous version of Slint, but is deprecated and may cause panic at runtime", p.declared_name()), &span);
393                } else {
394                    diag.push_error(format!("The binding for the property '{}' is part of a binding loop ({loop_description})", p.declared_name()), &span);
395                }
396            }
397            if it == current {
398                break;
399            }
400        }
401        return depends_on_external;
402    }
403
404    let element_borrow = element.borrow();
405    let binding = element_borrow.binding_cell_including_synthetic(name).unwrap();
406    if binding.borrow().analysis.as_ref().is_some_and(|a| a.no_external_dependencies) {
407        return depends_on_external;
408    } else if !context.visited.insert(current.clone()) {
409        return DependsOnExternal(true);
410    }
411
412    if let Ok(mut b) = binding.try_borrow_mut() {
413        b.analysis = Some(Default::default());
414    };
415    context.currently_analyzing.insert(current.clone());
416
417    let b = binding.borrow();
418    for twb in &b.two_way_bindings {
419        if let Some(p) = twb.property()
420            && p != &current.prop
421        {
422            depends_on_external |= process_property(
423                &current.relative(&p.clone().into()),
424                ReadType::PropertyRead,
425                context,
426                reverse_aliases,
427                diag,
428            );
429        }
430    }
431
432    let mut process_prop = |prop: &PropertyPath, r, context: &mut AnalysisContext| {
433        depends_on_external |=
434            process_property(&current.relative(prop), r, context, reverse_aliases, diag);
435        for x in find_alias_targets(prop, reverse_aliases) {
436            // Unlike `x == prop.prop` (a plain duplicate, skipped below), `x == current.prop`
437            // is kept: it re-enters the binding being analyzed through its own alias, which is
438            // how a loop like `foo <=> bar` plus `foo: bar` gets caught.
439            if x.prop != prop.prop {
440                depends_on_external |= process_property(
441                    &current.relative(&x),
442                    ReadType::PropertyRead,
443                    context,
444                    reverse_aliases,
445                    diag,
446                );
447            }
448        }
449    };
450
451    recurse_expression(&current.prop.element(), &b.expression, &mut |p, r| {
452        process_prop(p, r, context)
453    });
454
455    // `remove_aliases` merges two-way bound properties into one, keeping only one of the bindings,
456    // so the expression of a property aliased to this one is a dependency of this binding too.
457    // The other direction is covered by the `two_way_bindings` loop above.
458    let mut aliased_deps = Vec::new();
459    for alias in reverse_aliases.get(&current.prop).into_iter().flatten() {
460        let element = alias.element();
461        let element_borrow = element.borrow();
462        if let Some(alias_binding) = element_borrow.binding(alias.name()) {
463            recurse_expression(&element, &alias_binding.expression, &mut |p, r| {
464                // A reference back to this property is reported as "cannot refer to itself".
465                if !(p.elements.is_empty() && p.prop == current.prop) {
466                    aliased_deps.push((p.clone(), r))
467                }
468            });
469        }
470    }
471    // Process outside of the loop so that the alias binding isn't borrowed while it is analyzed.
472    for (p, r) in &aliased_deps {
473        process_prop(p, *r, context);
474    }
475
476    let mut is_const = b.expression.is_constant(Some(context.global_analysis))
477        && b.two_way_bindings.iter().all(|n| n.is_constant());
478
479    if is_const && matches!(b.expression, Expression::Invalid) {
480        // check the base
481        if let Some(base) = element.borrow().sub_component() {
482            is_const = NamedReference::new(&base.root_element, name.clone()).is_constant();
483        }
484    }
485    drop(b);
486
487    if let Ok(mut b) = binding.try_borrow_mut() {
488        // We have a loop (through different component so we're still borrowed)
489        b.analysis.as_mut().unwrap().is_const = is_const;
490    }
491
492    match &binding.borrow().animation {
493        Some(PropertyAnimation::Static(e)) => analyze_element(e, context, reverse_aliases, diag),
494        Some(PropertyAnimation::Transition { animations, state_ref }) => {
495            recurse_expression(&current.prop.element(), state_ref, &mut |p, r| {
496                process_prop(p, r, context)
497            });
498            for a in animations {
499                analyze_element(&a.animation, context, reverse_aliases, diag);
500            }
501        }
502        None => (),
503    }
504
505    let o = context.currently_analyzing.pop();
506    assert_eq!(&o.unwrap(), current);
507
508    depends_on_external
509}
510
511/// Find properties two-way-bound (via `<=>`) to `prop`, ascending through base components
512/// when the alias was declared there rather than on `prop`'s own element.
513fn find_alias_targets(prop: &PropertyPath, reverse_aliases: &ReverseAliases) -> Vec<PropertyPath> {
514    // Alias declared on prop's own element, so return the target(s) verbatim without rebasing
515    if let Some(v) = reverse_aliases.get(&prop.prop) {
516        return v
517            .iter()
518            .map(|x| PropertyPath { elements: prop.elements.clone(), prop: x.clone() })
519            .collect();
520    }
521
522    let start_element = prop.elements.first().map_or_else(|| prop.prop.element(), |e| e.0.clone());
523    let mut cur = prop.prop.clone();
524    loop {
525        let element = cur.element();
526        if element.borrow().binding(cur.name()).is_some() {
527            return Vec::new();
528        }
529        let next = match &element.borrow().base_type {
530            ElementType::Component(base) => {
531                if element.borrow().property_declarations.contains_key(cur.name()) {
532                    return Vec::new();
533                }
534                base.root_element.clone()
535            }
536            _ => return Vec::new(),
537        };
538        cur = NamedReference::new(&next, cur.name().clone());
539        if let Some(v) = reverse_aliases.get(&cur) {
540            return v
541                .iter()
542                .map(|x| PropertyPath::from(NamedReference::new(&start_element, x.name().clone())))
543                .collect();
544        }
545    }
546}
547
548#[derive(Copy, Clone, Eq, PartialEq)]
549enum ReadType {
550    // Read from the native code
551    NativeRead,
552    // Read from another property binding in Slint
553    PropertyRead,
554}
555
556/// Process the property `prop`
557///
558/// This will visit all the bindings from that property
559fn process_property(
560    prop: &PropertyPath,
561    read_type: ReadType,
562    context: &mut AnalysisContext,
563    reverse_aliases: &ReverseAliases,
564    diag: &mut BuildDiagnostics,
565) -> DependsOnExternal {
566    #[allow(clippy::match_single_binding)]
567    let depends_on_external = match prop
568        .prop
569        .element()
570        .borrow()
571        .property_analysis
572        .borrow_mut()
573        .entry(prop.prop.name().clone())
574        .or_default()
575    {
576        a => {
577            if read_type == ReadType::PropertyRead {
578                a.is_read = true;
579            }
580            DependsOnExternal(prop.elements.is_empty() && a.is_set_externally)
581        }
582    };
583
584    let mut prop = prop.clone();
585
586    loop {
587        let element = prop.prop.element();
588        if element.borrow().binding(prop.prop.name()).is_some() {
589            analyze_binding(&prop, context, reverse_aliases, diag);
590            break;
591        }
592        let next = match &element.borrow().base_type {
593            ElementType::Component(base) => {
594                if element.borrow().property_declarations.contains_key(prop.prop.name()) {
595                    break;
596                }
597                base.root_element.clone()
598            }
599            ElementType::Builtin(builtin) => {
600                if builtin.properties.contains_key(prop.prop.name()) {
601                    visit_builtin_property(builtin, &prop, context, reverse_aliases, diag);
602                }
603                break;
604            }
605            _ => break,
606        };
607        next.borrow()
608            .property_analysis
609            .borrow_mut()
610            .entry(prop.prop.name().clone())
611            .or_default()
612            .is_read_externally = true;
613        prop.elements.push(element.into());
614        prop.prop = NamedReference::new(&next, prop.prop.name().clone());
615    }
616    depends_on_external
617}
618
619// Same as in crate::visit_all_named_references_in_element, but not mut
620fn recurse_expression(
621    elem: &ElementRc,
622    expr: &Expression,
623    vis: &mut impl FnMut(&PropertyPath, ReadType),
624) {
625    const P: ReadType = ReadType::PropertyRead;
626    expr.visit(|sub| recurse_expression(elem, sub, vis));
627    match expr {
628        Expression::PropertyReference(r) => vis(&r.clone().into(), P),
629        Expression::LayoutCacheAccess { layout_cache_prop, .. } => {
630            vis(&layout_cache_prop.clone().into(), P)
631        }
632        Expression::GridRepeaterCacheAccess { layout_cache_prop, .. } => {
633            vis(&layout_cache_prop.clone().into(), P)
634        }
635        Expression::SolveBoxLayout(l, o)
636        | Expression::ComputeBoxLayoutInfo { layout: l, orientation: o, .. } => {
637            // we should only visit the layout geometry for the orientation
638            if matches!(expr, Expression::SolveBoxLayout(..))
639                && let Some(nr) = l.geometry.rect.size_reference(*o)
640            {
641                vis(&nr.clone().into(), P);
642            }
643            visit_layout_items_dependencies(l.elems.iter(), *o, vis);
644
645            // The orthogonal solve depends on `cross-axis-alignment` and on the
646            // cells' `cross-axis-self-alignment`.
647            if matches!(expr, Expression::SolveBoxLayout(..)) && *o != l.orientation {
648                if let Some(nr) = l.cross_alignment.as_ref() {
649                    vis(&nr.clone().into(), P);
650                }
651                for cell in l.elems.iter() {
652                    if let Some(nr) = cell.cross_axis_self_alignment.as_ref() {
653                        vis(&nr.clone().into(), P);
654                    }
655                }
656            }
657
658            let mut g = l.geometry.clone();
659            g.rect = Default::default(); // already visited;
660            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
661        }
662        Expression::SolveFlexboxLayout(layout)
663        | Expression::ComputeFlexboxLayoutInfo { layout, .. } => {
664            if let Some(nr) = layout.direction.as_ref() {
665                vis(&nr.clone().into(), P);
666            }
667            // Visit layout geometry dependencies
668            if matches!(expr, Expression::SolveFlexboxLayout(..)) {
669                // The solve needs the main-axis dimension (width for row,
670                // height for column). On the cross axis, *builtin* items
671                // receive the perpendicular size through the item VTable's
672                // `cross_axis_constraint` parameter and never read `self.width`
673                // at runtime, so no edge is declared for them here. *Component*
674                // items don't have that shortcut: their compiled
675                // `layoutinfo-<cross>` binding is evaluated as an ordinary
676                // property and really does depend on the cross-axis dimension
677                // (typically via inner height-for-width items). That edge is
678                // added by `visit_layout_items_layoutinfo_cross_axis_dependencies`
679                // so the binding-loop pass can detect the cycle.
680                use crate::layout::FlexboxAxisRelation;
681                match layout.axis_relation(Orientation::Horizontal) {
682                    FlexboxAxisRelation::MainAxis => {
683                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
684                            vis(&nr.clone().into(), P);
685                        }
686                        visit_layout_items_layoutinfo_cross_axis_dependencies(
687                            layout.elems.iter(),
688                            Orientation::Vertical,
689                            vis,
690                        );
691                    }
692                    FlexboxAxisRelation::CrossAxis => {
693                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
694                            vis(&nr.clone().into(), P);
695                        }
696                        visit_layout_items_layoutinfo_cross_axis_dependencies(
697                            layout.elems.iter(),
698                            Orientation::Horizontal,
699                            vis,
700                        );
701                    }
702                    FlexboxAxisRelation::Unknown => {
703                        // Runtime direction: conservatively depend on both
704                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
705                            vis(&nr.clone().into(), P);
706                        }
707                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
708                            vis(&nr.clone().into(), P);
709                        }
710                        visit_layout_items_layoutinfo_cross_axis_dependencies(
711                            layout.elems.iter(),
712                            Orientation::Horizontal,
713                            vis,
714                        );
715                        visit_layout_items_layoutinfo_cross_axis_dependencies(
716                            layout.elems.iter(),
717                            Orientation::Vertical,
718                            vis,
719                        );
720                    }
721                }
722            } else if let Expression::ComputeFlexboxLayoutInfo { orientation, .. } = expr {
723                let orientation = *orientation;
724                use crate::layout::FlexboxAxisRelation;
725                match layout.axis_relation(orientation) {
726                    FlexboxAxisRelation::MainAxis => {
727                        // Main axis: only visit same-axis item dependencies
728                        visit_layout_items_dependencies(layout.elems.iter(), orientation, vis);
729                    }
730                    FlexboxAxisRelation::CrossAxis => {
731                        // Cross axis: depends on the perpendicular (main-axis)
732                        // dimension for accurate wrapping. Skip that edge
733                        // when the element has a parametrized layout-info
734                        // function — callers that would otherwise cycle go
735                        // through it instead, so the bare binding's read of
736                        // `self.width` is a fallback only.
737                        if orientation == Orientation::Vertical
738                            && let Some(nr) = layout.geometry.rect.width_reference.as_ref()
739                            && nr.element().borrow().layout_info_v_with_constraint.is_none()
740                        {
741                            vis(&nr.clone().into(), P);
742                        }
743                        visit_layout_items_dependencies(
744                            layout.elems.iter(),
745                            Orientation::Horizontal,
746                            vis,
747                        );
748                        visit_layout_items_dependencies(
749                            layout.elems.iter(),
750                            Orientation::Vertical,
751                            vis,
752                        );
753                    }
754                    FlexboxAxisRelation::Unknown => {
755                        // Unknown direction: visit both orientations' item
756                        // dependencies but NOT perpendicular dimensions (adding
757                        // those leads to binding loops for runtime direction).
758                        visit_layout_items_dependencies(
759                            layout.elems.iter(),
760                            Orientation::Horizontal,
761                            vis,
762                        );
763                        visit_layout_items_dependencies(
764                            layout.elems.iter(),
765                            Orientation::Vertical,
766                            vis,
767                        );
768                    }
769                }
770            }
771            let mut g = layout.geometry.clone();
772            g.rect = Default::default(); // already visited;
773            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
774        }
775        Expression::OrganizeGridLayout(layout) => {
776            let mut layout = layout.clone();
777            layout.visit_rowcol_named_references(&mut |nr: &mut NamedReference| {
778                vis(&nr.clone().into(), P)
779            });
780        }
781        Expression::SolveGridLayout { layout_organized_data_prop, layout, orientation }
782        | Expression::ComputeGridLayoutInfo {
783            layout_organized_data_prop,
784            layout,
785            orientation,
786            ..
787        } => {
788            // we should only visit the layout geometry for the orientation
789            if matches!(expr, Expression::SolveGridLayout { .. })
790                && let Some(nr) = layout.geometry.rect.size_reference(*orientation)
791            {
792                vis(&nr.clone().into(), P);
793            }
794            vis(&layout_organized_data_prop.clone().into(), P);
795            visit_layout_items_dependencies(
796                layout.elems.iter().map(|it| &it.item),
797                *orientation,
798                vis,
799            );
800            let mut g = layout.geometry.clone();
801            g.rect = Default::default(); // already visited;
802            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
803        }
804        Expression::FunctionCall {
805            function: Callable::Callback(nr) | Callable::Function(nr),
806            ..
807        } => vis(&nr.clone().into(), P),
808        Expression::FunctionCall { function: Callable::Builtin(b), arguments, .. } => match b {
809            BuiltinFunction::ImplicitLayoutInfo(orientation) => {
810                if let [Expression::ElementReference(item), ..] = arguments.as_slice() {
811                    visit_implicit_layout_info_dependencies(
812                        *orientation,
813                        &item.upgrade().unwrap(),
814                        vis,
815                    );
816                }
817            }
818            BuiltinFunction::ItemAbsolutePosition => {
819                if let Some(Expression::ElementReference(item)) = arguments.first() {
820                    // The result depends on the element's own geometry origin as well as every
821                    // ancestor's (map_to_window walks the whole ancestor chain).
822                    let mut item = item.upgrade().unwrap();
823                    loop {
824                        vis(
825                            &NamedReference::new(&item, SmolStr::new_static("x")).into(),
826                            ReadType::NativeRead,
827                        );
828                        vis(
829                            &NamedReference::new(&item, SmolStr::new_static("y")).into(),
830                            ReadType::NativeRead,
831                        );
832                        let Some(parent) = find_parent_element(&item) else { break };
833                        item = parent;
834                    }
835                }
836            }
837            BuiltinFunction::ItemFontMetrics => {
838                if let Some(Expression::ElementReference(item)) = arguments.first() {
839                    let item = item.upgrade().unwrap();
840                    vis(
841                        &NamedReference::new(&item, SmolStr::new_static("font-size")).into(),
842                        ReadType::NativeRead,
843                    );
844                    vis(
845                        &NamedReference::new(&item, SmolStr::new_static("font-weight")).into(),
846                        ReadType::NativeRead,
847                    );
848                    vis(
849                        &NamedReference::new(&item, SmolStr::new_static("font-family")).into(),
850                        ReadType::NativeRead,
851                    );
852                    vis(
853                        &NamedReference::new(&item, SmolStr::new_static("font-italic")).into(),
854                        ReadType::NativeRead,
855                    );
856                }
857            }
858            BuiltinFunction::GetWindowDefaultFontSize => {
859                let root =
860                    elem.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
861                if root.borrow().builtin_type().is_some_and(|bt| bt.name == "Window") {
862                    vis(
863                        &NamedReference::new(&root, SmolStr::new_static("default-font-size"))
864                            .into(),
865                        ReadType::PropertyRead,
866                    );
867                }
868            }
869            _ => {}
870        },
871        _ => {}
872    }
873}
874
875fn visit_layout_items_dependencies<'a>(
876    items: impl Iterator<Item = &'a LayoutItem>,
877    orientation: Orientation,
878    vis: &mut impl FnMut(&PropertyPath, ReadType),
879) {
880    for it in items {
881        let mut element = it.element.clone();
882        if element
883            .borrow()
884            .repeated
885            .as_ref()
886            .map(|r| recurse_expression(&element, &r.model, vis))
887            .is_some()
888        {
889            element = it.element.borrow().base_type.as_component().root_element.clone();
890        }
891
892        if let Some(nr) = element.borrow().effective_layout_info_prop(orientation) {
893            vis(&nr.clone().into(), ReadType::PropertyRead);
894        } else {
895            let height_settled = element.borrow().height_is_literal;
896            if let Some(nr) = element.borrow().base_layout_info_prop(orientation, height_settled) {
897                vis(
898                    &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr },
899                    ReadType::PropertyRead,
900                );
901            }
902            visit_implicit_layout_info_dependencies(orientation, &element, vis);
903        }
904
905        for (nr, _) in it.constraints.for_each_restrictions(orientation) {
906            vis(&nr.clone().into(), ReadType::PropertyRead)
907        }
908    }
909}
910
911/// Visit cross-axis `layoutinfo-<cross>` dependencies for child elements that
912/// have a compiled `layoutinfo-<cross>` binding (i.e. an inlined component
913/// root, or a nested layout) and no parametrized variant that bypasses it.
914///
915/// Pure builtins (`Image`, `Text`, `Rectangle`, …) do not set `layout_info_prop`
916/// — their cross-axis size is computed through the item VTable, which accepts a
917/// `cross_axis_constraint` argument, so they never read `self.{w,h}` at
918/// runtime and the parent's `SolveFlexboxLayout` has no real dependency on
919/// them. Elements that *do* set `layout_info_prop` run an ordinary property
920/// binding that may transitively depend on the cross-axis dimension.
921/// `implicit_layout_info_call` dispatches via the parametrized
922/// `layoutinfo-v-with-constraint` function when the child carries one, so
923/// the property dependency only exists at runtime for cells without that
924/// function — mirror that here.
925fn visit_layout_items_layoutinfo_cross_axis_dependencies<'a>(
926    items: impl Iterator<Item = &'a LayoutItem>,
927    cross_axis: Orientation,
928    vis: &mut impl FnMut(&PropertyPath, ReadType),
929) {
930    for it in items {
931        let element = it.element.clone();
932        // Parent dispatches via the parametrized function, not the property.
933        if cross_axis == Orientation::Vertical
934            && element.borrow().inherited_layout_info_v_with_constraint().is_some()
935        {
936            continue;
937        }
938        if let Some(nr) = element.borrow().effective_layout_info_prop(cross_axis) {
939            vis(&nr.clone().into(), ReadType::PropertyRead);
940        } else if let Some(nr) = {
941            let height_settled = element.borrow().height_is_literal;
942            element.borrow().base_layout_info_prop(cross_axis, height_settled)
943        } {
944            vis(
945                &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr },
946                ReadType::PropertyRead,
947            );
948        } else {
949            visit_cell_cross_axis_implicit_dependency(cross_axis, &element, vis);
950        }
951    }
952}
953
954/// Cross-axis variant of [`visit_implicit_layout_info_dependencies`]: only
955/// declare deps that actually exist on the cross-axis path. Image/Text and
956/// other h-for-w builtins receive the cross-axis size via the item VTable's
957/// `cross_axis_constraint`, so they don't read `self.{w,h}` here. For other
958/// items (user components, plain builtins), the native `ImplicitLayoutInfo`
959/// reads `preferred-{w,h}` — declare it when the user has bound it to read
960/// the opposite-axis dim on the same element. That catches cycles like
961/// `preferred-height: self.width` at compile time instead of panicking at
962/// runtime.
963fn visit_cell_cross_axis_implicit_dependency(
964    cross_axis: Orientation,
965    item: &ElementRc,
966    vis: &mut impl FnMut(&PropertyPath, ReadType),
967) {
968    let base_type = item.borrow().base_type.to_smolstr();
969    if matches!(base_type.as_str(), "Image" | "ClippedImage" | "Text" | "TextInput" | "StyledText")
970    {
971        return;
972    }
973    let (prop, opposite_dim) = match cross_axis {
974        Orientation::Horizontal => ("preferred-width", "height"),
975        Orientation::Vertical => ("preferred-height", "width"),
976    };
977    if !item.borrow().is_binding_set(prop, false) {
978        return;
979    }
980    let reads_opposite = item
981        .borrow()
982        .binding(prop)
983        .map(|b| {
984            let mut seen = false;
985            b.expression.visit_recursive(&mut |sub| {
986                if let Expression::PropertyReference(nr) = sub
987                    && nr.name() == opposite_dim
988                    && Rc::ptr_eq(&nr.element(), item)
989                {
990                    seen = true;
991                }
992            });
993            seen
994        })
995        .unwrap_or(false);
996    if reads_opposite {
997        vis(&NamedReference::new(item, SmolStr::new_static(prop)).into(), ReadType::NativeRead);
998    }
999}
1000
1001/// The builtin function can call native code, and we need to visit the properties that are accessed by it
1002fn visit_implicit_layout_info_dependencies(
1003    orientation: crate::layout::Orientation,
1004    item: &ElementRc,
1005    vis: &mut impl FnMut(&PropertyPath, ReadType),
1006) {
1007    let base_type = item.borrow().base_type.to_smolstr();
1008    const N: ReadType = ReadType::NativeRead;
1009    match base_type.as_str() {
1010        "Image" => {
1011            vis(&NamedReference::new(item, SmolStr::new_static("source")).into(), N);
1012            vis(&NamedReference::new(item, SmolStr::new_static("source-clip-width")).into(), N);
1013            if orientation == Orientation::Vertical {
1014                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
1015                vis(
1016                    &NamedReference::new(item, SmolStr::new_static("source-clip-height")).into(),
1017                    N,
1018                );
1019            }
1020        }
1021        "Text" | "TextInput" => {
1022            vis(&NamedReference::new(item, SmolStr::new_static("text")).into(), N);
1023            vis(&NamedReference::new(item, SmolStr::new_static("font-family")).into(), N);
1024            vis(&NamedReference::new(item, SmolStr::new_static("font-size")).into(), N);
1025            vis(&NamedReference::new(item, SmolStr::new_static("font-weight")).into(), N);
1026            vis(&NamedReference::new(item, SmolStr::new_static("letter-spacing")).into(), N);
1027            // The line height only stretches the line boxes, so it feeds the vertical
1028            // layout info but can never influence the preferred width.
1029            if orientation == Orientation::Vertical {
1030                vis(
1031                    &NamedReference::new(item, SmolStr::new_static("line-height-factor")).into(),
1032                    N,
1033                );
1034            }
1035            vis(&NamedReference::new(item, SmolStr::new_static("wrap")).into(), N);
1036            let wrap_set = item.borrow().is_binding_set("wrap", false)
1037                || item
1038                    .borrow()
1039                    .property_analysis
1040                    .borrow()
1041                    .get("wrap")
1042                    .is_some_and(|a| a.is_set || a.is_set_externally);
1043            if wrap_set && orientation == Orientation::Vertical {
1044                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
1045            }
1046            if base_type.as_str() == "TextInput" {
1047                vis(&NamedReference::new(item, SmolStr::new_static("single-line")).into(), N);
1048            } else {
1049                vis(&NamedReference::new(item, SmolStr::new_static("overflow")).into(), N);
1050                // A line dropped by the limit is also excluded from the content widths, so
1051                // `max-lines` is a dependency of both orientations, not just the height.
1052                vis(&NamedReference::new(item, SmolStr::new_static("max-lines")).into(), N);
1053            }
1054        }
1055        "StyledText" => {
1056            vis(&NamedReference::new(item, SmolStr::new_static("text")).into(), N);
1057            vis(&NamedReference::new(item, SmolStr::new_static("default-font-family")).into(), N);
1058            vis(&NamedReference::new(item, SmolStr::new_static("default-font-size")).into(), N);
1059            // A line dropped by the limit is also excluded from the content widths, so
1060            // `max-lines` is a dependency of both orientations, not just the height.
1061            vis(&NamedReference::new(item, SmolStr::new_static("max-lines")).into(), N);
1062            if orientation == Orientation::Vertical {
1063                // StyledText always word-wraps, so its height depends on the width.
1064                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
1065            }
1066        }
1067
1068        _ => (),
1069    }
1070}
1071
1072fn visit_builtin_property(
1073    builtin: &crate::langtype::BuiltinElement,
1074    prop: &PropertyPath,
1075    context: &mut AnalysisContext,
1076    reverse_aliases: &ReverseAliases,
1077    diag: &mut BuildDiagnostics,
1078) {
1079    let name = prop.prop.name();
1080    if builtin.name == "Window" {
1081        for (p, orientation) in
1082            [("width", Orientation::Horizontal), ("height", Orientation::Vertical)]
1083        {
1084            if name == p {
1085                // find the actual root component
1086                let is_root = |e: &ElementRc| -> bool {
1087                    ElementRc::ptr_eq(
1088                        e,
1089                        &e.borrow().enclosing_component.upgrade().unwrap().root_element,
1090                    )
1091                };
1092                let mut root = prop.prop.element();
1093                if !is_root(&root) {
1094                    return;
1095                };
1096                for e in prop.elements.iter().rev() {
1097                    if !is_root(&e.0) {
1098                        return;
1099                    }
1100                    root = e.0.clone();
1101                }
1102                if let Some(p) = root.borrow().effective_layout_info_prop(orientation) {
1103                    let path = PropertyPath::from(p.clone());
1104                    let old_layout = context.window_layout_property.replace(path.clone());
1105                    process_property(&path, ReadType::NativeRead, context, reverse_aliases, diag);
1106                    context.window_layout_property = old_layout;
1107                };
1108            }
1109        }
1110    }
1111}
1112
1113/// Analyze the Window default-font-size property
1114fn check_window_properties(doc: &Document, global_analysis: &mut GlobalAnalysis) {
1115    doc.visit_all_used_components(|component| {
1116        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1117            component,
1118            &(),
1119            &mut |elem, _| {
1120                if elem.borrow().builtin_type().as_ref().is_some_and(|b| b.name == "Window") {
1121                    const DEFAULT_FONT_SIZE: &str = "default-font-size";
1122                    if elem.borrow().is_binding_set(DEFAULT_FONT_SIZE, false)
1123                        || elem
1124                            .borrow()
1125                            .property_analysis
1126                            .borrow()
1127                            .get(DEFAULT_FONT_SIZE)
1128                            .is_some_and(|a| a.is_set)
1129                    {
1130                        // Do not ignore debug hooks here. They make the expression variable, so the
1131                        // const-check would incorrectly mark the font size as const, even if it is
1132                        // not.
1133                        let value = elem.borrow().binding(DEFAULT_FONT_SIZE).and_then(|e| match e
1134                            .expression
1135                        {
1136                            Expression::NumberLiteral(v, crate::expression_tree::Unit::Px) => {
1137                                Some(v as f32)
1138                            }
1139                            _ => None,
1140                        });
1141                        let is_const = value.is_some()
1142                            || NamedReference::new(elem, SmolStr::new_static(DEFAULT_FONT_SIZE))
1143                                .is_constant();
1144                        global_analysis.default_font_size = match global_analysis.default_font_size
1145                        {
1146                            DefaultFontSize::Unknown => match value {
1147                                Some(v) => DefaultFontSize::LogicalValue(v),
1148                                None if is_const => DefaultFontSize::Const,
1149                                None => DefaultFontSize::Variable,
1150                            },
1151                            DefaultFontSize::NotSet if is_const => DefaultFontSize::NotSet,
1152                            DefaultFontSize::LogicalValue(val) => match value {
1153                                Some(v) if v == val => DefaultFontSize::LogicalValue(val),
1154                                _ if is_const => DefaultFontSize::Const,
1155                                _ => DefaultFontSize::Variable,
1156                            },
1157                            DefaultFontSize::Const if is_const => DefaultFontSize::Const,
1158                            _ => DefaultFontSize::Variable,
1159                        }
1160                    } else {
1161                        global_analysis.default_font_size = match global_analysis.default_font_size
1162                        {
1163                            DefaultFontSize::Unknown => DefaultFontSize::NotSet,
1164                            DefaultFontSize::NotSet => DefaultFontSize::NotSet,
1165                            DefaultFontSize::LogicalValue(_) => DefaultFontSize::NotSet,
1166                            DefaultFontSize::Const => DefaultFontSize::NotSet,
1167                            DefaultFontSize::Variable => DefaultFontSize::Variable,
1168                        }
1169                    }
1170                }
1171            },
1172        );
1173    });
1174}
1175
1176/// Make sure that the is_set property analysis is set to any property which has a two way binding
1177/// to a property that is, itself, is set
1178///
1179/// Example:
1180/// ```slint
1181/// Xx := TouchArea {
1182///    property <int> bar <=> foo;
1183///    clicked => { bar+=1; }
1184///    property <int> foo; // must ensure that this is not considered as const, because the alias with bar
1185/// }
1186/// ```
1187fn propagate_is_set_on_aliases(doc: &Document, reverse_aliases: &mut ReverseAliases) {
1188    doc.visit_all_used_components(|component| {
1189        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1190            component,
1191            &(),
1192            &mut |e, _| visit_element(e, reverse_aliases),
1193        );
1194    });
1195
1196    fn visit_element(e: &ElementRc, reverse_aliases: &mut ReverseAliases) {
1197        for (name, binding) in e.borrow().real_bindings() {
1198            if !binding.borrow().two_way_bindings.is_empty() {
1199                check_alias(e, name, &binding.borrow());
1200
1201                let nr = NamedReference::new(e, name.clone());
1202                for a in &binding.borrow().two_way_bindings {
1203                    if let Some(a) = a.property()
1204                        && a != &nr
1205                        && !a.element().borrow().enclosing_component.upgrade().unwrap().is_global()
1206                    {
1207                        reverse_aliases.entry(a.clone()).or_default().push(nr.clone())
1208                    }
1209                }
1210            }
1211        }
1212        for decl in e.borrow().property_declarations.values() {
1213            if let Some(alias) = &decl.is_alias {
1214                mark_alias(alias)
1215            }
1216        }
1217    }
1218
1219    fn check_alias(e: &ElementRc, name: &SmolStr, binding: &BindingExpression) {
1220        // Note: since the analysis hasn't been run, any property access will result in a non constant binding. this is slightly non-optimal
1221        let is_binding_constant =
1222            binding.is_constant(None) && binding.two_way_bindings.iter().all(|n| n.is_constant());
1223        if is_binding_constant && !NamedReference::new(e, name.clone()).is_externally_modified() {
1224            for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
1225                crate::namedreference::mark_property_set_derived_in_base(
1226                    alias.element(),
1227                    alias.name(),
1228                );
1229            }
1230            return;
1231        }
1232
1233        propagate_alias(binding);
1234    }
1235
1236    fn propagate_alias(binding: &BindingExpression) {
1237        for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
1238            mark_alias(alias);
1239        }
1240    }
1241
1242    fn mark_alias(alias: &NamedReference) {
1243        alias.mark_as_set();
1244        if !alias.is_externally_modified()
1245            && let Some(bind) = alias.element().borrow().binding(alias.name())
1246        {
1247            propagate_alias(&bind)
1248        }
1249    }
1250}
1251
1252/// Make sure that the is_set_externally is true for all bindings.
1253/// And change bindings are used externally
1254fn mark_used_base_properties(doc: &Document) {
1255    doc.visit_all_used_components(|component| {
1256        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1257            component,
1258            &(),
1259            &mut |element, _| {
1260                if !matches!(element.borrow().base_type, ElementType::Component(_)) {
1261                    return;
1262                }
1263                for (name, binding) in element.borrow().real_bindings() {
1264                    if binding.borrow().has_binding() {
1265                        crate::namedreference::mark_property_set_derived_in_base(
1266                            element.clone(),
1267                            name,
1268                        );
1269                    }
1270                }
1271                for name in element.borrow().change_callbacks.keys() {
1272                    crate::namedreference::mark_property_read_derived_in_base(
1273                        element.clone(),
1274                        name,
1275                    );
1276                }
1277            },
1278        );
1279    });
1280}