use std::{self, borrow::Cow, iter};
use itertools::{multipeek, MultiPeek};
-use syntax::source_map::Span;
+use lazy_static::lazy_static;
+use regex::Regex;
+use rustc_span::Span;
use crate::config::Config;
use crate::rewrite::RewriteContext;
use crate::shape::{Indent, Shape};
use crate::string::{rewrite_string, StringFormat};
use crate::utils::{
- count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, unicode_str_width,
+ count_newlines, first_line_width, last_line_width, trim_left_preserve_layout,
+ trimmed_last_line_width, unicode_str_width,
};
use crate::{ErrorKind, FormattingError};
+lazy_static! {
+ /// A regex matching reference doc links.
+ ///
+ /// ```markdown
+ /// /// An [example].
+ /// ///
+ /// /// [example]: this::is::a::link
+ /// ```
+ static ref REFERENCE_LINK_URL: Regex = Regex::new(r"^\[.+\]\s?:").unwrap();
+}
+
fn is_custom_comment(comment: &str) -> bool {
if !comment.starts_with("//") {
false
/// Returns `true` if the commenting style is for documentation.
pub(crate) fn is_doc_comment(&self) -> bool {
- match *self {
- CommentStyle::TripleSlash | CommentStyle::Doc => true,
- _ => false,
- }
+ matches!(*self, CommentStyle::TripleSlash | CommentStyle::Doc)
}
pub(crate) fn opener(&self) -> &'a str {
| CommentStyle::TripleSlash
| CommentStyle::Custom(..)
| CommentStyle::Doc => "",
- CommentStyle::DoubleBullet => " **/",
- CommentStyle::SingleBullet | CommentStyle::Exclamation => " */",
+ CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
+ " */"
+ }
}
}
CommentStyle::DoubleSlash => "// ",
CommentStyle::TripleSlash => "/// ",
CommentStyle::Doc => "//! ",
- CommentStyle::SingleBullet | CommentStyle::Exclamation => " * ",
- CommentStyle::DoubleBullet => " ** ",
+ CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
+ " * "
+ }
CommentStyle::Custom(opener) => opener,
}
}
}
}
-fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle<'_> {
+pub(crate) fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle<'_> {
if !normalize_comments {
if orig.starts_with("/**") && !orig.starts_with("/**/") {
CommentStyle::DoubleBullet
String::with_capacity(prev_str.len() + next_str.len() + shape.indent.width() + 128);
result.push_str(prev_str);
let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n');
- let first_sep = if prev_str.is_empty() || next_str.is_empty() {
- ""
- } else {
- " "
- };
+ let first_sep =
+ if prev_str.is_empty() || next_str.is_empty() || trimmed_last_line_width(prev_str) == 0 {
+ ""
+ } else {
+ " "
+ };
let mut one_line_width =
last_line_width(prev_str) + first_line_width(next_str) + first_sep.len();
let missing_comment = rewrite_missing_comment(span, shape, context)?;
if missing_comment.is_empty() {
- if allow_extend && prev_str.len() + first_sep.len() + next_str.len() <= shape.width {
+ if allow_extend && one_line_width <= shape.width {
result.push_str(first_sep);
} else if !prev_str.is_empty() {
result.push_str(&indent.to_string_with_newline(config))
}
}
-/// Attributes for code blocks in rustdoc.
-/// See https://doc.rust-lang.org/rustdoc/print.html#attributes
+/// Enum indicating if the code block contains rust based on attributes
enum CodeBlockAttribute {
Rust,
- Ignore,
- Text,
- ShouldPanic,
- NoRun,
- CompileFail,
+ NotRust,
}
impl CodeBlockAttribute {
- fn new(attribute: &str) -> CodeBlockAttribute {
- match attribute {
- "rust" | "" => CodeBlockAttribute::Rust,
- "ignore" => CodeBlockAttribute::Ignore,
- "text" => CodeBlockAttribute::Text,
- "should_panic" => CodeBlockAttribute::ShouldPanic,
- "no_run" => CodeBlockAttribute::NoRun,
- "compile_fail" => CodeBlockAttribute::CompileFail,
- _ => CodeBlockAttribute::Text,
+ /// Parse comma separated attributes list. Return rust only if all
+ /// attributes are valid rust attributes
+ /// See <https://doc.rust-lang.org/rustdoc/print.html#attributes>
+ fn new(attributes: &str) -> CodeBlockAttribute {
+ for attribute in attributes.split(',') {
+ match attribute.trim() {
+ "" | "rust" | "should_panic" | "no_run" | "edition2015" | "edition2018"
+ | "edition2021" => (),
+ "ignore" | "compile_fail" | "text" => return CodeBlockAttribute::NotRust,
+ _ => return CodeBlockAttribute::NotRust,
+ }
}
+ CodeBlockAttribute::Rust
}
}
.checked_sub(closer.len() + opener.len())
.unwrap_or(1);
let indent_str = shape.indent.to_string_with_newline(config).to_string();
- let fmt_indent = shape.indent + (opener.len() - line_start.len());
let mut cr = CommentRewrite {
result: String::with_capacity(orig.len() * 2),
comment_line_separator: format!("{}{}", indent_str, line_start),
max_width,
indent_str,
- fmt_indent,
+ fmt_indent: shape.indent,
fmt: StringFormat {
opener: "",
closer: "",
line_start,
line_end: "",
- shape: Shape::legacy(max_width, fmt_indent),
+ shape: Shape::legacy(max_width, shape.indent),
trim_end: true,
config,
},
result.push_str(line);
result.push_str(match iter.peek() {
Some(next_line) if next_line.is_empty() => sep.trim_end(),
- Some(..) => &sep,
+ Some(..) => sep,
None => "",
});
}
let is_last = i == count_newlines(orig);
if let Some(ref mut ib) = self.item_block {
- if ib.add_line(&line) {
+ if ib.add_line(line) {
return false;
}
self.is_prev_line_multi_line = false;
} else if self.code_block_attr.is_some() {
if line.starts_with("```") {
let code_block = match self.code_block_attr.as_ref().unwrap() {
- CodeBlockAttribute::Ignore | CodeBlockAttribute::Text => {
- trim_custom_comment_prefix(&self.code_block_buffer)
- }
- _ if self.code_block_buffer.is_empty() => String::new(),
- _ => {
+ CodeBlockAttribute::Rust
+ if self.fmt.config.format_code_in_doc_comments()
+ && !self.code_block_buffer.is_empty() =>
+ {
let mut config = self.fmt.config.clone();
config.set().wrap_comments(false);
- if config.format_code_in_doc_comments() {
- if let Some(s) =
- crate::format_code_block(&self.code_block_buffer, &config)
- {
- trim_custom_comment_prefix(&s.snippet)
- } else {
- trim_custom_comment_prefix(&self.code_block_buffer)
- }
+ if let Some(s) =
+ crate::format_code_block(&self.code_block_buffer, &config, false)
+ {
+ trim_custom_comment_prefix(&s.snippet)
} else {
trim_custom_comment_prefix(&self.code_block_buffer)
}
}
+ _ => trim_custom_comment_prefix(&self.code_block_buffer),
};
if !code_block.is_empty() {
self.result.push_str(&self.comment_line_separator);
self.code_block_attr = None;
self.item_block = None;
- if line.starts_with("```") {
- self.code_block_attr = Some(CodeBlockAttribute::new(&line[3..]))
- } else if self.fmt.config.wrap_comments() && ItemizedBlock::is_itemized_line(&line) {
- let ib = ItemizedBlock::new(&line);
+ if let Some(stripped) = line.strip_prefix("```") {
+ self.code_block_attr = Some(CodeBlockAttribute::new(stripped))
+ } else if self.fmt.config.wrap_comments() && ItemizedBlock::is_itemized_line(line) {
+ let ib = ItemizedBlock::new(line);
self.item_block = Some(ib);
return false;
}
const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
fn hide_sharp_behind_comment(s: &str) -> Cow<'_, str> {
- if s.trim_start().starts_with("# ") {
+ let s_trimmed = s.trim();
+ if s_trimmed.starts_with("# ") || s_trimmed == "#" {
Cow::from(format!("{}{}", RUSTFMT_CUSTOM_COMMENT_PREFIX, s))
} else {
Cow::from(s)
/// Returns `true` if the given string MAY include URLs or alike.
fn has_url(s: &str) -> bool {
// This function may return false positive, but should get its job done in most cases.
- s.contains("https://") || s.contains("http://") || s.contains("ftp://") || s.contains("file://")
+ s.contains("https://")
+ || s.contains("http://")
+ || s.contains("ftp://")
+ || s.contains("file://")
+ || REFERENCE_LINK_URL.is_match(s)
}
/// Given the span, rewrite the missing comment inside it if available.
) -> Option<String> {
let missing_snippet = context.snippet(span);
let trimmed_snippet = missing_snippet.trim();
- if !trimmed_snippet.is_empty() {
+ // check the span starts with a comment
+ let pos = trimmed_snippet.find('/');
+ if !trimmed_snippet.is_empty() && pos.is_some() {
rewrite_comment(trimmed_snippet, false, shape, context.config)
} else {
Some(String::new())
Some(String::new())
} else {
let missing_snippet = context.snippet(span);
- let pos = missing_snippet.find('/').unwrap_or(0);
+ let pos = missing_snippet.find('/')?;
// 1 = ` `
let total_width = missing_comment.len() + used_width + 1;
let force_new_line_before_comment =
{
(&line[4..], true)
} else if let CommentStyle::Custom(opener) = *style {
- if line.starts_with(opener) {
- (&line[opener.len()..], true)
+ if let Some(stripped) = line.strip_prefix(opener) {
+ (stripped, true)
} else {
(&line[opener.trim_end().len()..], false)
}
|| line.starts_with("**")
{
(&line[2..], line.chars().nth(1).unwrap() == ' ')
- } else if line.starts_with('*') {
- (&line[1..], false)
+ } else if let Some(stripped) = line.strip_prefix('*') {
+ (stripped, false)
} else {
(line, line.starts_with(' '))
}
pub(crate) trait FindUncommented {
fn find_uncommented(&self, pat: &str) -> Option<usize>;
+ fn find_last_uncommented(&self, pat: &str) -> Option<usize>;
}
impl FindUncommented for str {
None => Some(self.len() - pat.len()),
}
}
+
+ fn find_last_uncommented(&self, pat: &str) -> Option<usize> {
+ if let Some(left) = self.find_uncommented(pat) {
+ let mut result = left;
+ // add 1 to use find_last_uncommented for &str after pat
+ while let Some(next) = self[(result + 1)..].find_last_uncommented(pat) {
+ result += next + 1;
+ }
+ Some(result)
+ } else {
+ None
+ }
+ }
}
// Returns the first byte position after the first comment. The given string
char_kind = FullCodeCharKind::InStringCommented;
if chr == '"' {
CharClassesStatus::BlockComment(deepness)
+ } else if chr == '*' && self.base.peek().map(RichChar::get_char) == Some('/') {
+ char_kind = FullCodeCharKind::InComment;
+ CharClassesStatus::BlockCommentClosing(deepness - 1)
} else {
CharClassesStatus::StringInBlockComment(deepness)
}
None => unreachable!(),
};
- while let Some((kind, c)) = self.base.next() {
+ for (kind, c) in self.base.by_ref() {
// needed to set the kind of the ending character on the last line
self.kind = kind;
if c == '\n' {
// We missed some comments. Warn and keep the original text.
if context.config.error_on_unformatted() {
context.report.append(
- context.source_map.span_to_filename(span).into(),
+ context.parse_sess.span_to_filename(span),
vec![FormattingError::from_span(
span,
- &context.source_map,
+ context.parse_sess,
ErrorKind::LostComment,
)],
);
fn remove_comment_header(comment: &str) -> &str {
if comment.starts_with("///") || comment.starts_with("//!") {
&comment[3..]
- } else if comment.starts_with("//") {
- &comment[2..]
+ } else if let Some(stripped) = comment.strip_prefix("//") {
+ stripped
} else if (comment.starts_with("/**") && !comment.starts_with("/**/"))
|| comment.starts_with("/*!")
{
} else {
assert!(
comment.starts_with("/*"),
- format!("string '{}' is not a comment", comment)
+ "string '{}' is not a comment",
+ comment
);
&comment[2..comment.len() - 2]
}