Skip to main content

xtask/tasks/fmt/lints/
trailing_newline.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Checks that text files end with exactly one trailing newline.
5
6use super::Lint;
7use super::LintCtx;
8use super::Lintable;
9use toml_edit::DocumentMut;
10
11const CHECKED_EXTENSIONS: &[&str] = &[
12    "c", "md", "proto", "py", "rs", "sh", "toml", "txt", "yml", "js", "ts",
13];
14
15pub struct TrailingNewline;
16
17impl Lint for TrailingNewline {
18    fn new(_ctx: &LintCtx) -> Self {
19        TrailingNewline
20    }
21
22    fn enter_workspace(&mut self, _content: &Lintable<DocumentMut>) {}
23    fn enter_crate(&mut self, _content: &Lintable<DocumentMut>) {}
24
25    fn visit_file(&mut self, content: &mut Lintable<String>) {
26        let mut ending_newlines = 0;
27        for c in content.as_bytes().iter().rev() {
28            if *c == b'\n' {
29                ending_newlines += 1;
30            } else {
31                break;
32            }
33        }
34
35        if ending_newlines == 0 {
36            content.fix("missing trailing newline", |content| content.push('\n'));
37        } else if ending_newlines != 1 {
38            content.fix("too many trailing newlines", |content| {
39                // Trim all trailing newlines but one.
40                content.truncate(content.len() - ending_newlines + 1);
41            });
42        }
43    }
44
45    fn exit_crate(&mut self, content: &mut Lintable<DocumentMut>) {
46        // toml_edit unfortunately makes checking for a trailing newline
47        // inconvenient, as it parses into the decor of the last item, not the
48        // document as a whole. It's easier to just check the raw file content.
49        let mut ending_newlines = 0;
50        for c in content.raw().unwrap().as_bytes().iter().rev() {
51            if *c == b'\n' {
52                ending_newlines += 1;
53            } else {
54                break;
55            }
56        }
57
58        // Writing a trailing newline is also annoying, as it likes to synthesize
59        // one if it was originally missing. So we just trim all trailing newlines
60        // and let it add one back if needed, being careful not to remove any trailing
61        // comments or the like.
62        if ending_newlines != 1 {
63            content.fix("missing or too many trailing newlines", |content| {
64                content.set_trailing(content.trailing().as_str().unwrap().trim_end().to_owned());
65            });
66        }
67    }
68
69    fn exit_workspace(&mut self, content: &mut Lintable<DocumentMut>) {
70        self.exit_crate(content)
71    }
72
73    fn visit_nonrust_file(&mut self, extension: &str, content: &mut Lintable<String>) {
74        // workaround for `mdbook-docfx` emitting yaml with no trailing newline
75        if content.path().file_name().unwrap_or_default() == "toc.yml" {
76            return;
77        }
78
79        // TODO: Should we just check everything?
80        if CHECKED_EXTENSIONS.contains(&extension) {
81            self.visit_file(content)
82        }
83    }
84}