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

Function Components & Hooks V2 #2401

Merged
merged 45 commits into from Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c1ed876
Make a use_hook hook with the new Hook trait.
futursolo Jan 12, 2022
863fa92
Merge branch 'master' into fc-hook-v2
futursolo Jan 12, 2022
22d91f6
Implement Lifetime.
futursolo Jan 13, 2022
d940a7d
Rewrites function signature.
futursolo Jan 13, 2022
572becb
Only apply lifetime if there're other lifetimes.
futursolo Jan 16, 2022
7dc8373
Merge branch 'master' into fc-hook-v2
futursolo Jan 22, 2022
7e8bcdd
Cleanup signature rewrite logic.
futursolo Jan 22, 2022
2664141
Rewrite hook body.
futursolo Jan 22, 2022
aac78b1
Port some built-in hooks.
futursolo Jan 22, 2022
1e5c373
Finish porting all built-in hooks.
futursolo Jan 22, 2022
5f086e8
Port tests.
futursolo Jan 22, 2022
a87b2c7
Fix tests.
futursolo Jan 22, 2022
62396f0
Migrate to macro-based hooks.
futursolo Jan 22, 2022
72518e2
Fix HookContext, add tests on non-possible locations.
futursolo Jan 22, 2022
38e06b1
Fix stderr for trybuild.
futursolo Jan 22, 2022
19af0b3
Add 1 more test case.
futursolo Jan 22, 2022
49454cd
Adjust doc location.
futursolo Jan 22, 2022
eb85f47
Pretty print hook signature.
futursolo Jan 22, 2022
4f0260c
Fix Items & std::ops::Fn*.
futursolo Jan 22, 2022
344ae10
Add use_memo.
futursolo Jan 22, 2022
ffca655
Optimise Implementation of hooks.
futursolo Jan 22, 2022
7b8978b
Use Box to capture function value only.
futursolo Jan 22, 2022
15585ff
Detect whether needs boxing.
futursolo Jan 22, 2022
3dbcdac
Merge commit '38021e31fe97202b112c952c6f5fb5b06289fbc5' into fc-hook-v2
futursolo Jan 23, 2022
f07c648
Add args if boxing not needed.
futursolo Jan 23, 2022
7118b19
Enforce hook number.
futursolo Jan 23, 2022
171e085
Deduplicate use_effect.
futursolo Jan 23, 2022
2f3856a
Optimise Implementation.
futursolo Jan 23, 2022
2b1d6f1
Update documentation.
futursolo Jan 23, 2022
ac765cb
Fix website test. Strip BoxedHook implementation from it.
futursolo Jan 23, 2022
9c300ed
Allow doc string.
futursolo Jan 23, 2022
76da3f6
Workaround doc tests.
futursolo Jan 23, 2022
541a945
Optimise codebase & documentation.
futursolo Jan 24, 2022
80dc7e2
Fix website test.
futursolo Jan 24, 2022
347623b
Reduce implementation complexity.
futursolo Jan 25, 2022
e974dff
Destructor is no more.
futursolo Jan 25, 2022
7160c18
Documentation and macros.
futursolo Jan 26, 2022
aa2e803
Reduce heap allocation and hook complexity.
futursolo Jan 27, 2022
1799102
Remove Queue as well.
futursolo Jan 27, 2022
acc87d1
Prefer Generics.
futursolo Jan 27, 2022
00a653a
Fix typo.
futursolo Jan 27, 2022
c861231
Remove more allocations.
futursolo Jan 27, 2022
e82b4ec
Add comments.
futursolo Jan 27, 2022
935a672
Remove outdated comment.
futursolo Jan 27, 2022
806f618
Bare Function Pointer for better code size.
futursolo Jan 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/function_todomvc/src/hooks/use_bool_toggle.rs
@@ -1,6 +1,6 @@
use std::ops::Deref;
use std::rc::Rc;
use yew::{use_state_eq, UseStateHandle};
use yew::prelude::*;

#[derive(Clone)]
pub struct UseBoolToggleHandle {
Expand Down Expand Up @@ -47,6 +47,7 @@ impl Deref for UseBoolToggleHandle {
/// <button {onclick}>{ "Click me" }</button>
/// ...
/// ```
#[hook]
pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle {
let state = use_state_eq(|| default);

Expand Down
1 change: 1 addition & 0 deletions examples/simple_ssr/src/main.rs
Expand Up @@ -57,6 +57,7 @@ impl PartialEq for UuidState {
}
}

#[hook]
fn use_random_uuid() -> SuspensionResult<Uuid> {
let s = use_state(UuidState::new);

Expand Down
1 change: 1 addition & 0 deletions examples/suspense/src/use_sleep.rs
Expand Up @@ -28,6 +28,7 @@ impl Reducible for SleepState {
}
}

#[hook]
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);

Expand Down
1 change: 1 addition & 0 deletions packages/yew-agent/src/hooks.rs
Expand Up @@ -29,6 +29,7 @@ where
///
/// Takes a callback as the only argument. The callback will be updated on every render to make
/// sure captured values (if any) are up to date.
#[hook]
pub fn use_bridge<T, F>(on_output: F) -> UseBridgeHandle<T>
where
T: Bridged,
Expand Down
4 changes: 3 additions & 1 deletion packages/yew-macro/Cargo.toml
Expand Up @@ -21,7 +21,9 @@ lazy_static = "1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] }
syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] }
once_cell = "1"
prettyplease = "0.1.1"
WorldSEnder marked this conversation as resolved.
Show resolved Hide resolved

# testing
[dev-dependencies]
Expand Down
21 changes: 16 additions & 5 deletions packages/yew-macro/src/function_component.rs
Expand Up @@ -3,7 +3,11 @@ use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::{Comma, Fn};
use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility};
use syn::{
visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility,
};

use crate::hook::BodyRewriter;

#[derive(Clone)]
pub struct FunctionComponent {
Expand Down Expand Up @@ -169,7 +173,7 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
fn_token,
name,
attrs,
block,
mut block,
return_type,
generics,
arg,
Expand All @@ -184,9 +188,14 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
Ident::new("inner", Span::mixed_site())
};

let ctx_ident = Ident::new("ctx", Span::mixed_site());

let mut body_rewriter = BodyRewriter::default();
visit_mut::visit_block_mut(&mut body_rewriter, &mut *block);

quote! {
#(#attrs)*
#fn_token #name #ty_generics (#arg) -> #return_type
#fn_token #name #ty_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type
#where_clause
{
#block
Expand Down Expand Up @@ -241,6 +250,8 @@ pub fn function_component_impl(
Ident::new("inner", Span::mixed_site())
};

let ctx_ident = Ident::new("ctx", Span::mixed_site());

let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
Expand All @@ -253,10 +264,10 @@ pub fn function_component_impl(
impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
type TProps = #props_type;

fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
fn run(#ctx_ident: &mut ::yew::functional::HookContext, #provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
#func

::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props))
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #provider_props))
}
}

Expand Down
118 changes: 118 additions & 0 deletions packages/yew-macro/src/hook/body.rs
@@ -0,0 +1,118 @@
use proc_macro2::Span;
use proc_macro_error::emit_error;
use std::sync::{Arc, Mutex};
use syn::spanned::Spanned;
use syn::visit_mut::VisitMut;
use syn::{
parse_quote_spanned, visit_mut, Expr, ExprCall, ExprClosure, ExprForLoop, ExprIf, ExprLoop,
ExprMatch, ExprWhile, Ident, Item,
};

#[derive(Debug, Default)]
pub struct BodyRewriter {
branch_lock: Arc<Mutex<()>>,
}

impl BodyRewriter {
fn is_branched(&self) -> bool {
self.branch_lock.try_lock().is_err()
}

fn with_branch<F, O>(&mut self, f: F) -> O
where
F: FnOnce(&mut BodyRewriter) -> O,
{
let branch_lock = self.branch_lock.clone();
let _branched = branch_lock.try_lock();
f(self)
}
}

impl VisitMut for BodyRewriter {
fn visit_expr_call_mut(&mut self, i: &mut ExprCall) {
let ctx_ident = Ident::new("ctx", Span::mixed_site());

// Only rewrite hook calls.
if let Expr::Path(ref m) = &*i.func {
if let Some(m) = m.path.segments.last().as_ref().map(|m| &m.ident) {
if m.to_string().starts_with("use_") {
if self.is_branched() {
emit_error!(m, "hooks cannot be called at this position.");
} else {
*i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) };
}

return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it that this design means I can't call any function starting with use_ that is not a hook and false positives are a conscious trade-off? How do I turn this off when such a false positive occurs?
I have some doubts that this kind of syntax guided rewriting is maintainable in the long term, do you know any bigger library that uses this approach successfully to convince me that it's not going to break on us in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I turn this off when such a false positive occurs?

use some_crate::{use_something as not_a_hook} ;

that uses this approach successfully

This is used in std as well, which I will reply to the main comment instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@futursolo we should mention this in the error to be more user friendly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way to do it. The procedural does not know the type of your expression.

The only error will be from the compiler saying that use_something does not implement Hook.

}
}

visit_mut::visit_expr_call_mut(self, i);
}

fn visit_expr_closure_mut(&mut self, i: &mut ExprClosure) {
self.with_branch(move |m| visit_mut::visit_expr_closure_mut(m, i))
}

fn visit_expr_if_mut(&mut self, i: &mut ExprIf) {
for it in &mut i.attrs {
visit_mut::visit_attribute_mut(self, it);
}

visit_mut::visit_expr_mut(self, &mut *i.cond);

self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.then_branch));

if let Some(it) = &mut i.else_branch {
self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut *(it).1));
}
}

fn visit_expr_loop_mut(&mut self, i: &mut ExprLoop) {
self.with_branch(|m| visit_mut::visit_expr_loop_mut(m, i));
}

fn visit_expr_for_loop_mut(&mut self, i: &mut ExprForLoop) {
for it in &mut i.attrs {
visit_mut::visit_attribute_mut(self, it);
}
if let Some(it) = &mut i.label {
visit_mut::visit_label_mut(self, it);
}
visit_mut::visit_pat_mut(self, &mut i.pat);
visit_mut::visit_expr_mut(self, &mut *i.expr);

self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body));
}

fn visit_expr_match_mut(&mut self, i: &mut ExprMatch) {
for it in &mut i.attrs {
visit_mut::visit_attribute_mut(self, it);
}

visit_mut::visit_expr_mut(self, &mut *i.expr);

self.with_branch(|m| {
for it in &mut i.arms {
visit_mut::visit_arm_mut(m, it);
}
});
}

fn visit_expr_while_mut(&mut self, i: &mut ExprWhile) {
for it in &mut i.attrs {
visit_mut::visit_attribute_mut(self, it);
}
if let Some(it) = &mut i.label {
visit_mut::visit_label_mut(self, it);
}

self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut i.cond));
self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body));
}

fn visit_item_mut(&mut self, _i: &mut Item) {
// We don't do anything for items.
// for components / hooks in other components / hooks, apply the attribute again.
}
}
121 changes: 121 additions & 0 deletions packages/yew-macro/src/hook/lifetime.rs
@@ -0,0 +1,121 @@
use proc_macro2::Span;
use std::sync::{Arc, Mutex};
use syn::visit_mut::{self, VisitMut};
use syn::{
GenericArgument, Lifetime, ParenthesizedGenericArguments, Receiver, TypeBareFn, TypeImplTrait,
TypeParamBound, TypeReference,
};

// borrowed from the awesome async-trait crate.
pub struct CollectLifetimes {
pub elided: Vec<Lifetime>,
pub explicit: Vec<Lifetime>,
pub name: &'static str,
pub default_span: Span,

pub impl_trait_lock: Arc<Mutex<()>>,
pub impl_fn_lock: Arc<Mutex<()>>,
}

impl CollectLifetimes {
pub fn new(name: &'static str, default_span: Span) -> Self {
CollectLifetimes {
elided: Vec::new(),
explicit: Vec::new(),
name,
default_span,

impl_trait_lock: Arc::default(),
impl_fn_lock: Arc::default(),
}
}

fn is_impl_trait(&self) -> bool {
self.impl_trait_lock.try_lock().is_err()
}

fn is_impl_fn(&self) -> bool {
self.impl_fn_lock.try_lock().is_err()
}

fn visit_opt_lifetime(&mut self, lifetime: &mut Option<Lifetime>) {
match lifetime {
None => *lifetime = Some(self.next_lifetime(None)),
Some(lifetime) => self.visit_lifetime(lifetime),
}
}

fn visit_lifetime(&mut self, lifetime: &mut Lifetime) {
if lifetime.ident == "_" {
*lifetime = self.next_lifetime(lifetime.span());
} else {
self.explicit.push(lifetime.clone());
}
}

fn next_lifetime<S: Into<Option<Span>>>(&mut self, span: S) -> Lifetime {
let name = format!("{}{}", self.name, self.elided.len());
let span = span.into().unwrap_or(self.default_span);
let life = Lifetime::new(&name, span);
self.elided.push(life.clone());
life
}
}

impl VisitMut for CollectLifetimes {
fn visit_receiver_mut(&mut self, arg: &mut Receiver) {
if let Some((_, lifetime)) = &mut arg.reference {
self.visit_opt_lifetime(lifetime);
}
}

fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) {
// We don't rewrite references in the impl FnOnce(&arg) or fn(&arg)
if self.is_impl_fn() {
return;
}

self.visit_opt_lifetime(&mut ty.lifetime);
visit_mut::visit_type_reference_mut(self, ty);
}

fn visit_generic_argument_mut(&mut self, gen: &mut GenericArgument) {
// We don't rewrite types in the impl FnOnce(&arg) -> Type<'_>
if self.is_impl_fn() {
return;
}

if let GenericArgument::Lifetime(lifetime) = gen {
self.visit_lifetime(lifetime);
}
visit_mut::visit_generic_argument_mut(self, gen);
}

fn visit_type_impl_trait_mut(&mut self, impl_trait: &mut TypeImplTrait) {
let impl_trait_lock = self.impl_trait_lock.clone();
let _locked = impl_trait_lock.try_lock();

impl_trait
.bounds
.insert(0, TypeParamBound::Lifetime(self.next_lifetime(None)));

visit_mut::visit_type_impl_trait_mut(self, impl_trait);
}

fn visit_parenthesized_generic_arguments_mut(
&mut self,
generic_args: &mut ParenthesizedGenericArguments,
) {
let impl_fn_lock = self.impl_fn_lock.clone();
let _maybe_locked = self.is_impl_trait().then(|| impl_fn_lock.try_lock());

visit_mut::visit_parenthesized_generic_arguments_mut(self, generic_args);
}

fn visit_type_bare_fn_mut(&mut self, i: &mut TypeBareFn) {
let impl_fn_lock = self.impl_fn_lock.clone();
let _locked = impl_fn_lock.try_lock();

visit_mut::visit_type_bare_fn_mut(self, i);
}
}