//! Provides the [StyleChecker] visitor to verify the coding style of //! this library. //! //! This is split out so that the implementation itself can be tested //! separately, see test/check_style.rs for how it's used and //! test/style_tests.rs for the implementation tests. //! //! ## Guidelines //! //! The current style is: //! //! * Specific module layout: //! 1. use directives //! 2. typedefs //! 3. structs //! 4. constants //! 5. f! { ... } functions //! 6. extern functions //! 7. modules + pub use //! * No manual deriving Copy/Clone //! * Only one f! per module //! * Multiple s! macros are allowed as long as there isn't a duplicate cfg, //! whether as a standalone attribute (#[cfg]) or in a cfg_if! //! * s! macros should not just have a positive cfg since they should //! just go into the relevant file but combined cfgs with all(...) and //! any(...) are allowed use std::collections::HashMap; use std::fs; use std::ops::Deref; use std::path::{Path, PathBuf}; use annotate_snippets::{Level, Renderer, Snippet}; use proc_macro2::Span; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::visit::{self, Visit}; use syn::Token; const ALLOWED_REPEATED_MACROS: &[&str] = &["s", "s_no_extra_traits", "s_paren"]; pub type Error = Box; pub type Result = std::result::Result; #[derive(Default)] pub struct StyleChecker { /// The state the style checker is in, used to enforce the module layout. state: State, /// Span of the first item encountered in this state to use in help /// diagnostic text. state_span: Option, /// The s! macro cfgs we have seen, whether through #[cfg] attributes /// or within the branches of cfg_if! blocks so that we can check for duplicates. seen_s_macro_cfgs: HashMap, /// Span of the first f! macro seen, used to enforce only one f! macro /// per module. first_f_macro: Option, /// The errors that the style checker has seen. errors: Vec, /// Path of the currently active file. path: PathBuf, /// Whether the style checker is currently in an `impl` block. in_impl: bool, } /// The part of the module layout we are currently checking. #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum State { #[default] Start, Imports, Typedefs, Structs, Constants, FunctionDefinitions, Functions, Modules, } /// Similar to [syn::ExprIf] except with [syn::Attribute] /// as the condition instead of [syn::Expr]. struct ExprCfgIf { _cond: syn::Attribute, /// A `cfg_if!` branch can only contain items. then_branch: Vec, else_branch: Option>, } enum ExprCfgElse { /// Final block with no condition `else { /* ... */ }`. Block(Vec), /// `else if { /* ... */ }` block. If(ExprCfgIf), } /// Describes an that occurred error when checking the file /// at the given `path`. Besides the error message, it contains /// additional span information so that we can print nice error messages. #[derive(Debug)] struct FileError { path: PathBuf, span: Span, title: String, msg: String, help: Option, } /// Help message with an optional span where the help should point to. type HelpMsg = (Option, String); impl StyleChecker { pub fn new() -> Self { Self::default() } /// Reads and parses the file at the given path and checks /// for any style violations. pub fn check_file(&mut self, path: &Path) -> Result<()> { let contents = fs::read_to_string(path)?; self.path = PathBuf::from(path); self.check_string(contents) } pub fn check_string(&mut self, contents: String) -> Result<()> { let file = syn::parse_file(&contents)?; self.visit_file(&file); Ok(()) } /// Resets the state of the [StyleChecker]. pub fn reset_state(&mut self) { *self = Self { errors: std::mem::take(&mut self.errors), ..Self::default() }; } /// Collect all errors into a single error, reporting them if any. pub fn finalize(self) -> Result<()> { if self.errors.is_empty() { return Ok(()); } let renderer = Renderer::styled(); for error in self.errors { let source = fs::read_to_string(&error.path)?; let mut snippet = Snippet::source(&source) .origin(error.path.to_str().expect("path to be UTF-8")) .fold(true) .annotation(Level::Error.span(error.span.byte_range()).label(&error.msg)); if let Some((help_span, help_msg)) = &error.help { if let Some(help_span) = help_span { snippet = snippet .annotation(Level::Help.span(help_span.byte_range()).label(help_msg)); } } let mut msg = Level::Error.title(&error.title).snippet(snippet); if let Some((help_span, help_msg)) = &error.help { if help_span.is_none() { msg = msg.footer(Level::Help.title(help_msg)) } } eprintln!("{}", renderer.render(msg)); } Err("some tests failed".into()) } fn set_state(&mut self, new_state: State, span: Span) { if self.state > new_state && !self.in_impl { let help_span = self .state_span .expect("state_span should be set since we are on a second state"); self.error( "incorrect module layout".to_string(), span, format!( "{} found after {} when it belongs before", new_state.desc(), self.state.desc() ), ( Some(help_span), format!( "move the {} to before this {}", new_state.desc(), self.state.desc() ), ), ); } if self.state != new_state { self.state = new_state; self.state_span = Some(span); } } /// Visit the items inside the [ExprCfgIf], restoring the state after /// each branch. fn visit_expr_cfg_if(&mut self, expr_cfg_if: &ExprCfgIf) { let initial_state = self.state; for item in &expr_cfg_if.then_branch { self.visit_item(item); } self.state = initial_state; if let Some(else_branch) = &expr_cfg_if.else_branch { match else_branch.deref() { ExprCfgElse::Block(items) => { for item in items { self.visit_item(item); } } ExprCfgElse::If(expr_cfg_if) => self.visit_expr_cfg_if(&expr_cfg_if), } } self.state = initial_state; } /// If we see a normal s! macro without any attributes we just need /// to check if there are any duplicates. fn handle_s_macro_no_attrs(&mut self, item_macro: &syn::ItemMacro) { let span = item_macro.span(); match self.seen_s_macro_cfgs.get("") { Some(seen_span) => { self.error( "duplicate s! macro".to_string(), span, format!("other s! macro"), (Some(*seen_span), "combine the two".to_string()), ); } None => { self.seen_s_macro_cfgs.insert(String::new(), span); } } } /// If an s! macro has attributes we check for any duplicates as well /// as if they are standalone positive cfgs that would be better /// in a separate file. fn handle_s_macro_with_attrs(&mut self, item_macro: &syn::ItemMacro) { for attr in &item_macro.attrs { let Ok(meta_list) = attr.meta.require_list() else { continue; }; if meta_list.path.is_ident("cfg") { let span = meta_list.span(); let meta_str = meta_list.tokens.to_string(); match self.seen_s_macro_cfgs.get(&meta_str) { Some(seen_span) => { self.error( "duplicate #[cfg] for s! macro".to_string(), span, "duplicated #[cfg]".to_string(), (Some(*seen_span), "combine the two".to_string()), ); } None => { self.seen_s_macro_cfgs.insert(meta_str.clone(), span); } } if !meta_str.starts_with("not") && !meta_str.starts_with("any") && !meta_str.starts_with("all") { self.error( "positive #[cfg] for s! macro".to_string(), span, String::new(), (None, "move it to the relevant file".to_string()), ); } } } } fn push_error(&mut self, title: String, span: Span, msg: String, help: Option) { self.errors.push(FileError { path: self.path.clone(), title, span, msg, help, }); } fn error(&mut self, title: String, span: Span, msg: String, help: HelpMsg) { self.push_error(title, span, msg, Some(help)); } } impl<'ast> Visit<'ast> for StyleChecker { /// Visit all items; most just update our current state but some also /// perform additional checks like for the s! macro. fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) { let span = item_use.span(); let new_state = if matches!(item_use.vis, syn::Visibility::Public(_)) { State::Modules } else { State::Imports }; self.set_state(new_state, span); visit::visit_item_use(self, item_use); } fn visit_item_const(&mut self, item_const: &'ast syn::ItemConst) { let span = item_const.span(); self.set_state(State::Constants, span); visit::visit_item_const(self, item_const); } fn visit_item_impl(&mut self, item_impl: &'ast syn::ItemImpl) { self.in_impl = true; visit::visit_item_impl(self, item_impl); self.in_impl = false; } fn visit_item_struct(&mut self, item_struct: &'ast syn::ItemStruct) { let span = item_struct.span(); self.set_state(State::Structs, span); visit::visit_item_struct(self, item_struct); } fn visit_item_type(&mut self, item_type: &'ast syn::ItemType) { let span = item_type.span(); self.set_state(State::Typedefs, span); visit::visit_item_type(self, item_type); } /// Checks s! macros for any duplicate cfgs and whether they are /// just positive #[cfg(...)] attributes. We need [syn::ItemMacro] /// instead of [syn::Macro] because it contains the attributes. fn visit_item_macro(&mut self, item_macro: &'ast syn::ItemMacro) { if item_macro.mac.path.is_ident("s") { if item_macro.attrs.is_empty() { self.handle_s_macro_no_attrs(item_macro); } else { self.handle_s_macro_with_attrs(item_macro); } } visit::visit_item_macro(self, item_macro); } fn visit_macro(&mut self, mac: &'ast syn::Macro) { let span = mac.span(); if mac.path.is_ident("cfg_if") { let expr_cfg_if: ExprCfgIf = mac .parse_body() .expect("cfg_if! should be parsed since it compiled"); self.visit_expr_cfg_if(&expr_cfg_if); } else { let new_state = if mac.path.get_ident().is_some_and(|ident| { ALLOWED_REPEATED_MACROS.contains(&ident.to_string().as_str()) }) { // multiple macros of this type are allowed State::Structs } else if mac.path.is_ident("f") { match self.first_f_macro { Some(f_macro_span) => { self.error( "multiple f! macros in one module".to_string(), span, "other f! macro".to_string(), ( Some(f_macro_span), "combine it with this f! macro".to_string(), ), ); } None => { self.first_f_macro = Some(span); } } State::FunctionDefinitions } else { self.state }; self.set_state(new_state, span); } visit::visit_macro(self, mac); } fn visit_item_foreign_mod(&mut self, item_foreign_mod: &'ast syn::ItemForeignMod) { let span = item_foreign_mod.span(); self.set_state(State::Functions, span); visit::visit_item_foreign_mod(self, item_foreign_mod); } fn visit_item_mod(&mut self, item_mod: &'ast syn::ItemMod) { let span = item_mod.span(); self.set_state(State::Modules, span); visit::visit_item_mod(self, item_mod); } fn visit_meta_list(&mut self, meta_list: &'ast syn::MetaList) { let span = meta_list.span(); let meta_str = meta_list.tokens.to_string(); if meta_list.path.is_ident("derive") && (meta_str.contains("Copy") || meta_str.contains("Clone")) { self.error( "impl Copy and Clone manually".to_string(), span, "found manual implementation of Copy and/or Clone".to_string(), (None, "use one of the s! macros instead".to_string()), ); } visit::visit_meta_list(self, meta_list); } } impl Parse for ExprCfgIf { fn parse(input: ParseStream) -> syn::Result { input.parse::()?; let cond = input .call(syn::Attribute::parse_outer)? .into_iter() .next() .expect("an attribute should be present since it compiled"); let content; syn::braced!(content in input); let mut then_branch = Vec::new(); while !content.is_empty() { let mut value = content.parse()?; if let syn::Item::Macro(item_macro) = &mut value { item_macro.attrs.push(cond.clone()); } then_branch.push(value); } let mut else_branch = None; if input.peek(Token![else]) { input.parse::()?; if input.peek(Token![if]) { else_branch = Some(Box::new(ExprCfgElse::If(input.parse()?))); } else { let content; syn::braced!(content in input); let mut items = Vec::new(); while !content.is_empty() { items.push(content.parse()?); } else_branch = Some(Box::new(ExprCfgElse::Block(items))); } } Ok(Self { _cond: cond, then_branch, else_branch, }) } } impl State { fn desc(&self) -> &str { match *self { State::Start => "start", State::Imports => "import", State::Typedefs => "typedef", State::Structs => "struct", State::Constants => "constant", State::FunctionDefinitions => "function definition", State::Functions => "extern function", State::Modules => "module", } } }