blob: 3330cc5defd3f322be58703286e250ec1d34f832 [file] [log] [blame]
use clippy_utils::diagnostics::span_lint_and_then;
use rustc_ast::attr::AttributeExt as _;
use rustc_ast::token::CommentKind;
use rustc_errors::Applicability;
use rustc_hir::{AttrStyle, Attribute};
use rustc_lint::{LateContext, LintContext};
use rustc_resolve::rustdoc::DocFragmentKind;
use std::ops::Range;
use super::{DOC_SUSPICIOUS_FOOTNOTES, Fragments};
pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &Fragments<'_>, attrs: &[Attribute]) {
for i in doc[range.clone()]
.bytes()
.enumerate()
.filter_map(|(i, c)| if c == b'[' { Some(i) } else { None })
{
let start = i + range.start;
if doc.as_bytes().get(start + 1) == Some(&b'^')
&& let Some(end) = all_numbers_upto_brace(doc, start + 2)
&& doc.as_bytes().get(end) != Some(&b':')
&& doc.as_bytes().get(start - 1) != Some(&b'\\')
&& let Some(this_fragment) = {
// the `doc` string contains all fragments concatenated together
// figure out which one this suspicious footnote comes from
let mut starting_position = 0;
let mut found_fragment = fragments.fragments.last();
for fragment in fragments.fragments {
if start >= starting_position && start < starting_position + fragment.doc.as_str().len() {
found_fragment = Some(fragment);
break;
}
starting_position += fragment.doc.as_str().len();
}
found_fragment
}
{
let span = fragments.span(cx, start..end).unwrap_or(this_fragment.span);
span_lint_and_then(
cx,
DOC_SUSPICIOUS_FOOTNOTES,
span,
"looks like a footnote ref, but has no matching footnote",
|diag| {
if this_fragment.kind == DocFragmentKind::SugaredDoc {
let (doc_attr, (_, doc_attr_comment_kind), attr_style) = attrs
.iter()
.filter(|attr| attr.span().overlaps(this_fragment.span))
.rev()
.find_map(|attr| {
Some((attr, attr.doc_str_and_comment_kind()?, attr.doc_resolution_scope()?))
})
.unwrap();
let (to_add, terminator) = match (doc_attr_comment_kind, attr_style) {
(CommentKind::Line, AttrStyle::Outer) => ("\n///\n/// ", ""),
(CommentKind::Line, AttrStyle::Inner) => ("\n//!\n//! ", ""),
(CommentKind::Block, AttrStyle::Outer) => ("\n/** ", " */"),
(CommentKind::Block, AttrStyle::Inner) => ("\n/*! ", " */"),
};
diag.span_suggestion_verbose(
doc_attr.span().shrink_to_hi(),
"add footnote definition",
format!(
"{to_add}{label}: <!-- description -->{terminator}",
label = &doc[start..end]
),
Applicability::HasPlaceholders,
);
} else {
let is_file_include = cx
.sess()
.source_map()
.span_to_snippet(this_fragment.span)
.as_ref()
.map(|vdoc| vdoc.trim())
== Ok(doc);
if is_file_include {
// if this is a file include, then there's no quote marks
diag.span_suggestion_verbose(
this_fragment.span.shrink_to_hi(),
"add footnote definition",
format!("\n\n{label}: <!-- description -->", label = &doc[start..end],),
Applicability::HasPlaceholders,
);
} else {
// otherwise, we wrap in a string
diag.span_suggestion_verbose(
this_fragment.span,
"add footnote definition",
format!(
"r#\"{doc}\n\n{label}: <!-- description -->\"#",
doc = this_fragment.doc,
label = &doc[start..end],
),
Applicability::HasPlaceholders,
);
}
}
},
);
}
}
}
fn all_numbers_upto_brace(text: &str, i: usize) -> Option<usize> {
for (j, c) in text.as_bytes()[i..].iter().copied().enumerate().take(64) {
if c == b']' && j != 0 {
return Some(i + j + 1);
}
if !c.is_ascii_digit() || j >= 64 {
break;
}
}
None
}