Skip to main content

tidy/style/
directive.rs

1use std::cell::Cell;
2use std::mem;
3use std::path::Path;
4use std::rc::Rc;
5
6use crate::diagnostics::RunningCheck;
7
8macro_rules! configurable_checks {
9        (@parse_error_message doc = $doc:literal) => {
10            Some($doc)
11        };
12        (@parse_error_message dont_check_unused) => {
13            None
14        };
15
16        ($(#[$($doc: tt)*] $field: ident => $name: expr),* $(,)?) => {
17        #[derive(Debug)]
18        pub struct Directives {
19            $(
20                pub $field: NamedDirective
21            ),*
22        }
23
24        impl Default for Directives {
25            fn default() -> Self {
26                Self {
27                    $($field: NamedDirective {
28                        directive: Default::default(),
29                        name: $name,
30                        error_message: configurable_checks!(@parse_error_message $($doc)*),
31                    }),*
32                }
33            }
34        }
35
36        impl Directives {
37            pub fn iter(&self) -> impl Iterator<Item = &NamedDirective> {
38                vec![
39                    $(
40                        &self.$field
41                    ),*
42                ].into_iter()
43            }
44
45            pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut NamedDirective> {
46                vec![
47                    $(
48                        &mut self.$field
49                    ),*
50                ].into_iter()
51            }
52
53            pub fn create_child(&self, new: Directives, check: &mut RunningCheck, file: &Path) -> Directives {
54                Directives {
55                    $($field: self.$field.create_child(new.$field, check, file)),*
56                }
57            }
58        }
59    };
60}
61
62const LINELENGTH_CHECK: &str = "linelength";
63configurable_checks!(
64
65    // We deliberately do not warn about this being unnecessary,
66    // that would just lead to annoying churn.
67    #[dont_check_unused]
68    linelength => LINELENGTH_CHECK,
69
70    // We deliberately do not warn about this being unnecessary,
71    // that would just lead to annoying churn.
72    #[dont_check_unused]
73    filelength => "filelength",
74
75    // ignore tidy for this entire file
76    #[dont_check_unused]
77    all => "all",
78
79    /// ignoring CR characters unnecessarily
80    cr => "cr",
81    /// ignoring undocumented unsafe unnecessarily
82    undocumented_unsafe => "undocumented-unsafe",
83    /// ignoring tab characters unnecessarily
84    tab => "tab",
85    /// ignoring trailing whitespace unnecessarily
86    end_whitespace => "end-whitespace",
87    /// ignoring trailing newlines unnecessarily
88    trailing_newlines => "trailing-newlines",
89    /// ignoring leading newlines unnecessarily
90    leading_newlines => "leading-newlines",
91    /// ignoring leading newlines unnecessarily
92    copyright => "copyright",
93    /// ignoring dbg usage unnecessarily
94    dbg => "dbg",
95    /// ignoring odd backticks unnecessarily
96    odd_backticks => "odd-backticks",
97    /// ignoring todo usage unnecessarily
98    todo => "todo",
99);
100
101#[derive(Debug)]
102pub struct NamedDirective {
103    directive: Directive,
104    name: &'static str,
105    error_message: Option<&'static str>,
106}
107
108impl NamedDirective {
109    pub fn check_usage(&self, check: &mut RunningCheck, file: &Path) {
110        let Some(message) = self.error_message else {
111            self.force_discard_unsused_ignore();
112            return;
113        };
114
115        if let Err(line) = self.directive.is_ignore_unused_and_mark_checked() {
116            match line {
117                LineNumber::Line(line) => {
118                    check.error(format!("{}:{}: {}", file.display(), line, message));
119                }
120                LineNumber::WholeFile => {
121                    check.error(format!("{}: {}", file.display(), message));
122                }
123            }
124        }
125    }
126
127    fn create_child(&self, new: Self, check: &mut RunningCheck, file: &Path) -> Self {
128        let directive = match (&self.directive, &new.directive) {
129            // If both are deny, we don't care.
130            (Directive::Deny, Directive::Deny) => Directive::Deny,
131            // If (for example) ignored at the file level, but denied at the line level,
132            // keep the ignore as a derived. Deny is the default, so we care about the
133            // ignore.
134            (Directive::Ignore { used, line_number, inherited: _ }, Directive::Deny) => {
135                Directive::Ignore {
136                    used: Rc::clone(used),
137                    line_number: *line_number,
138                    inherited: true,
139                }
140            }
141            // If (for example) ignored at the line level, but not at the file level,
142            // copy in the line-level one verbatim.
143            (Directive::Deny, Directive::Ignore { .. }) => return new,
144            // If (for example) ignored at the file level, and also at the line level,
145            // keep the file-level one. It takes precedence. If a lint is ignored at
146            // a file level, the line-level ignore should be marked "unused".
147            (Directive::Ignore { used, line_number, inherited: _ }, Directive::Ignore { .. }) => {
148                new.check_usage(check, file);
149                Directive::Ignore {
150                    used: Rc::clone(used),
151                    line_number: *line_number,
152                    inherited: true,
153                }
154            }
155        };
156
157        Self { directive, name: self.name, error_message: self.error_message }
158    }
159
160    pub fn is_ignore_and_defuse(&self) -> bool {
161        if let Directive::Ignore { used, .. } = &self.directive {
162            used.set(DirectiveUsed::Checked);
163            true
164        } else {
165            false
166        }
167    }
168
169    pub fn take(&mut self) -> Self {
170        mem::replace(
171            self,
172            Self {
173                directive: Default::default(),
174                name: self.name,
175                error_message: self.error_message,
176            },
177        )
178    }
179
180    /// Check whether we should error on this directive, or whether it was ignored.
181    pub fn check(&self) -> Result<(), ()> {
182        match &self.directive {
183            Directive::Deny => Err(()),
184            Directive::Ignore { used, .. } => {
185                used.set(DirectiveUsed::Yes);
186                Ok(())
187            }
188        }
189    }
190
191    /// Explicitly discard the fact that this directive may be ignored unnecessary.
192    pub fn force_discard_unsused_ignore(&self) {
193        self.is_ignore_and_defuse();
194    }
195}
196
197impl Drop for NamedDirective {
198    fn drop(&mut self) {
199        if let Directive::Ignore { used, inherited: false, .. } = &self.directive
200            && !matches!(used.get(), DirectiveUsed::Checked)
201        {
202            panic!("unchecked directive {} (call Directives::check_usage)", self.name);
203        }
204    }
205}
206
207/// When a directive is set to "ignore",  it means a normally-active
208/// tidy lint is now ignored. This can be bad, if it's ignored for no
209/// reason, so we track whether the ignore is actualy ignoring something.
210#[derive(Clone, Copy, Debug)]
211pub enum DirectiveUsed {
212    /// A lint is ignored, and something used that fact.
213    /// i.e. A lint would've been emitted if it wasn't ignored.
214    Yes,
215    /// A lint is ignored but nothing yet used this ignore.
216    No,
217    /// For drop-bomb behavior: a directive is ignored, and we've
218    /// checked whether it was used or not. Directives panic on drop
219    /// if they aren't checked, to ensure we always emit a lint when
220    /// a directive is uselessly ignored
221    Checked,
222}
223
224#[derive(Debug, Default)]
225pub enum Directive {
226    /// By default, tidy always warns against style issues.
227    #[default]
228    Deny,
229
230    /// `Ignore {used: No}` means that an `ignore-tidy-*` directive has been provided,
231    /// but is unnecessary. `Ignore {used: Yes}` means that it is necessary
232    /// (i.e. a warning would be produced if `ignore-tidy-*` was not present).
233    Ignore {
234        /// line on which the ignore was found
235        line_number: LineNumber,
236        used: Rc<Cell<DirectiveUsed>>,
237        /// If this ignore is inherited from some higher-level,
238        /// like from a file-level ignore, we only want to check whether
239        /// it was used or not (and run the drop bomb) at the end of the file.
240        inherited: bool,
241    },
242}
243
244impl Directive {
245    pub fn set_ignore(&mut self, line_number: LineNumber) {
246        if let Self::Ignore { used, .. } = self {
247            used.set(DirectiveUsed::No);
248        } else {
249            *self = Directive::Ignore {
250                used: Rc::new(Cell::new(DirectiveUsed::No)),
251                line_number,
252                inherited: false,
253            }
254        }
255    }
256
257    /// Check whether this directive was ignored unnecessary.
258    fn is_ignore_unused_and_mark_checked(&self) -> Result<(), LineNumber> {
259        // only if inherted = false, otherwise, we shouldn't set checked and shouldn't care about its value
260        if let Self::Ignore { used, line_number, inherited: false } = self {
261            let used = used.replace(DirectiveUsed::Checked);
262            if matches!(used, DirectiveUsed::No) { Err(*line_number) } else { Ok(()) }
263        } else {
264            Ok(())
265        }
266    }
267}
268
269impl Directives {
270    pub fn check_usage(self, check: &mut RunningCheck, file: &Path) {
271        for i in self.iter() {
272            i.check_usage(check, file);
273        }
274    }
275
276    pub fn from_str(
277        path_str: &str,
278        line_number: LineNumber,
279        can_contain_directive_fastpath: bool,
280        contents: &str,
281    ) -> Self {
282        let mut res = if !can_contain_directive_fastpath {
283            Default::default()
284        } else {
285            Self::parse(line_number, contents)
286        };
287
288        // The rustdoc-json test syntax often requires very long lines, so the checks
289        // for long lines aren't really useful.
290        let always_ignore_linelength = path_str.contains("rustdoc-json");
291
292        if always_ignore_linelength {
293            res.linelength.directive.set_ignore(line_number);
294        }
295
296        res
297    }
298
299    pub fn parse(line_number: LineNumber, contents: &str) -> Self {
300        let mut directives = Self::default();
301
302        for directive in directives.iter_mut() {
303            if match_ignore(
304                contents,
305                matches!(line_number, LineNumber::WholeFile),
306                Some(directive.name),
307            ) {
308                directive.directive.set_ignore(line_number);
309            }
310        }
311
312        directives
313    }
314}
315
316#[derive(Clone, Copy, Debug)]
317pub enum LineNumber {
318    Line(usize),
319    /// file ignores were used, which scans the whole file for patterns.
320    /// We don't know exactly on which line it happened.
321    /// FIXME: do know
322    WholeFile,
323}
324
325pub fn match_ignore(contents: &str, whole_file: bool, check: Option<&str>) -> bool {
326    let comments = [("// ", ""), ("# ", ""), ("/* ", " */"), ("<!-- ", " -->")];
327    let base = "ignore-tidy";
328    let file = if whole_file { "file-" } else { "" };
329
330    for (start, end) in comments {
331        if let Some(check) = check {
332            if contents.contains(&format!("{start}{base}-{file}{check}{end}")) {
333                return true;
334            }
335        } else {
336            if contents.contains(&format!("{start}{base}")) {
337                return true;
338            }
339        }
340    }
341
342    false
343}