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 #[dont_check_unused]
68 linelength => LINELENGTH_CHECK,
69
70 #[dont_check_unused]
73 filelength => "filelength",
74
75 #[dont_check_unused]
77 all => "all",
78
79 cr => "cr",
81 undocumented_unsafe => "undocumented-unsafe",
83 tab => "tab",
85 end_whitespace => "end-whitespace",
87 trailing_newlines => "trailing-newlines",
89 leading_newlines => "leading-newlines",
91 copyright => "copyright",
93 dbg => "dbg",
95 odd_backticks => "odd-backticks",
97 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 (Directive::Deny, Directive::Deny) => Directive::Deny,
131 (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 (Directive::Deny, Directive::Ignore { .. }) => return new,
144 (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 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 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#[derive(Clone, Copy, Debug)]
211pub enum DirectiveUsed {
212 Yes,
215 No,
217 Checked,
222}
223
224#[derive(Debug, Default)]
225pub enum Directive {
226 #[default]
228 Deny,
229
230 Ignore {
234 line_number: LineNumber,
236 used: Rc<Cell<DirectiveUsed>>,
237 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 fn is_ignore_unused_and_mark_checked(&self) -> Result<(), LineNumber> {
259 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 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 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}