diff --git a/clippy_lints/src/tabs_in_doc_comments.rs b/clippy_lints/src/tabs_in_doc_comments.rs new file mode 100644 index 000000000000..ccea700f3538 --- /dev/null +++ b/clippy_lints/src/tabs_in_doc_comments.rs @@ -0,0 +1,219 @@ +use crate::utils::span_lint_and_sugg; +use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass}; +use rustc::{declare_lint_pass, declare_tool_lint}; +use rustc_errors::Applicability; +use std::convert::TryFrom; +use syntax::ast; +use syntax::source_map::{BytePos, Span}; + +declare_clippy_lint! { + /// **What it does:** Checks doc comments for usage of tab characters. + /// + /// **Why is this bad?** The rust style-guide promotes spaces instead of tabs for indentation. + /// To keep a consistent view on the source, also doc comments should not have tabs. + /// Also, explaining ascii-diagrams containing tabs can get displayed incorrectly when the + /// display settings of the author and reader differ. + /// + /// **Known problems:** None. + /// + /// **Example:** + /// ```rust + /// /// + /// /// Struct to hold two strings: + /// /// - first one + /// /// - second one + /// pub struct DoubleString { + /// /// + /// /// - First String: + /// /// - needs to be inside here + /// first_string: String, + /// /// + /// /// - Second String: + /// /// - needs to be inside here + /// second_string: String, + ///} + /// ``` + /// + /// Will be converted to: + /// ```rust + /// /// + /// /// Struct to hold two strings: + /// /// - first one + /// /// - second one + /// pub struct DoubleString { + /// /// + /// /// - First String: + /// /// - needs to be inside here + /// first_string: String, + /// /// + /// /// - Second String: + /// /// - needs to be inside here + /// second_string: String, + ///} + /// ``` + pub TABS_IN_DOC_COMMENTS, + style, + "using tabs in doc comments is not recommended" +} + +declare_lint_pass!(TabsInDocComments => [TABS_IN_DOC_COMMENTS]); + +impl TabsInDocComments { + fn warn_if_tabs_in_doc(cx: &EarlyContext<'_>, attr: &ast::Attribute) { + if let ast::AttrKind::DocComment(comment) = attr.kind { + let comment = comment.as_str(); + + for (lo, hi) in get_chunks_of_tabs(&comment) { + let new_span = Span::new( + attr.span.lo() + BytePos(lo), + attr.span.lo() + BytePos(hi), + attr.span.ctxt(), + ); + span_lint_and_sugg( + cx, + TABS_IN_DOC_COMMENTS, + new_span, + "using tabs in doc comments is not recommended", + "consider using four spaces per tab", + " ".repeat((hi - lo) as usize), + Applicability::MaybeIncorrect, + ); + } + } + } +} + +impl EarlyLintPass for TabsInDocComments { + fn check_attribute(&mut self, cx: &EarlyContext<'_>, attribute: &ast::Attribute) { + Self::warn_if_tabs_in_doc(cx, &attribute); + } +} + +/// +/// scans the string for groups of tabs and returns the start(inclusive) and end positions +/// (exclusive) of all groups +/// e.g. "sd\tasd\t\taa" will be converted to [(2, 3), (6, 8)] as +/// 012 3456 7 89 +/// ^-^ ^---^ +fn get_chunks_of_tabs(the_str: &str) -> Vec<(u32, u32)> { + let line_length_way_to_long = "doc comment longer than 2^32 chars"; + let mut spans: Vec<(u32, u32)> = vec![]; + let mut current_start: u32 = 0; + + // tracker to decide if the last group of tabs is not closed by a non-tab character + let mut is_active = false; + + let chars_array: Vec<_> = the_str.chars().collect(); + + if chars_array == vec!['\t'] { + return vec![(0, 1)]; + } + + for (index, arr) in chars_array.windows(2).enumerate() { + let index = u32::try_from(index).expect(line_length_way_to_long); + match arr { + ['\t', '\t'] => { + // either string starts with double tab, then we have to set it active, + // otherwise is_active is true anyway + is_active = true; + }, + [_, '\t'] => { + // as ['\t', '\t'] is excluded, this has to be a start of a tab group, + // set indices accordingly + is_active = true; + current_start = index + 1; + }, + ['\t', _] => { + // this now has to be an end of the group, hence we have to push a new tuple + is_active = false; + spans.push((current_start, index + 1)); + }, + _ => {}, + } + } + + // only possible when tabs are at the end, insert last group + if is_active { + spans.push(( + current_start, + u32::try_from(the_str.chars().count()).expect(line_length_way_to_long), + )); + } + + spans +} + +#[cfg(test)] +mod tests_for_get_chunks_of_tabs { + use super::get_chunks_of_tabs; + + #[test] + fn test_empty_string() { + let res = get_chunks_of_tabs(""); + + assert_eq!(res, vec![]); + } + + #[test] + fn test_simple() { + let res = get_chunks_of_tabs("sd\t\t\taa"); + + assert_eq!(res, vec![(2, 5)]); + } + + #[test] + fn test_only_t() { + let res = get_chunks_of_tabs("\t\t"); + + assert_eq!(res, vec![(0, 2)]); + } + + #[test] + fn test_only_one_t() { + let res = get_chunks_of_tabs("\t"); + + assert_eq!(res, vec![(0, 1)]); + } + + #[test] + fn test_double() { + let res = get_chunks_of_tabs("sd\tasd\t\taa"); + + assert_eq!(res, vec![(2, 3), (6, 8)]); + } + + #[test] + fn test_start() { + let res = get_chunks_of_tabs("\t\taa"); + + assert_eq!(res, vec![(0, 2)]); + } + + #[test] + fn test_end() { + let res = get_chunks_of_tabs("aa\t\t"); + + assert_eq!(res, vec![(2, 4)]); + } + + #[test] + fn test_start_single() { + let res = get_chunks_of_tabs("\taa"); + + assert_eq!(res, vec![(0, 1)]); + } + + #[test] + fn test_end_single() { + let res = get_chunks_of_tabs("aa\t"); + + assert_eq!(res, vec![(2, 3)]); + } + + #[test] + fn test_no_tabs() { + let res = get_chunks_of_tabs("dsfs"); + + assert_eq!(res, vec![]); + } +} diff --git a/tests/ui/tabs_in_doc_comments.fixed b/tests/ui/tabs_in_doc_comments.fixed new file mode 100644 index 000000000000..4bc4bc86c76c --- /dev/null +++ b/tests/ui/tabs_in_doc_comments.fixed @@ -0,0 +1,22 @@ +// run-rustfix + +#![warn(clippy::tabs_in_doc_comments)] +#[allow(dead_code)] + +/// +/// Struct to hold two strings: +/// - first one +/// - second one +pub struct DoubleString { + /// + /// - First String: + /// - needs to be inside here + first_string: String, + /// + /// - Second String: + /// - needs to be inside here + second_string: String, +} + +/// This is main +fn main() {} diff --git a/tests/ui/tabs_in_doc_comments.rs b/tests/ui/tabs_in_doc_comments.rs new file mode 100644 index 000000000000..9db3416e6596 --- /dev/null +++ b/tests/ui/tabs_in_doc_comments.rs @@ -0,0 +1,22 @@ +// run-rustfix + +#![warn(clippy::tabs_in_doc_comments)] +#[allow(dead_code)] + +/// +/// Struct to hold two strings: +/// - first one +/// - second one +pub struct DoubleString { + /// + /// - First String: + /// - needs to be inside here + first_string: String, + /// + /// - Second String: + /// - needs to be inside here + second_string: String, +} + +/// This is main +fn main() {} diff --git a/tests/ui/tabs_in_doc_comments.stderr b/tests/ui/tabs_in_doc_comments.stderr new file mode 100644 index 000000000000..355f2e805796 --- /dev/null +++ b/tests/ui/tabs_in_doc_comments.stderr @@ -0,0 +1,52 @@ +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:12:9 + | +LL | /// - First String: + | ^^^^ help: consider using four spaces per tab + | + = note: `-D clippy::tabs-in-doc-comments` implied by `-D warnings` + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:13:9 + | +LL | /// - needs to be inside here + | ^^^^^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:16:9 + | +LL | /// - Second String: + | ^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:17:9 + | +LL | /// - needs to be inside here + | ^^^^^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:8:5 + | +LL | /// - first one + | ^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:8:13 + | +LL | /// - first one + | ^^^^^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:9:5 + | +LL | /// - second one + | ^^^^ help: consider using four spaces per tab + +error: using tabs in doc comments is not recommended + --> $DIR/tabs_in_doc_comments.rs:9:14 + | +LL | /// - second one + | ^^^^ help: consider using four spaces per tab + +error: aborting due to 8 previous errors +