Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better debugging for ReactiveScopes #307

Merged
merged 5 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/sycamore-macro/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,23 @@ pub fn component_impl(
arg,
mut generics,
vis,
attrs,
mut attrs,
name,
return_type,
} = component;

let mut doc_attrs = Vec::new();
let mut i = 0;
while i < attrs.len() {
if attrs[i].path.is_ident("doc") {
// Attribute is a doc attribute. Remove from attrs and add to doc_attrs.
let at = attrs.remove(i);
doc_attrs.push(at);
} else {
i += 1;
}
}

let prop_ty = match &arg {
FnArg::Receiver(_) => unreachable!(),
FnArg::Typed(pat_ty) => &pat_ty.ty,
Expand Down Expand Up @@ -226,7 +238,7 @@ pub fn component_impl(
}

let quoted = quote! {
#(#attrs)*
#(#doc_attrs)*
#vis struct #component_name#generics {
#[doc(hidden)]
_marker: ::std::marker::PhantomData<(#phantom_generics)>,
Expand All @@ -239,6 +251,7 @@ pub fn component_impl(
const NAME: &'static ::std::primitive::str = #component_name_str;
type Props = #prop_ty;

#(#attrs)*
fn create_component(#arg) -> #return_type{
#block
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sycamore-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod view;
/// A macro for ergonomically creating complex UI structures.
///
/// To learn more about the template syntax, see the chapter on
/// [the `view!` macro](https://sycamore-rs.netlify.app/docs/basics/template) in the Sycamore Book.
/// [the `view!` macro](https://sycamore-rs.netlify.app/docs/basics/view) in the Sycamore Book.
#[proc_macro]
pub fn view(component: TokenStream) -> TokenStream {
let component = parse_macro_input!(component as view::HtmlRoot);
Expand Down
27 changes: 24 additions & 3 deletions packages/sycamore-reactive/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ pub(super) trait ContextAny {
/// Get the value stored in the context. The concrete type of the returned value is guaranteed
/// to match the type when calling [`get_type_id`](ContextAny::get_type_id).
fn get_value(&self) -> &dyn Any;

/// Get the name of type of context or `None` if not available.
fn get_type_name(&self) -> Option<&'static str>;
}

/// Inner representation of a context.
struct Context<T: 'static> {
value: T,
/// The type name of the context. Only available in debug mode.
#[cfg(debug_assertions)]
type_name: &'static str,
}

impl<T: 'static> ContextAny for Context<T> {
Expand All @@ -31,6 +37,13 @@ impl<T: 'static> ContextAny for Context<T> {
fn get_value(&self) -> &dyn Any {
&self.value
}

fn get_type_name(&self) -> Option<&'static str> {
#[cfg(debug_assertions)]
return Some(self.type_name);
#[cfg(not(debug_assertions))]
return None;
}
}

/// Get the value of a context in the current [`ReactiveScope`] or `None` if not found.
Expand Down Expand Up @@ -68,11 +81,19 @@ pub fn use_context<T: Clone + 'static>() -> T {
}

/// Creates a new [`ReactiveScope`] with a context and runs the supplied callback function.
#[cfg_attr(debug_assertions, track_caller)]
pub fn create_context_scope<T: 'static, Out>(value: T, f: impl FnOnce() -> Out) -> Out {
// Create a new ReactiveScope.
// We make sure to create the ReactiveScope outside of the closure so that track_caller can do
// its thing.
let scope = ReactiveScope::new();
SCOPES.with(|scopes| {
// Create a new ReactiveScope with a context.
let scope = ReactiveScope::new();
scope.0.borrow_mut().context = Some(Box::new(Context { value }));
// Attach the context to the scope.
scope.0.borrow_mut().context = Some(Box::new(Context {
value,
#[cfg(debug_assertions)]
type_name: std::any::type_name::<T>(),
}));
scopes.borrow_mut().push(scope);
let out = f();
let scope = scopes.borrow_mut().pop().unwrap_throw();
Expand Down
98 changes: 95 additions & 3 deletions packages/sycamore-reactive/src/effect.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::cell::RefCell;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::panic::Location;
use std::rc::{Rc, Weak};
use std::{mem, ptr};

Expand Down Expand Up @@ -57,7 +59,6 @@ impl Listener {
}

/// Internal representation for [`ReactiveScope`].
#[derive(Default)]
pub(crate) struct ReactiveScopeInner {
/// Effects created in this scope.
effects: SmallVec<[Rc<RefCell<Option<Listener>>>; REACTIVE_SCOPE_EFFECTS_STACK_CAPACITY]>,
Expand All @@ -66,6 +67,31 @@ pub(crate) struct ReactiveScopeInner {
/// Contexts created in this scope.
pub context: Option<Box<dyn ContextAny>>,
pub parent: ReactiveScopeWeak,
/// The source location where this scope was created.
/// Only available when in debug mode.
#[cfg(debug_assertions)]
pub loc: &'static Location<'static>,
}

impl ReactiveScopeInner {
#[cfg_attr(debug_assertions, track_caller)]
pub fn new() -> Self {
Self {
effects: SmallVec::new(),
cleanup: Vec::new(),
context: None,
parent: ReactiveScopeWeak::default(),
#[cfg(debug_assertions)]
loc: Location::caller(),
}
}
}

impl Default for ReactiveScopeInner {
#[cfg_attr(debug_assertions, track_caller)]
fn default() -> Self {
Self::new()
}
}

/// Owns the effects created in the current reactive scope.
Expand All @@ -75,15 +101,17 @@ pub(crate) struct ReactiveScopeInner {
/// A new [`ReactiveScope`] is usually created with [`create_root`]. A new [`ReactiveScope`] is also
/// created when a new effect is created with [`create_effect`] and other reactive utilities that
/// call it under the hood.
#[derive(Default)]
pub struct ReactiveScope(pub(crate) Rc<RefCell<ReactiveScopeInner>>);

impl ReactiveScope {
/// Create a new empty [`ReactiveScope`].
///
/// This should be rarely used and only serve as a placeholder.
#[cfg_attr(debug_assertions, track_caller)]
pub fn new() -> Self {
Self::default()
// We call this first to make sure that track_caller can do its thing.
let inner = ReactiveScopeInner::new();
Self(Rc::new(RefCell::new(inner)))
}

/// Add an effect that is owned by this [`ReactiveScope`].
Expand Down Expand Up @@ -126,6 +154,21 @@ impl ReactiveScope {
});
u
}

/// Returns the source code [`Location`] where this [`ReactiveScope`] was created.
pub fn creation_loc(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
return Some(self.0.borrow().loc);
#[cfg(not(debug_assertions))]
return None;
}
}

impl Default for ReactiveScope {
#[cfg_attr(debug_assertions, track_caller)]
fn default() -> Self {
Self::new()
}
}

impl Drop for ReactiveScope {
Expand Down Expand Up @@ -621,6 +664,55 @@ pub fn current_scope() -> Option<ReactiveScopeWeak> {
})
}

/// A struct that can be debug-printed to view the scope hierarchy at the location it was created.
pub struct DebugScopeHierarchy {
scope: Option<Rc<RefCell<ReactiveScopeInner>>>,
loc: &'static Location<'static>,
}

/// Returns a [`DebugScopeHierarchy`] which can be printed using [`std::fmt::Debug`] to debug the
/// scope hierarchy at the current level.
#[track_caller]
pub fn debug_scope_hierarchy() -> DebugScopeHierarchy {
let loc = Location::caller();
SCOPES.with(|scope| DebugScopeHierarchy {
scope: scope.borrow().last().map(|x| x.0.clone()),
loc,
})
}

impl Debug for DebugScopeHierarchy {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Reactive scope hierarchy at {}:", self.loc)?;
if let Some(scope) = &self.scope {
let mut s = Some(scope.clone());
while let Some(x) = s {
// Print scope.
if let Some(loc) = ReactiveScope(x.clone()).creation_loc() {
write!(f, "\tScope created at {}", loc)?;
} else {
write!(f, "\tScope")?;
}
// Print context.
if let Some(context) = &x.borrow().context {
let type_name = context.get_type_name();
if let Some(type_name) = type_name {
write!(f, " with context (type = {})", type_name)?;
} else {
write!(f, " with context")?;
}
}
writeln!(f)?;
// Set next iteration with scope parent.
s = x.borrow().parent.0.upgrade();
}
} else {
writeln!(f, "Not inside a reactive scope")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 7 additions & 3 deletions packages/sycamore-reactive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,23 @@ pub fn create_child_scope_in<'a>(
/// ```
/// TODO: deprecate this method in favor of [`create_scope`].
#[must_use = "create_root returns the reactive scope of the effects created inside this scope"]
#[cfg_attr(debug_assertions, track_caller)]
pub fn create_root<'a>(callback: impl FnOnce() + 'a) -> ReactiveScope {
_create_child_scope_in(None, Box::new(callback))
}

/// Internal implementation: use dynamic dispatch to reduce code bloat.
#[cfg_attr(debug_assertions, track_caller)]
fn _create_child_scope_in<'a>(
parent: Option<&ReactiveScopeWeak>,
callback: Box<dyn FnOnce() + 'a>,
) -> ReactiveScope {
SCOPES.with(|scopes| {
// Push new empty scope on the stack.
let scope = ReactiveScope::new();
// Push new empty scope on the stack.
// We make sure to create the ReactiveScope outside of the closure so that track_caller can do
// its thing.
let scope = ReactiveScope::new();

SCOPES.with(|scopes| {
// If `parent` was specified, use it as the parent of the new scope. Else use the parent of
// the scope this function is called in.
if let Some(parent) = parent {
Expand Down
1 change: 1 addition & 0 deletions packages/sycamore/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ where
/// # }
/// ```
#[component(ContextProvider<G>)]
#[cfg_attr(debug_assertions, track_caller)]
pub fn context_provider<T, F>(props: ContextProviderProps<T, F, G>) -> View<G>
where
T: 'static,
Expand Down