Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/yew-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ rustversion = "1"
[dev-dependencies]
trybuild = "1"
yew = { path = "../yew" }
rand = "0.9"

[lints]
workspace = true
17 changes: 7 additions & 10 deletions packages/yew-macro/src/function_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use syn::{
};

use crate::hook::BodyRewriter;
use crate::DisplayExt;

#[derive(Clone)]
pub struct FunctionComponent {
Expand Down Expand Up @@ -157,29 +158,25 @@ impl FunctionComponent {
fn filter_attrs_for_component_struct(&self) -> Vec<Attribute> {
self.attrs
.iter()
.filter_map(|m| {
.filter(|m| {
m.path()
.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
"doc" | "allow" => Some(m.clone()),
_ => None,
})
.is_some_and(|ident| (ident.eq_str("doc") || ident.eq_str("allow")))
})
.cloned()
.collect()
}

/// Filters attributes that should be copied to the component impl block.
fn filter_attrs_for_component_impl(&self) -> Vec<Attribute> {
self.attrs
.iter()
.filter_map(|m| {
.filter(|m| {
m.path()
.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
"allow" => Some(m.clone()),
_ => None,
})
.is_some_and(|ident| ident.eq_str("allow"))
})
.cloned()
.collect()
}

Expand Down
6 changes: 4 additions & 2 deletions packages/yew-macro/src/hook/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use syn::{
ExprMatch, ExprWhile, Ident, Item,
};

use crate::DisplayExt;

#[derive(Debug)]
pub struct BodyRewriter {
branch_lock: Arc<Mutex<()>>,
Expand Down Expand Up @@ -43,7 +45,7 @@ impl VisitMut for BodyRewriter {
// 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 m.starts_with("use_") {
if self.is_branched() {
emit_error!(
m,
Expand All @@ -69,7 +71,7 @@ impl VisitMut for BodyRewriter {
match &mut *i {
Expr::Macro(m) => {
if let Some(ident) = m.mac.path.segments.last().as_ref().map(|m| &m.ident) {
if ident.to_string().starts_with("use_") {
if ident.starts_with("use_") {
if self.is_branched() {
emit_error!(
ident,
Expand Down
4 changes: 3 additions & 1 deletion packages/yew-macro/src/hook/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ mod signature;
pub use body::BodyRewriter;
use signature::HookSignature;

use crate::DisplayExt;

#[derive(Clone)]
pub struct HookFn {
inner: ItemFn,
Expand Down Expand Up @@ -42,7 +44,7 @@ impl Parse for HookFn {
emit_error!(sig.unsafety, "unsafe functions can't be hooks");
}

if !sig.ident.to_string().starts_with("use_") {
if !sig.ident.starts_with("use_") {
emit_error!(sig.ident, "hooks must have a name starting with `use_`");
}

Expand Down
37 changes: 37 additions & 0 deletions packages/yew-macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::{Display, Write};

use proc_macro2::Span;
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::discouraged::Speculative;
Expand All @@ -9,6 +11,41 @@ use super::{HtmlChildrenTree, TagTokens};
use crate::is_ide_completion;
use crate::props::ComponentProps;

/// Returns `true` if `s` looks like a component name
pub fn is_component_name(s: impl Display) -> bool {
struct X {
is_ide_completion: bool,
empty: bool,
}

impl Write for X {
fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
if self.empty {
self.empty = chunk.is_empty();
if !self.is_ide_completion
&& chunk
.bytes()
.next()
.is_some_and(|b| !b.is_ascii_uppercase())
{
return Err(std::fmt::Error);
}
}
chunk
.bytes()
.any(|b| b.is_ascii_uppercase())
.then_some(())
.ok_or(std::fmt::Error)
}
}

let mut writer = X {
is_ide_completion: is_ide_completion(),
empty: true,
};
write!(writer, "{s}").is_ok_and(|_| !writer.empty)
Copy link
Member

Choose a reason for hiding this comment

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

This deserves a better comment and improved naming, as it is very hard to follow currently.

A name is, at the moment, assumed to be a component name if it

  • starts with an ascii uppercase letter
  • OR ide completion is enable and it contains any ascii uppercase

Both of these are rough approximations, for example I think the second condition could be changed to match a specific identifier passed via the RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER env variable, and I'm not even sure how much adoption that experiment got at this point.

}

pub struct HtmlComponent {
ty: Type,
props: ComponentProps,
Expand Down
15 changes: 2 additions & 13 deletions packages/yew-macro/src/html_tree/html_dashed_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::spanned::Spanned;
use syn::{LitStr, Token};

use crate::stringify::Stringify;
use crate::{non_capitalized_ascii, Peek};
use crate::{DisplayExt, Peek};

#[derive(Clone, PartialEq, Eq)]
pub struct HtmlDashedName {
Expand All @@ -18,17 +18,6 @@ pub struct HtmlDashedName {
}

impl HtmlDashedName {
/// Checks if this name is equal to the provided item (which can be anything implementing
/// `Into<String>`).
pub fn eq_ignore_ascii_case<S>(&self, other: S) -> bool
where
S: Into<String>,
{
let mut s = other.into();
s.make_ascii_lowercase();
s == self.to_ascii_lowercase_string()
}

pub fn to_ascii_lowercase_string(&self) -> String {
let mut s = self.to_string();
s.make_ascii_lowercase();
Expand All @@ -53,7 +42,7 @@ impl fmt::Display for HtmlDashedName {
impl Peek<'_, Self> for HtmlDashedName {
fn peek(cursor: Cursor) -> Option<(Self, Cursor)> {
let (name, cursor) = cursor.ident()?;
if !non_capitalized_ascii(&name.to_string()) {
if !name.is_non_capitalized_ascii() {
return None;
}

Expand Down
11 changes: 5 additions & 6 deletions packages/yew-macro/src/html_tree/html_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::{Expr, Ident, Lit, LitStr, Token};
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
use crate::props::{ElementProps, Prop, PropDirective};
use crate::stringify::{Stringify, Value};
use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue};
use crate::{is_ide_completion, DisplayExt, Peek, PeekValue};

fn is_normalised_element_name(name: &str) -> bool {
match name {
Expand Down Expand Up @@ -457,8 +457,7 @@ impl ToTokens for HtmlElement {
#[cfg(nightly_yew)]
let invalid_void_tag_msg_start = {
let span = vtag.span().unwrap();
let source_file = span.source_file().path();
let source_file = source_file.display();
let source_file = span.file();
let start = span.start();
format!("[{}:{}:{}] ", source_file, start.line(), start.column())
};
Expand Down Expand Up @@ -668,13 +667,13 @@ impl PeekValue<TagKey> for HtmlElementOpen {
let (tag_key, cursor) = TagName::peek(cursor)?;
if let TagKey::Lit(name) = &tag_key {
// Avoid parsing `<key=[...]>` as an element. It needs to be parsed as an `HtmlList`.
if name.to_string() == "key" {
if name.eq_str("key") {
let (punct, _) = cursor.punct()?;
// ... unless it isn't followed by a '='. `<key></key>` is a valid element!
if punct.as_char() == '=' {
return None;
}
} else if !non_capitalized_ascii(&name.to_string()) {
} else if !name.is_non_capitalized_ascii() {
return None;
}
}
Expand Down Expand Up @@ -743,7 +742,7 @@ impl PeekValue<TagKey> for HtmlElementClose {
}

let (tag_key, cursor) = TagName::peek(cursor)?;
if matches!(&tag_key, TagKey::Lit(name) if !non_capitalized_ascii(&name.to_string())) {
if matches!(&tag_key, TagKey::Lit(name) if !name.is_non_capitalized_ascii()) {
return None;
}

Expand Down
7 changes: 2 additions & 5 deletions packages/yew-macro/src/html_tree/html_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use syn::Lit;

use super::ToNodeIterator;
use crate::stringify::Stringify;
use crate::PeekValue;
use crate::{DisplayExt, PeekValue};

pub enum HtmlNode {
Literal(Box<Lit>),
Expand Down Expand Up @@ -44,10 +44,7 @@ impl PeekValue<()> for HtmlNode {
fn peek(cursor: Cursor) -> Option<()> {
cursor.literal().map(|_| ()).or_else(|| {
let (ident, _) = cursor.ident()?;
match ident.to_string().as_str() {
"true" | "false" => Some(()),
_ => None,
}
(ident.eq_str("true") || ident.eq_str("false")).then_some(())
})
}
}
Expand Down
9 changes: 5 additions & 4 deletions packages/yew-macro/src/html_tree/lint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use syn::spanned::Spanned;
use super::html_element::{HtmlElement, TagName};
use super::HtmlTree;
use crate::props::{ElementProps, Prop};
use crate::DisplayExt;

/// Lints HTML elements to check if they are well formed. If the element is not well-formed, then
/// use `proc-macro-error` (and the `emit_warning!` macro) to produce a warning. At present, these
Expand Down Expand Up @@ -49,7 +50,7 @@ fn get_attribute<'a>(props: &'a ElementProps, name: &str) -> Option<&'a Prop> {
props
.attributes
.iter()
.find(|item| item.label.eq_ignore_ascii_case(name))
.find(|item| item.label.eq_str_ignore_ascii_case(name))
}

/// Lints to check if anchor (`<a>`) tags have valid `href` attributes defined.
Expand All @@ -58,7 +59,7 @@ pub struct AHrefLint;
impl Lint for AHrefLint {
fn lint(element: &HtmlElement) {
if let TagName::Lit(ref tag_name) = element.name {
if !tag_name.eq_ignore_ascii_case("a") {
if !tag_name.eq_str_ignore_ascii_case("a") {
return;
};
if let Some(prop) = get_attribute(&element.props, "href") {
Expand All @@ -80,7 +81,7 @@ impl Lint for AHrefLint {
};
} else {
emit_warning!(
quote::quote! {#tag_name}.span(),
tag_name.span(),
"All `<a>` elements should have a `href` attribute. This makes it possible \
for assistive technologies to correctly interpret what your links point to. \
https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#more_on_links"
Expand All @@ -96,7 +97,7 @@ pub struct ImgAltLint;
impl Lint for ImgAltLint {
fn lint(element: &HtmlElement) {
if let super::html_element::TagName::Lit(ref tag_name) = element.name {
if !tag_name.eq_ignore_ascii_case("img") {
if !tag_name.eq_str_ignore_ascii_case("img") {
return;
};
if get_attribute(&element.props, "alt").is_none() {
Expand Down
10 changes: 3 additions & 7 deletions packages/yew-macro/src/html_tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{braced, token, Token};

use crate::{is_ide_completion, PeekValue};
use crate::PeekValue;

mod html_block;
mod html_component;
Expand All @@ -20,7 +20,7 @@ mod lint;
mod tag;

use html_block::HtmlBlock;
use html_component::HtmlComponent;
use html_component::{is_component_name, HtmlComponent};
pub use html_dashed_name::HtmlDashedName;
use html_element::HtmlElement;
use html_if::HtmlIf;
Expand Down Expand Up @@ -97,14 +97,10 @@ impl HtmlTree {
Some(HtmlType::Component)
} else if input.peek(Ident::peek_any) {
let ident = Ident::parse_any(&input).ok()?;
let ident_str = ident.to_string();

if input.peek(Token![=]) || (input.peek(Token![?]) && input.peek2(Token![=])) {
Some(HtmlType::List)
} else if ident_str.chars().next().unwrap().is_ascii_uppercase()
|| input.peek(Token![::])
|| is_ide_completion() && ident_str.chars().any(|c| c.is_ascii_uppercase())
{
} else if input.peek(Token![::]) || is_component_name(&ident) {
Some(HtmlType::Component)
} else {
Some(HtmlType::Element)
Expand Down
2 changes: 1 addition & 1 deletion packages/yew-macro/src/html_tree/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn span_eq_hack(a: &Span, b: &Span) -> bool {
fn error_replace_span(err: syn::Error, from: Span, to: impl ToTokens) -> syn::Error {
let err_it = err.into_iter().map(|err| {
if span_eq_hack(&err.span(), &from) {
syn::Error::new_spanned(&to, err.to_string())
syn::Error::new_spanned(&to, err)
} else {
err
}
Expand Down
Loading
Loading