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

Emit a deprecation warning on an encounter of a String field when deriving Properties #3382

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
79 changes: 55 additions & 24 deletions packages/yew-macro/src/derive_props/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub struct PropField {
}

impl PropField {
pub fn ty(&self) -> &Type {
&self.ty
}

/// All required property fields are wrapped in an `Option`
pub fn is_required(&self) -> bool {
matches!(self.attr, PropAttr::Required { .. })
Expand Down Expand Up @@ -293,22 +297,16 @@ impl<'a> PropFieldCheck<'a> {
}
}

fn is_path_segments_an_option(path_segments: impl Iterator<Item = String>) -> bool {
fn is_option_path_seg(seg_index: usize, path: &str) -> u8 {
match (seg_index, path) {
(0, "core") => 0b001,
(0, "std") => 0b001,
(0, "Option") => 0b111,
(1, "option") => 0b010,
(2, "Option") => 0b100,
_ => 0,
}
}

path_segments
.enumerate()
.fold(0, |flags, (i, ps)| flags | is_option_path_seg(i, &ps))
== 0b111
fn is_path_segments_an_option<'a, T>(mut iter: impl Iterator<Item = &'a T>) -> bool
where
T: 'a + ?Sized + PartialEq<str>,
{
iter.next().map_or(false, |first| {
first == "Option"
|| (first == "std" || first == "core")
&& iter.next().map_or(false, |x| x == "option")
&& iter.next().map_or(false, |x| x == "Option")
})
}

/// Returns true when the [`Path`] seems like an [`Option`] type.
Expand All @@ -320,7 +318,29 @@ fn is_path_segments_an_option(path_segments: impl Iterator<Item = String>) -> bo
///
/// Users can define their own [`Option`] type and this will return true - this is unavoidable.
fn is_path_an_option(path: &Path) -> bool {
is_path_segments_an_option(path.segments.iter().take(3).map(|ps| ps.ident.to_string()))
is_path_segments_an_option(path.segments.iter().map(|ps| &ps.ident))
}

fn is_path_segments_a_string<'a, T>(mut iter: impl Iterator<Item = &'a T>) -> bool
where
T: 'a + ?Sized + PartialEq<str>,
{
iter.next().map_or(false, |first| {
first == "String"
|| (first == "std" || first == "alloc")
&& iter.next().map_or(false, |x| x == "string")
&& iter.next().map_or(false, |x| x == "String")
})
Copy link

@msga-mmm msga-mmm Aug 22, 2023

Choose a reason for hiding this comment

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

I think adding an early return might help to make the conditions chain easier to read.

Suggested change
iter.next().map_or(false, |first| {
first == "String"
|| (first == "std" || first == "alloc")
&& iter.next().map_or(false, |x| x == "string")
&& iter.next().map_or(false, |x| x == "String")
})
iter.next().map_or(false, |first| {
if first == "String" {
return true;
}
(first == "std" || first == "alloc")
&& iter.next().map_or(false, |x| x == "string")
&& iter.next().map_or(false, |x| x == "String")
})

Also, do you think that could be a good idea to add some names like second and third?

Suggested change
iter.next().map_or(false, |first| {
first == "String"
|| (first == "std" || first == "alloc")
&& iter.next().map_or(false, |x| x == "string")
&& iter.next().map_or(false, |x| x == "String")
})
iter.next().map_or(false, |first| {
first == "String"
|| (first == "std" || first == "alloc")
&& iter.next().map_or(false, |second| second == "string")
&& iter.next().map_or(false, |third| third == "String")
})

What do you think?

Copy link
Contributor Author

@schvv31n schvv31n Aug 22, 2023

Choose a reason for hiding this comment

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

I believe the || operator is perfectly readable and makes the function concise, but I do agree that giving descriptive names to closures' arguments is a worthwhile measure, I'll do that right away

}

/// returns true when the [`Path`] seems like a [`String`] type.
///
/// This function considers the following paths as Strings:
/// - std::string::String
/// - alloc::string::String
/// - String
pub(crate) fn is_path_a_string(path: &Path) -> bool {
is_path_segments_a_string(path.segments.iter().map(|ps| &ps.ident))
Copy link
Member

@futursolo futursolo Aug 22, 2023

Choose a reason for hiding this comment

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

Suggested change
pub(crate) fn is_path_a_string(path: &Path) -> bool {
is_path_segments_a_string(path.segments.iter().map(|ps| &ps.ident))
pub(crate) fn is_path_a_string(path: &Path) -> bool {
use syn::parse_quote as q;
path == q!(String)
|| path == q!(alloc::string::String)
|| path == q!(std::string::String)
|| path == q!(::alloc::string::String)
|| path == q!(::std::string::String)

I think is_path_segments_a_string is only used for testing purpose, so we can simply check the path here instead.

This should also help with adding more variants in the future should we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to benchmark both variants, here's the code of the benchmarks:

#![feature(test)]
extern crate test;

use syn::parse_quote as q;
use syn::Path;
use test::Bencher;

fn is_path_segments_a_string<'a, T>(mut iter: impl Iterator<Item = &'a T>) -> bool
where
    T: 'a + ?Sized + PartialEq<str>,
{
    iter.next().map_or(false, |first| {
        first == "String"
            || (first == "std" || first == "alloc")
                && iter.next().map_or(false, |second| second == "string")
                && iter.next().map_or(false, |third| third == "String")
    })
}

fn is_path_a_string_1(path: &Path) -> bool {
    is_path_segments_a_string(path.segments.iter().map(|ps| &ps.ident))
}

fn is_path_a_string_2(path: &Path) -> bool {
    path == &q!(String)
        || path == &q!(alloc::string::String)
        || path == &q!(std::string::String)
        || path == &q!(::alloc::string::String)
        || path == &q!(::std::string::String)
}

#[bench]
fn bench_1(b: &mut Bencher) {
    b.iter(|| {
        is_path_a_string_1(&q!(String));
        is_path_a_string_1(&q!(::std::String));
        is_path_a_string_1(&q!(std::string::String));
        is_path_a_string_1(&q!(Vec));
    })
}

#[bench]
fn bench_2(b: &mut Bencher) {
    b.iter(|| {
        is_path_a_string_2(&q!(String));
        is_path_a_string_2(&q!(::std::String));
        is_path_a_string_2(&q!(std::string::String));
        is_path_a_string_2(&q!(Vec));
    })
}

here's the output of cargo bench:

running 2 tests
test bench_1 ... bench:       4,079 ns/iter (+/- 137)
test bench_2 ... bench:      23,215 ns/iter (+/- 541)

The first approach is as readable as the second one, but is much faster

Copy link
Member

Choose a reason for hiding this comment

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

Whilst I do not believe the readability is the same (imagine if we need to add Rc<str>), if the first approach demonstrates better performance at this moment and it is not difficult to understand, I am fine with the current approach as well.

}

impl TryFrom<Field> for PropField {
Expand Down Expand Up @@ -379,22 +399,33 @@ impl PartialEq for PropField {

#[cfg(test)]
mod tests {
use crate::derive_props::field::is_path_segments_an_option;
use std::iter::once;

use crate::derive_props::field::{is_path_segments_a_string, is_path_segments_an_option};

#[test]
fn all_std_and_core_option_path_seg_return_true() {
assert!(is_path_segments_an_option(
vec!["core".to_owned(), "option".to_owned(), "Option".to_owned()].into_iter()
["core", "option", "Option"].into_iter()
));
assert!(is_path_segments_an_option(
vec!["std".to_owned(), "option".to_owned(), "Option".to_owned()].into_iter()
));
assert!(is_path_segments_an_option(
vec!["Option".to_owned()].into_iter()
["std", "option", "Option"].into_iter()
));
assert!(is_path_segments_an_option(once("Option")));
// why OR instead of XOR
assert!(is_path_segments_an_option(
vec!["Option".to_owned(), "Vec".to_owned(), "Option".to_owned()].into_iter()
["Option", "Vec", "Option"].into_iter()
));
}

#[test]
fn all_std_and_alloc_string_seg_return_true() {
assert!(is_path_segments_a_string(
["alloc", "string", "String"].into_iter()
));
assert!(is_path_segments_a_string(
["std", "string", "String"].into_iter()
));
assert!(is_path_segments_a_string(once("String")));
}
}
29 changes: 21 additions & 8 deletions packages/yew-macro/src/derive_props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ use std::convert::TryInto;
use builder::PropsBuilder;
use field::PropField;
use proc_macro2::{Ident, Span};
use proc_macro_error::emit_warning;
use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream, Result};
use syn::{Attribute, DeriveInput, Generics, Visibility};
use syn::spanned::Spanned;
use syn::{Attribute, DeriveInput, Generics, Type, TypePath, Visibility};
use wrapper::PropsWrapper;

use self::field::is_path_a_string;
use self::generics::to_arguments;

pub struct DerivePropsInput {
Expand Down Expand Up @@ -79,17 +82,27 @@ impl ToTokens for DerivePropsInput {
let Self {
generics,
props_name,
prop_fields,
preserved_attrs,
..
} = self;

for field in prop_fields {
match field.ty() {
Type::Path(TypePath { qself: None, path }) if is_path_a_string(path) => {
emit_warning!(
path.span(),
"storing string values with `String` is not recommended, prefer `AttrValue`.\n\
for further info visit https://yew.rs/docs/concepts/function-components/properties#anti-patterns"
)
}
_ => (),
}
}

// The wrapper is a new struct which wraps required props in `Option`
let wrapper_name = format_ident!("{}Wrapper", props_name, span = Span::mixed_site());
let wrapper = PropsWrapper::new(
&wrapper_name,
generics,
&self.prop_fields,
&self.preserved_attrs,
);
let wrapper = PropsWrapper::new(&wrapper_name, generics, prop_fields, preserved_attrs);
tokens.extend(wrapper.into_token_stream());

// The builder will only build if all required props have been set
Expand All @@ -101,7 +114,7 @@ impl ToTokens for DerivePropsInput {
self,
&wrapper_name,
&check_all_props_name,
&self.preserved_attrs,
preserved_attrs,
);
let generic_args = to_arguments(generics);
tokens.extend(builder.into_token_stream());
Expand Down
1 change: 1 addition & 0 deletions packages/yew-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ fn is_ide_completion() -> bool {
}
}

#[proc_macro_error::proc_macro_error]
#[proc_macro_derive(Properties, attributes(prop_or, prop_or_else, prop_or_default))]
pub fn derive_props(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DerivePropsInput);
Expand Down