1 //! Provides the [StyleChecker] visitor to verify the coding style of 2 //! this library. 3 //! 4 //! This is split out so that the implementation itself can be tested 5 //! separately, see test/check_style.rs for how it's used and 6 //! test/style_tests.rs for the implementation tests. 7 //! 8 //! ## Guidelines 9 //! 10 //! The current style is: 11 //! 12 //! * Specific module layout: 13 //! 1. use directives 14 //! 2. typedefs 15 //! 3. structs 16 //! 4. constants 17 //! 5. f! { ... } functions 18 //! 6. extern functions 19 //! 7. modules + pub use 20 //! * No manual deriving Copy/Clone 21 //! * Only one f! per module 22 //! * Multiple s! macros are allowed as long as there isn't a duplicate cfg, 23 //! whether as a standalone attribute (#[cfg]) or in a cfg_if! 24 //! * s! macros should not just have a positive cfg since they should 25 //! just go into the relevant file but combined cfgs with all(...) and 26 //! any(...) are allowed 27 28 use std::collections::HashMap; 29 use std::fs; 30 use std::ops::Deref; 31 use std::path::{Path, PathBuf}; 32 33 use annotate_snippets::{Level, Renderer, Snippet}; 34 use proc_macro2::Span; 35 use syn::parse::{Parse, ParseStream}; 36 use syn::spanned::Spanned; 37 use syn::visit::{self, Visit}; 38 use syn::Token; 39 40 const ALLOWED_REPEATED_MACROS: &[&str] = &["s", "s_no_extra_traits", "s_paren"]; 41 42 pub type Error = Box<dyn std::error::Error>; 43 pub type Result<T> = std::result::Result<T, Error>; 44 45 #[derive(Default)] 46 pub struct StyleChecker { 47 /// The state the style checker is in, used to enforce the module layout. 48 state: State, 49 /// Span of the first item encountered in this state to use in help 50 /// diagnostic text. 51 state_span: Option<Span>, 52 /// The s! macro cfgs we have seen, whether through #[cfg] attributes 53 /// or within the branches of cfg_if! blocks so that we can check for duplicates. 54 seen_s_macro_cfgs: HashMap<String, Span>, 55 /// Span of the first f! macro seen, used to enforce only one f! macro 56 /// per module. 57 first_f_macro: Option<Span>, 58 /// The errors that the style checker has seen. 59 errors: Vec<FileError>, 60 /// Path of the currently active file. 61 path: PathBuf, 62 /// Whether the style checker is currently in an `impl` block. 63 in_impl: bool, 64 } 65 66 /// The part of the module layout we are currently checking. 67 #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 68 enum State { 69 #[default] 70 Start, 71 Imports, 72 Typedefs, 73 Structs, 74 Constants, 75 FunctionDefinitions, 76 Functions, 77 Modules, 78 } 79 80 /// Similar to [syn::ExprIf] except with [syn::Attribute] 81 /// as the condition instead of [syn::Expr]. 82 struct ExprCfgIf { 83 _cond: syn::Attribute, 84 /// A `cfg_if!` branch can only contain items. 85 then_branch: Vec<syn::Item>, 86 else_branch: Option<Box<ExprCfgElse>>, 87 } 88 89 enum ExprCfgElse { 90 /// Final block with no condition `else { /* ... */ }`. 91 Block(Vec<syn::Item>), 92 /// `else if { /* ... */ }` block. 93 If(ExprCfgIf), 94 } 95 96 /// Describes an that occurred error when checking the file 97 /// at the given `path`. Besides the error message, it contains 98 /// additional span information so that we can print nice error messages. 99 #[derive(Debug)] 100 struct FileError { 101 path: PathBuf, 102 span: Span, 103 title: String, 104 msg: String, 105 help: Option<HelpMsg>, 106 } 107 108 /// Help message with an optional span where the help should point to. 109 type HelpMsg = (Option<Span>, String); 110 111 impl StyleChecker { new() -> Self112 pub fn new() -> Self { 113 Self::default() 114 } 115 116 /// Reads and parses the file at the given path and checks 117 /// for any style violations. check_file(&mut self, path: &Path) -> Result<()>118 pub fn check_file(&mut self, path: &Path) -> Result<()> { 119 let contents = fs::read_to_string(path)?; 120 121 self.path = PathBuf::from(path); 122 self.check_string(contents) 123 } 124 check_string(&mut self, contents: String) -> Result<()>125 pub fn check_string(&mut self, contents: String) -> Result<()> { 126 let file = syn::parse_file(&contents)?; 127 self.visit_file(&file); 128 Ok(()) 129 } 130 131 /// Resets the state of the [StyleChecker]. reset_state(&mut self)132 pub fn reset_state(&mut self) { 133 *self = Self { 134 errors: std::mem::take(&mut self.errors), 135 ..Self::default() 136 }; 137 } 138 139 /// Collect all errors into a single error, reporting them if any. finalize(self) -> Result<()>140 pub fn finalize(self) -> Result<()> { 141 if self.errors.is_empty() { 142 return Ok(()); 143 } 144 145 let renderer = Renderer::styled(); 146 for error in self.errors { 147 let source = fs::read_to_string(&error.path)?; 148 149 let mut snippet = Snippet::source(&source) 150 .origin(error.path.to_str().expect("path to be UTF-8")) 151 .fold(true) 152 .annotation(Level::Error.span(error.span.byte_range()).label(&error.msg)); 153 if let Some((help_span, help_msg)) = &error.help { 154 if let Some(help_span) = help_span { 155 snippet = snippet 156 .annotation(Level::Help.span(help_span.byte_range()).label(help_msg)); 157 } 158 } 159 160 let mut msg = Level::Error.title(&error.title).snippet(snippet); 161 if let Some((help_span, help_msg)) = &error.help { 162 if help_span.is_none() { 163 msg = msg.footer(Level::Help.title(help_msg)) 164 } 165 } 166 167 eprintln!("{}", renderer.render(msg)); 168 } 169 170 Err("some tests failed".into()) 171 } 172 set_state(&mut self, new_state: State, span: Span)173 fn set_state(&mut self, new_state: State, span: Span) { 174 if self.state > new_state && !self.in_impl { 175 let help_span = self 176 .state_span 177 .expect("state_span should be set since we are on a second state"); 178 self.error( 179 "incorrect module layout".to_string(), 180 span, 181 format!( 182 "{} found after {} when it belongs before", 183 new_state.desc(), 184 self.state.desc() 185 ), 186 ( 187 Some(help_span), 188 format!( 189 "move the {} to before this {}", 190 new_state.desc(), 191 self.state.desc() 192 ), 193 ), 194 ); 195 } 196 197 if self.state != new_state { 198 self.state = new_state; 199 self.state_span = Some(span); 200 } 201 } 202 203 /// Visit the items inside the [ExprCfgIf], restoring the state after 204 /// each branch. visit_expr_cfg_if(&mut self, expr_cfg_if: &ExprCfgIf)205 fn visit_expr_cfg_if(&mut self, expr_cfg_if: &ExprCfgIf) { 206 let initial_state = self.state; 207 208 for item in &expr_cfg_if.then_branch { 209 self.visit_item(item); 210 } 211 self.state = initial_state; 212 213 if let Some(else_branch) = &expr_cfg_if.else_branch { 214 match else_branch.deref() { 215 ExprCfgElse::Block(items) => { 216 for item in items { 217 self.visit_item(item); 218 } 219 } 220 ExprCfgElse::If(expr_cfg_if) => self.visit_expr_cfg_if(&expr_cfg_if), 221 } 222 } 223 self.state = initial_state; 224 } 225 226 /// If we see a normal s! macro without any attributes we just need 227 /// to check if there are any duplicates. handle_s_macro_no_attrs(&mut self, item_macro: &syn::ItemMacro)228 fn handle_s_macro_no_attrs(&mut self, item_macro: &syn::ItemMacro) { 229 let span = item_macro.span(); 230 match self.seen_s_macro_cfgs.get("") { 231 Some(seen_span) => { 232 self.error( 233 "duplicate s! macro".to_string(), 234 span, 235 format!("other s! macro"), 236 (Some(*seen_span), "combine the two".to_string()), 237 ); 238 } 239 None => { 240 self.seen_s_macro_cfgs.insert(String::new(), span); 241 } 242 } 243 } 244 245 /// If an s! macro has attributes we check for any duplicates as well 246 /// as if they are standalone positive cfgs that would be better 247 /// in a separate file. handle_s_macro_with_attrs(&mut self, item_macro: &syn::ItemMacro)248 fn handle_s_macro_with_attrs(&mut self, item_macro: &syn::ItemMacro) { 249 for attr in &item_macro.attrs { 250 let Ok(meta_list) = attr.meta.require_list() else { 251 continue; 252 }; 253 254 if meta_list.path.is_ident("cfg") { 255 let span = meta_list.span(); 256 let meta_str = meta_list.tokens.to_string(); 257 258 match self.seen_s_macro_cfgs.get(&meta_str) { 259 Some(seen_span) => { 260 self.error( 261 "duplicate #[cfg] for s! macro".to_string(), 262 span, 263 "duplicated #[cfg]".to_string(), 264 (Some(*seen_span), "combine the two".to_string()), 265 ); 266 } 267 None => { 268 self.seen_s_macro_cfgs.insert(meta_str.clone(), span); 269 } 270 } 271 272 if !meta_str.starts_with("not") 273 && !meta_str.starts_with("any") 274 && !meta_str.starts_with("all") 275 { 276 self.error( 277 "positive #[cfg] for s! macro".to_string(), 278 span, 279 String::new(), 280 (None, "move it to the relevant file".to_string()), 281 ); 282 } 283 } 284 } 285 } 286 push_error(&mut self, title: String, span: Span, msg: String, help: Option<HelpMsg>)287 fn push_error(&mut self, title: String, span: Span, msg: String, help: Option<HelpMsg>) { 288 self.errors.push(FileError { 289 path: self.path.clone(), 290 title, 291 span, 292 msg, 293 help, 294 }); 295 } 296 error(&mut self, title: String, span: Span, msg: String, help: HelpMsg)297 fn error(&mut self, title: String, span: Span, msg: String, help: HelpMsg) { 298 self.push_error(title, span, msg, Some(help)); 299 } 300 } 301 302 impl<'ast> Visit<'ast> for StyleChecker { 303 /// Visit all items; most just update our current state but some also 304 /// perform additional checks like for the s! macro. visit_item_use(&mut self, item_use: &'ast syn::ItemUse)305 fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) { 306 let span = item_use.span(); 307 let new_state = if matches!(item_use.vis, syn::Visibility::Public(_)) { 308 State::Modules 309 } else { 310 State::Imports 311 }; 312 self.set_state(new_state, span); 313 314 visit::visit_item_use(self, item_use); 315 } 316 visit_item_const(&mut self, item_const: &'ast syn::ItemConst)317 fn visit_item_const(&mut self, item_const: &'ast syn::ItemConst) { 318 let span = item_const.span(); 319 self.set_state(State::Constants, span); 320 321 visit::visit_item_const(self, item_const); 322 } 323 visit_item_impl(&mut self, item_impl: &'ast syn::ItemImpl)324 fn visit_item_impl(&mut self, item_impl: &'ast syn::ItemImpl) { 325 self.in_impl = true; 326 visit::visit_item_impl(self, item_impl); 327 self.in_impl = false; 328 } 329 visit_item_struct(&mut self, item_struct: &'ast syn::ItemStruct)330 fn visit_item_struct(&mut self, item_struct: &'ast syn::ItemStruct) { 331 let span = item_struct.span(); 332 self.set_state(State::Structs, span); 333 334 visit::visit_item_struct(self, item_struct); 335 } 336 visit_item_type(&mut self, item_type: &'ast syn::ItemType)337 fn visit_item_type(&mut self, item_type: &'ast syn::ItemType) { 338 let span = item_type.span(); 339 self.set_state(State::Typedefs, span); 340 341 visit::visit_item_type(self, item_type); 342 } 343 344 /// Checks s! macros for any duplicate cfgs and whether they are 345 /// just positive #[cfg(...)] attributes. We need [syn::ItemMacro] 346 /// instead of [syn::Macro] because it contains the attributes. visit_item_macro(&mut self, item_macro: &'ast syn::ItemMacro)347 fn visit_item_macro(&mut self, item_macro: &'ast syn::ItemMacro) { 348 if item_macro.mac.path.is_ident("s") { 349 if item_macro.attrs.is_empty() { 350 self.handle_s_macro_no_attrs(item_macro); 351 } else { 352 self.handle_s_macro_with_attrs(item_macro); 353 } 354 } 355 356 visit::visit_item_macro(self, item_macro); 357 } 358 visit_macro(&mut self, mac: &'ast syn::Macro)359 fn visit_macro(&mut self, mac: &'ast syn::Macro) { 360 let span = mac.span(); 361 if mac.path.is_ident("cfg_if") { 362 let expr_cfg_if: ExprCfgIf = mac 363 .parse_body() 364 .expect("cfg_if! should be parsed since it compiled"); 365 366 self.visit_expr_cfg_if(&expr_cfg_if); 367 } else { 368 let new_state = 369 if mac.path.get_ident().is_some_and(|ident| { 370 ALLOWED_REPEATED_MACROS.contains(&ident.to_string().as_str()) 371 }) { 372 // multiple macros of this type are allowed 373 State::Structs 374 } else if mac.path.is_ident("f") { 375 match self.first_f_macro { 376 Some(f_macro_span) => { 377 self.error( 378 "multiple f! macros in one module".to_string(), 379 span, 380 "other f! macro".to_string(), 381 ( 382 Some(f_macro_span), 383 "combine it with this f! macro".to_string(), 384 ), 385 ); 386 } 387 None => { 388 self.first_f_macro = Some(span); 389 } 390 } 391 State::FunctionDefinitions 392 } else { 393 self.state 394 }; 395 self.set_state(new_state, span); 396 } 397 398 visit::visit_macro(self, mac); 399 } 400 visit_item_foreign_mod(&mut self, item_foreign_mod: &'ast syn::ItemForeignMod)401 fn visit_item_foreign_mod(&mut self, item_foreign_mod: &'ast syn::ItemForeignMod) { 402 let span = item_foreign_mod.span(); 403 self.set_state(State::Functions, span); 404 405 visit::visit_item_foreign_mod(self, item_foreign_mod); 406 } 407 visit_item_mod(&mut self, item_mod: &'ast syn::ItemMod)408 fn visit_item_mod(&mut self, item_mod: &'ast syn::ItemMod) { 409 let span = item_mod.span(); 410 self.set_state(State::Modules, span); 411 412 visit::visit_item_mod(self, item_mod); 413 } 414 visit_meta_list(&mut self, meta_list: &'ast syn::MetaList)415 fn visit_meta_list(&mut self, meta_list: &'ast syn::MetaList) { 416 let span = meta_list.span(); 417 let meta_str = meta_list.tokens.to_string(); 418 if meta_list.path.is_ident("derive") 419 && (meta_str.contains("Copy") || meta_str.contains("Clone")) 420 { 421 self.error( 422 "impl Copy and Clone manually".to_string(), 423 span, 424 "found manual implementation of Copy and/or Clone".to_string(), 425 (None, "use one of the s! macros instead".to_string()), 426 ); 427 } 428 429 visit::visit_meta_list(self, meta_list); 430 } 431 } 432 433 impl Parse for ExprCfgIf { parse(input: ParseStream) -> syn::Result<Self>434 fn parse(input: ParseStream) -> syn::Result<Self> { 435 input.parse::<Token![if]>()?; 436 let cond = input 437 .call(syn::Attribute::parse_outer)? 438 .into_iter() 439 .next() 440 .expect("an attribute should be present since it compiled"); 441 442 let content; 443 syn::braced!(content in input); 444 let mut then_branch = Vec::new(); 445 while !content.is_empty() { 446 let mut value = content.parse()?; 447 if let syn::Item::Macro(item_macro) = &mut value { 448 item_macro.attrs.push(cond.clone()); 449 } 450 then_branch.push(value); 451 } 452 453 let mut else_branch = None; 454 if input.peek(Token![else]) { 455 input.parse::<Token![else]>()?; 456 457 if input.peek(Token![if]) { 458 else_branch = Some(Box::new(ExprCfgElse::If(input.parse()?))); 459 } else { 460 let content; 461 syn::braced!(content in input); 462 let mut items = Vec::new(); 463 while !content.is_empty() { 464 items.push(content.parse()?); 465 } 466 else_branch = Some(Box::new(ExprCfgElse::Block(items))); 467 } 468 } 469 Ok(Self { 470 _cond: cond, 471 then_branch, 472 else_branch, 473 }) 474 } 475 } 476 477 impl State { desc(&self) -> &str478 fn desc(&self) -> &str { 479 match *self { 480 State::Start => "start", 481 State::Imports => "import", 482 State::Typedefs => "typedef", 483 State::Structs => "struct", 484 State::Constants => "constant", 485 State::FunctionDefinitions => "function definition", 486 State::Functions => "extern function", 487 State::Modules => "module", 488 } 489 } 490 } 491