Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)
Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx! Add a TUI renderer to the CLI. Improve the CLI build system to be async and parallel. Refactor RSX to allow partial expansion of expressions. Merge autofmt implementations for consistency. Merge the representation of elements and components under the hood. Add a diagnostics system for rsx for improved error messages. Drop interprocess and move to websockets for communication between the CLI and the server. Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts. Add hotreloading to any body of component/for loop/if chain/etc. --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com> Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
181 files changed
+12755
-10806
No files matched your search
+385
-326
@@ -1,224 +1,134 @@
|
||||
//! Parse components into the VNode::Component variant
|
||||
//! ==========================================
|
||||
//!
|
||||
//! We can be reasonably sure that whatever enters this parsing path is in the right format.
|
||||
//! This feature must support
|
||||
//! - [x] Namespaced components
|
||||
//! - [x] Fields
|
||||
//! - [x] Componentbuilder synax
|
||||
//! - [x] Optional commas
|
||||
//! - [ ] Children
|
||||
//! - [ ] Keys
|
||||
//! - [ ] Properties spreading with with `..` syntax
|
||||
|
||||
use self::{location::CallerLocation, util::try_parse_braces};
|
||||
|
||||
use super::*;
|
||||
//! Uses the regular robust RsxBlock parser and then validates the component, emitting errors as
|
||||
//! diagnostics. This was refactored from a straightforward parser to this validation approach so
|
||||
//! that we can emit errors as diagnostics instead of returning results.
|
||||
//!
|
||||
//! Using this approach we can provide *much* better errors as well as partial expansion whereever
|
||||
//! possible.
|
||||
//!
|
||||
//! It does lead to the code actually being larger than it was before, but it should be much easier
|
||||
//! to work with and extend. To add new syntax, we add it to the RsxBlock parser and then add a
|
||||
//! validation step here. This does make using the component as a source of truth not as good, but
|
||||
//! oddly enoughly, we want the tree to actually be capable of being technically invalid. This is not
|
||||
//! usual for building in Rust - you want strongly typed things to be valid - but in this case, we
|
||||
//! want to accept all sorts of malformed input and then provide the best possible error messages.
|
||||
//!
|
||||
//! If you're generally parsing things, you'll just want to parse and then check if it's valid.
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, quote_spanned};
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
ext::IdentExt, spanned::Spanned, token::Brace, AngleBracketedGenericArguments, Error, Expr,
|
||||
Ident, LitStr, PathArguments, Token,
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token, AngleBracketedGenericArguments, Expr, Ident, PathArguments, Result,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Component {
|
||||
pub name: syn::Path,
|
||||
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
|
||||
pub key: Option<IfmtInput>,
|
||||
pub fields: Vec<ComponentField>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
pub brace: Option<syn::token::Brace>,
|
||||
pub location: CallerLocation,
|
||||
errors: Vec<syn::Error>,
|
||||
}
|
||||
|
||||
impl PartialEq for Component {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
&& self.prop_gen_args == other.prop_gen_args
|
||||
&& self.key == other.key
|
||||
&& self.fields == other.fields
|
||||
&& self.children == other.children
|
||||
&& self.manual_props == other.manual_props
|
||||
&& self.brace == other.brace
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Component {}
|
||||
|
||||
impl Hash for Component {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.prop_gen_args.hash(state);
|
||||
self.key.hash(state);
|
||||
self.fields.hash(state);
|
||||
self.children.hash(state);
|
||||
self.manual_props.hash(state);
|
||||
self.brace.hash(state);
|
||||
}
|
||||
pub generics: Option<AngleBracketedGenericArguments>,
|
||||
pub fields: Vec<Attribute>,
|
||||
pub spreads: Vec<Spread>,
|
||||
pub brace: token::Brace,
|
||||
pub children: TemplateBody,
|
||||
pub dyn_idx: DynIdx,
|
||||
pub diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let mut name = stream.parse::<syn::Path>()?;
|
||||
Component::validate_component_path(&name)?;
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut name = input.parse::<syn::Path>()?;
|
||||
let generics = normalize_path(&mut name);
|
||||
|
||||
// extract the path arguments from the path into prop_gen_args
|
||||
let prop_gen_args = normalize_path(&mut name);
|
||||
|
||||
let Ok((brace, content)) = try_parse_braces(stream) else {
|
||||
// If there are no braces, this is an incomplete component. We still parse it so that we can autocomplete it, but we don't need to parse the children
|
||||
return Ok(Self::incomplete(name));
|
||||
if !input.peek(token::Brace) {
|
||||
return Ok(Self::empty(name, generics));
|
||||
};
|
||||
|
||||
let mut fields = Vec::new();
|
||||
let mut children = Vec::new();
|
||||
let mut manual_props = None;
|
||||
let mut key = None;
|
||||
|
||||
while !content.is_empty() {
|
||||
// if we splat into a component then we're merging properties
|
||||
if content.peek(Token![..]) {
|
||||
content.parse::<Token![..]>()?;
|
||||
manual_props = Some(content.parse()?);
|
||||
} else if
|
||||
// Named fields
|
||||
(content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]))
|
||||
// shorthand struct initialization
|
||||
// Not a div {}, mod::Component {}, or web-component {}
|
||||
|| (content.peek(Ident)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:])
|
||||
&& !content.peek2(Token![-]))
|
||||
{
|
||||
// If it is a key, make sure it isn't static and then add it to the component
|
||||
if content.fork().parse::<Ident>()? == "key" {
|
||||
_ = content.parse::<Ident>()?;
|
||||
_ = content.parse::<Token![:]>()?;
|
||||
|
||||
let _key: IfmtInput = content.parse()?;
|
||||
if _key.is_static() {
|
||||
invalid_key!(_key);
|
||||
}
|
||||
key = Some(_key);
|
||||
} else {
|
||||
fields.push(content.parse()?);
|
||||
}
|
||||
} else {
|
||||
children.push(content.parse()?);
|
||||
}
|
||||
|
||||
if content.peek(Token![,]) {
|
||||
let _ = content.parse::<Token![,]>();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
location: CallerLocation::default(),
|
||||
name,
|
||||
prop_gen_args,
|
||||
fields,
|
||||
let RsxBlock {
|
||||
attributes: fields,
|
||||
children,
|
||||
manual_props,
|
||||
brace: Some(brace),
|
||||
key,
|
||||
errors: Vec::new(),
|
||||
})
|
||||
brace,
|
||||
spreads,
|
||||
diagnostics,
|
||||
} = input.parse::<RsxBlock>()?;
|
||||
|
||||
let mut component = Self {
|
||||
dyn_idx: DynIdx::default(),
|
||||
children: TemplateBody::new(children),
|
||||
name,
|
||||
generics,
|
||||
fields,
|
||||
brace,
|
||||
spreads,
|
||||
diagnostics,
|
||||
};
|
||||
|
||||
// We've received a valid rsx block, but it's not necessarily a valid component
|
||||
// validating it will dump diagnostics into the output
|
||||
component.validate_component_path();
|
||||
component.validate_fields();
|
||||
component.validate_key();
|
||||
component.validate_component_spread();
|
||||
|
||||
Ok(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Component {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let Self {
|
||||
name,
|
||||
prop_gen_args,
|
||||
..
|
||||
} = self;
|
||||
let Self { name, generics, .. } = self;
|
||||
|
||||
let builder = self
|
||||
.manual_props
|
||||
.as_ref()
|
||||
.map(|props| self.collect_manual_props(props))
|
||||
.unwrap_or_else(|| self.collect_props());
|
||||
// Create props either from manual props or from the builder approach
|
||||
let props = self.create_props();
|
||||
|
||||
let fn_name = self.fn_name();
|
||||
// Make sure we stringify the component name
|
||||
let fn_name = self.fn_name().to_string();
|
||||
|
||||
let errors = self.errors();
|
||||
// Make sure we emit any errors
|
||||
let diagnostics = &self.diagnostics;
|
||||
|
||||
let component_node = quote_spanned! { name.span() =>
|
||||
tokens.append_all(quote! {
|
||||
dioxus_core::DynamicNode::Component({
|
||||
#[allow(unused_imports)]
|
||||
|
||||
// todo: ensure going through the trait actually works
|
||||
// we want to avoid importing traits
|
||||
// use dioxus_core::prelude::Properties;
|
||||
use dioxus_core::prelude::Properties;
|
||||
(#builder).into_vcomponent(
|
||||
#name #prop_gen_args,
|
||||
let __comp = ({
|
||||
#props
|
||||
}).into_vcomponent(
|
||||
#name #generics,
|
||||
#fn_name
|
||||
)
|
||||
);
|
||||
#diagnostics
|
||||
__comp
|
||||
})
|
||||
};
|
||||
|
||||
let component = if errors.is_empty() {
|
||||
component_node
|
||||
} else {
|
||||
quote_spanned! {
|
||||
name.span() => {
|
||||
#component_node
|
||||
#errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokens.append_all(component);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component {
|
||||
/// Create a new Component
|
||||
pub fn new(
|
||||
name: syn::Path,
|
||||
prop_gen_args: Option<AngleBracketedGenericArguments>,
|
||||
fields: Vec<ComponentField>,
|
||||
children: Vec<BodyNode>,
|
||||
manual_props: Option<Expr>,
|
||||
key: Option<IfmtInput>,
|
||||
brace: syn::token::Brace,
|
||||
) -> Self {
|
||||
Self {
|
||||
errors: vec![],
|
||||
name,
|
||||
prop_gen_args,
|
||||
fields,
|
||||
children,
|
||||
manual_props,
|
||||
brace: Some(brace),
|
||||
key,
|
||||
location: CallerLocation::default(),
|
||||
}
|
||||
}
|
||||
// Make sure this a proper component path (uppercase ident, a path, or contains an underscorea)
|
||||
// This should be validated by the RsxBlock parser when it peeks bodynodes
|
||||
fn validate_component_path(&mut self) {
|
||||
let path = &self.name;
|
||||
|
||||
pub(crate) fn incomplete(name: syn::Path) -> Self {
|
||||
Self {
|
||||
errors: vec![syn::Error::new(
|
||||
name.span(),
|
||||
format!(
|
||||
"Missing braces after component name `{}`",
|
||||
name.segments.last().unwrap().ident
|
||||
),
|
||||
)],
|
||||
name,
|
||||
prop_gen_args: None,
|
||||
fields: Vec::new(),
|
||||
children: Vec::new(),
|
||||
manual_props: None,
|
||||
brace: None,
|
||||
key: None,
|
||||
location: CallerLocation::default(),
|
||||
// First, ensure the path is not a single lowercase ident with no underscores
|
||||
if path.segments.len() == 1 {
|
||||
let seg = path.segments.first().unwrap();
|
||||
if seg.ident.to_string().chars().next().unwrap().is_lowercase()
|
||||
&& !seg.ident.to_string().contains('_')
|
||||
{
|
||||
self.diagnostics.push(seg.ident.span().error(
|
||||
"Component names must be uppercase, contain an underscore, or abe a path.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_component_path(path: &syn::Path) -> Result<()> {
|
||||
// ensure path segments doesn't have PathArguments, only the last
|
||||
// segment is allowed to have one.
|
||||
if path
|
||||
@@ -227,7 +137,9 @@ impl Component {
|
||||
.take(path.segments.len() - 1)
|
||||
.any(|seg| seg.arguments != PathArguments::None)
|
||||
{
|
||||
component_path_cannot_have_arguments!(path.span());
|
||||
self.diagnostics.push(path.span().error(
|
||||
"Component names must not have path arguments. Only the last segment is allowed to have one.",
|
||||
));
|
||||
}
|
||||
|
||||
// ensure last segment only have value of None or AngleBracketed
|
||||
@@ -235,183 +147,330 @@ impl Component {
|
||||
path.segments.last().unwrap().arguments,
|
||||
PathArguments::None | PathArguments::AngleBracketed(_)
|
||||
) {
|
||||
invalid_component_path!(path.span());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key(&self) -> Option<&IfmtInput> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 {
|
||||
let mut toks =
|
||||
quote_spanned! { manual_props.span() => let mut __manual_props = #manual_props; };
|
||||
for field in &self.fields {
|
||||
if field.name == "key" {
|
||||
continue;
|
||||
}
|
||||
let ComponentField { name, content } = field;
|
||||
toks.append_all(
|
||||
quote_spanned! { manual_props.span() => __manual_props.#name = #content; },
|
||||
self.diagnostics.push(
|
||||
path.span()
|
||||
.error("Component names must have no arguments or angle bracketed arguments."),
|
||||
);
|
||||
}
|
||||
toks.append_all(quote_spanned! { manual_props.span() => __manual_props });
|
||||
quote! {{ #toks }}
|
||||
}
|
||||
|
||||
fn collect_props(&self) -> TokenStream2 {
|
||||
// Make sure the spread argument is being used as props spreading
|
||||
fn validate_component_spread(&mut self) {
|
||||
// Next, ensure that there's only one spread argument in the attributes *and* it's the last one
|
||||
for spread in self.spreads.iter().skip(1) {
|
||||
self.diagnostics.push(
|
||||
spread
|
||||
.expr
|
||||
.span()
|
||||
.error("Only one set of manual props is allowed for a component."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure only one key and that the key is not a static str
|
||||
///
|
||||
/// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
|
||||
fn validate_key(&mut self) {
|
||||
let key = self.get_key();
|
||||
|
||||
if let Some(attr) = key {
|
||||
let diagnostic = match &attr.value {
|
||||
AttributeValue::AttrLiteral(ifmt) if ifmt.is_static() => {
|
||||
ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
|
||||
}
|
||||
AttributeValue::AttrLiteral(_) => return,
|
||||
_ => attr
|
||||
.value
|
||||
.span()
|
||||
.error("Key must be in the form of a formatted string like `key: \"{value}\""),
|
||||
};
|
||||
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_key(&self) -> Option<&Attribute> {
|
||||
self.fields
|
||||
.iter()
|
||||
.find(|attr| matches!(&attr.name, AttributeName::BuiltIn(key) if key == "key"))
|
||||
}
|
||||
|
||||
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
||||
/// diagnostic, thankfully
|
||||
///
|
||||
/// Also ensure there's no stringly typed propsa
|
||||
fn validate_fields(&mut self) {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for field in self.fields.iter() {
|
||||
match &field.name {
|
||||
AttributeName::Custom(name) => self.diagnostics.push(
|
||||
name.span()
|
||||
.error("Custom attributes are not supported for Components. Only known attributes are allowed."),
|
||||
),
|
||||
AttributeName::BuiltIn(k) => {
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k);
|
||||
} else {
|
||||
self.diagnostics.push(
|
||||
k.span()
|
||||
.error("Duplicate prop field found. Only one prop field per name is allowed."),
|
||||
);
|
||||
}
|
||||
},
|
||||
AttributeName::Spread(_) => {
|
||||
unreachable!("Spread attributes should be handled in the spread validation step.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the tokens we'll use for the props of the component
|
||||
///
|
||||
/// todo: don't create the tokenstream from scratch and instead dump it into the existing streama
|
||||
fn create_props(&self) -> TokenStream2 {
|
||||
let manual_props = self.manual_props();
|
||||
|
||||
let name = &self.name;
|
||||
let generics = &self.generics;
|
||||
|
||||
let mut toks = match &self.prop_gen_args {
|
||||
Some(gen_args) => quote_spanned! { name.span() => fc_to_builder(#name #gen_args) },
|
||||
None => quote_spanned! { name.span() => fc_to_builder(#name) },
|
||||
let mut tokens = if let Some(props) = manual_props.as_ref() {
|
||||
quote! { let mut __manual_props = #props; }
|
||||
} else {
|
||||
quote! { fc_to_builder(#name #generics) }
|
||||
};
|
||||
for field in &self.fields {
|
||||
toks.append_all(quote! {#field})
|
||||
|
||||
for (name, value) in self.make_field_idents() {
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props.#name = #value; })
|
||||
} else {
|
||||
tokens.append_all(quote! { .#name(#value) })
|
||||
}
|
||||
}
|
||||
|
||||
if !self.children.is_empty() {
|
||||
let renderer = TemplateRenderer::as_tokens(&self.children, None);
|
||||
toks.append_all(quote_spanned! { name.span() => .children( #renderer ) });
|
||||
let children = &self.children;
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props.children = { #children }; })
|
||||
} else {
|
||||
tokens.append_all(quote! { .children( { #children } ) })
|
||||
}
|
||||
}
|
||||
toks.append_all(quote_spanned! { name.span() => .build() });
|
||||
toks
|
||||
}
|
||||
|
||||
fn fn_name(&self) -> String {
|
||||
self.name.segments.last().unwrap().ident.to_string()
|
||||
}
|
||||
|
||||
/// If this element is only partially complete, return the errors that occurred during parsing
|
||||
pub(crate) fn errors(&self) -> TokenStream2 {
|
||||
let Self { errors, .. } = self;
|
||||
|
||||
let mut tokens = quote! {};
|
||||
for error in errors {
|
||||
tokens.append_all(error.to_compile_error());
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props })
|
||||
} else {
|
||||
tokens.append_all(quote! { .build() })
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
// the struct's fields info
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct ComponentField {
|
||||
pub name: Ident,
|
||||
pub content: ContentField,
|
||||
}
|
||||
fn manual_props(&self) -> Option<&Expr> {
|
||||
self.spreads.first().map(|spread| &spread.expr)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub enum ContentField {
|
||||
Shorthand(Ident),
|
||||
ManExpr(Expr),
|
||||
Formatted(IfmtInput),
|
||||
}
|
||||
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
|
||||
self.fields
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
let Attribute { name, value, .. } = attr;
|
||||
|
||||
impl ContentField {
|
||||
fn new(input: ParseStream) -> Result<Self> {
|
||||
if input.peek(LitStr) {
|
||||
let forked = input.fork();
|
||||
let t: LitStr = forked.parse()?;
|
||||
|
||||
// the string literal must either be the end of the input or a followed by a comma
|
||||
let res =
|
||||
match (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) {
|
||||
true => ContentField::Formatted(input.parse()?),
|
||||
false => ContentField::ManExpr(input.parse()?),
|
||||
let attr = match name {
|
||||
AttributeName::BuiltIn(k) => {
|
||||
if k == "key" {
|
||||
return None;
|
||||
}
|
||||
quote! { #k }
|
||||
}
|
||||
AttributeName::Custom(_) => return None,
|
||||
AttributeName::Spread(_) => return None,
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
Ok(ContentField::ManExpr(input.parse()?))
|
||||
Some((attr, value.to_token_stream()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ContentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ContentField::Shorthand(i) => tokens.append_all(quote! { #i }),
|
||||
ContentField::ManExpr(e) => e.to_tokens(tokens),
|
||||
ContentField::Formatted(s) => tokens.append_all(quote! {
|
||||
#s
|
||||
}),
|
||||
fn fn_name(&self) -> Ident {
|
||||
self.name.segments.last().unwrap().ident.clone()
|
||||
}
|
||||
|
||||
fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
diagnostics.push(
|
||||
name.span()
|
||||
.error("Components must have a body")
|
||||
.help("Components must have a body, for example `Component {}`"),
|
||||
);
|
||||
Component {
|
||||
name,
|
||||
generics,
|
||||
brace: token::Brace::default(),
|
||||
fields: vec![],
|
||||
spreads: vec![],
|
||||
children: TemplateBody::new(vec![]),
|
||||
dyn_idx: DynIdx::default(),
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ComponentField {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let name = Ident::parse_any(input)?;
|
||||
|
||||
// if the next token is not a colon, then it's a shorthand field
|
||||
if input.parse::<Token![:]>().is_err() {
|
||||
return Ok(Self {
|
||||
content: ContentField::Shorthand(name.clone()),
|
||||
name,
|
||||
});
|
||||
};
|
||||
|
||||
let content = ContentField::new(input)?;
|
||||
|
||||
if input.peek(LitStr) || input.peek(Ident) {
|
||||
missing_trailing_comma!(content.span());
|
||||
}
|
||||
|
||||
Ok(Self { name, content })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ComponentField { name, content, .. } = self;
|
||||
tokens.append_all(quote! { .#name(#content) })
|
||||
}
|
||||
}
|
||||
|
||||
fn is_literal_foramtted(lit: &LitStr) -> bool {
|
||||
let s = lit.value();
|
||||
let mut chars = s.chars();
|
||||
|
||||
while let Some(next) = chars.next() {
|
||||
if next == '{' {
|
||||
let nen = chars.next();
|
||||
if nen != Some('{') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Normalize the generics of a path
|
||||
///
|
||||
/// Ensure there's a `::` after the last segment if there are generics
|
||||
fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments> {
|
||||
let seg = name.segments.last_mut()?;
|
||||
match seg.arguments.clone() {
|
||||
|
||||
let mut generics = match seg.arguments.clone() {
|
||||
PathArguments::AngleBracketed(args) => {
|
||||
seg.arguments = PathArguments::None;
|
||||
Some(args)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(generics) = generics.as_mut() {
|
||||
use syn::Token;
|
||||
generics.colon2_token = Some(Token));
|
||||
}
|
||||
|
||||
generics
|
||||
}
|
||||
|
||||
impl ComponentField {
|
||||
pub fn can_be_shorthand(&self) -> bool {
|
||||
// If it's a shorthand...
|
||||
if matches!(self.content, ContentField::Shorthand(_)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's in the form of attr: attr, return true
|
||||
if let ContentField::ManExpr(Expr::Path(path)) = &self.content {
|
||||
if path.path.segments.len() == 1 && path.path.segments[0].ident == self.name {
|
||||
return true;
|
||||
/// Ensure we can parse a component
|
||||
#[test]
|
||||
fn parses() {
|
||||
let input = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
..props,
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
false
|
||||
}
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
|
||||
dbg!(component);
|
||||
|
||||
let input_without_manual_props = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input_without_manual_props).unwrap();
|
||||
dbg!(component);
|
||||
}
|
||||
|
||||
/// Ensure we reject invalid forms
|
||||
///
|
||||
/// Maybe want to snapshot the errors?
|
||||
#[test]
|
||||
fn rejects() {
|
||||
let input = quote! {
|
||||
myComponent {
|
||||
key: "value",
|
||||
prop: "value",
|
||||
prop: "other",
|
||||
..props,
|
||||
..other_props,
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
dbg!(component.diagnostics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_properly() {
|
||||
let input = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: 123,
|
||||
..props,
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
println!("{}", component.to_token_stream());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_no_manual_props() {
|
||||
let input_without_manual_props = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
named: "value {something}",
|
||||
prop: "value",
|
||||
count: 1,
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
let component: Component = syn::parse2(input_without_manual_props).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generics_params() {
|
||||
let input_without_children = quote! {
|
||||
Outlet::<R> {}
|
||||
};
|
||||
let component: CallBody = syn::parse2(input_without_children).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generics_no_fish() {
|
||||
let name = quote! { Outlet<R> };
|
||||
let mut p = syn::parse2::<syn::Path>(name).unwrap();
|
||||
let generics = normalize_path(&mut p);
|
||||
assert!(generics.is_some());
|
||||
|
||||
let input_without_children = quote! {
|
||||
div {
|
||||
Component<Generic> {}
|
||||
}
|
||||
};
|
||||
let component: BodyNode = syn::parse2(input_without_children).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fmt_passes_properly() {
|
||||
let input = quote! {
|
||||
Link { to: Route::List, class: "pure-button", "Go back" }
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_components() {
|
||||
let input = quote::quote! {
|
||||
some::cool::Component
|
||||
};
|
||||
|
||||
let _parsed: Component = syn::parse2(input).unwrap();
|
||||
|
||||
let input = quote::quote! {
|
||||
some::cool::C
|
||||
};
|
||||
|
||||
let _parsed: syn::Path = syn::parse2(input).unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user