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