From 8cb5e035dcf494b0146391e38fb1472883440456 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 9 Aug 2026 18:45:17 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(theme):=20Evita=20referencias=20c?= =?UTF-8?q?=C3=ADclicas=20de=20`parent()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Como `Theme::parent()` se resuelve en tiempo de ejecución, una referencia cíclica (en la que un tema acaba siendo padre de sí mismo, directa o transitivamente) no puede descartarse en compilación. Se detecta al registrar el tema y aborta el arranque si procede. --- src/core/extension/all.rs | 28 ++++++++++++++++++++++++++++ src/core/theme.rs | 7 +++++++ src/core/theme/definition.rs | 5 +++++ 3 files changed, 40 insertions(+) diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index 3d8baab6..709af8aa 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -1,5 +1,6 @@ use crate::core::action::add_action; use crate::core::extension::ExtensionRef; +use crate::core::theme::ThemeRef; use crate::core::theme::all::THEMES; use crate::web::Router; use crate::{global, serve_static_files, trace, web}; @@ -45,6 +46,8 @@ fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { // Comprueba si la extensión tiene un tema asociado que deba registrarse. if let Some(theme) = extension.theme() { + check_theme_parent_chain(theme); + let mut registered_themes = THEMES.write(); // Asegura que el tema no esté ya registrado para evitar duplicados. if !registered_themes @@ -60,6 +63,31 @@ fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { } } +// Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se +// resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el +// arranque al detectar uno, antes de provocar un bucle infinito (en `Theme::handle_component()`) o +// un desbordamiento de pila (en los métodos predefinidos de `Theme` que delegan recursivamente en +// el tema padre). +fn check_theme_parent_chain(theme: ThemeRef) { + let mut chain: Vec = vec![theme]; + let mut current = theme; + while let Some(parent) = current.parent() { + if let Some(pos) = chain.iter().position(|t| t.type_id() == parent.type_id()) { + let cycle: Vec<&str> = chain[pos..] + .iter() + .map(|t| t.short_name()) + .chain(std::iter::once(parent.short_name())) + .collect(); + panic!( + "Circular theme parent chain detected: {}", + cycle.join(" -> ") + ); + } + chain.push(parent); + current = parent; + } +} + // **< REGISTRO DE LAS ACCIONES >******************************************************************* pub fn register_actions() { diff --git a/src/core/theme.rs b/src/core/theme.rs index bdc3ac81..86845707 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -20,6 +20,13 @@ //! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez //! padre de otro, basta declararlo cada vez con [`Theme::parent()`]. //! +//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación +//! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese +//! ciclo provocaría un bucle infinito en [`Theme::handle_component()`] o un desbordamiento de pila +//! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo, +//! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación** +//! si detecta una referencia circular. +//! //! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio //! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa //! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 822cd9a9..95e7e77f 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -56,6 +56,11 @@ pub trait Theme: Extension + Send + Sync { /// no los sobrescribe. /// /// La implementación por defecto devuelve `None` (tema sin padre). + /// + /// Una referencia circular (un tema acaba siendo padre de sí mismo, directa o transitivamente) + /// no puede descartarse en tiempo de compilación. PageTop la detecta al registrar el tema y + /// aborta el arranque de la aplicación si encuentra una, para evitar bucles infinitos o + /// desbordamientos de pila al usar el tema. fn parent(&self) -> Option { None }