Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_attr_parsing/src/attributes/stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[
Allow(Target::Static),
Allow(Target::ForeignFn),
Allow(Target::ForeignStatic),
Allow(Target::ForeignTy),
Allow(Target::ExternCrate),
]);

Expand Down
16 changes: 16 additions & 0 deletions src/librustdoc/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ declare_rustdoc_lint! {
"detects redundant explicit links in doc comments"
}

declare_rustdoc_lint! {
/// This lint checks for uses of footnote references without definition.
BROKEN_FOOTNOTE,
Warn,
"detects footnote references with no associated definition"
}

declare_rustdoc_lint! {
/// This lint checks if all footnote definitions are used.
UNUSED_FOOTNOTE_DEFINITION,
Warn,
"detects unused footnote definitions"
}

pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
vec![
BROKEN_INTRA_DOC_LINKS,
Expand All @@ -209,6 +223,8 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
MISSING_CRATE_LEVEL_DOCS,
UNESCAPED_BACKTICKS,
REDUNDANT_EXPLICIT_LINKS,
BROKEN_FOOTNOTE,
UNUSED_FOOTNOTE_DEFINITION,
]
});

Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/passes/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod bare_urls;
mod check_code_block_syntax;
mod footnotes;
mod html_tags;
mod redundant_explicit_links;
mod unescaped_backticks;
Expand Down Expand Up @@ -41,6 +42,7 @@ impl DocVisitor<'_> for Linter<'_, '_> {
if may_have_link {
bare_urls::visit_item(self.cx, item, hir_id, &dox);
redundant_explicit_links::visit_item(self.cx, item, hir_id);
footnotes::visit_item(self.cx, item, hir_id, &dox);
}
if may_have_code {
check_code_block_syntax::visit_item(self.cx, item, &dox);
Expand Down
117 changes: 117 additions & 0 deletions src/librustdoc/passes/lint/footnotes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use std::ops::Range;

use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::DiagDecorator;
use rustc_hir::HirId;
use rustc_lint_defs::Applicability;
use rustc_resolve::rustdoc::pulldown_cmark::{Event, Options, Parser, Tag};
use rustc_resolve::rustdoc::source_span_for_markdown_range;

use crate::clean::Item;
use crate::core::DocContext;

pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
let tcx = cx.tcx;

let mut missing_footnote_references = FxHashSet::default();
let mut footnote_references = FxHashSet::default();
let mut footnote_definitions = FxHashMap::default();

let options = Options::ENABLE_FOOTNOTES;
let mut parser = Parser::new_ext(dox, options).into_offset_iter().peekable();
while let Some((event, span)) = parser.next() {
match event {
Event::Text(text)
if &*text == "["
&& (span.start == 0 || dox.as_bytes().get(span.start - 1) != Some(&b'\\'))
&& let Some(len) = scan_footnote_ref(&dox[span.start..]) =>
{
missing_footnote_references
.insert(Range { start: span.start, end: span.start + len });
}
Event::FootnoteReference(label) => {
footnote_references.insert(label);
}
Event::Start(Tag::FootnoteDefinition(label)) => {
footnote_definitions.insert(label, span.start + 1);
}
_ => {}
}
}

#[allow(rustc::potential_query_instability)]
for (footnote, span) in footnote_definitions {
if !footnote_references.contains(&footnote) {
let (span, _) = source_span_for_markdown_range(
tcx,
dox,
&(span..span + 1),
&item.attrs.doc_strings,
)
.unwrap_or_else(|| (item.attr_span(tcx), false));

tcx.emit_node_span_lint(
crate::lint::UNUSED_FOOTNOTE_DEFINITION,
hir_id,
span,
DiagDecorator(|lint| {
lint.primary_message("unused footnote definition");
}),
);
}
}

#[allow(rustc::potential_query_instability)]
for span in missing_footnote_references {
let ref_span = source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings)
.map(|(span, _)| span)
.unwrap_or_else(|| item.attr_span(tcx));

tcx.emit_node_span_lint(
crate::lint::BROKEN_FOOTNOTE,
hir_id,
ref_span,
DiagDecorator(|lint| {
lint.primary_message("no footnote definition matching this footnote");
lint.span_suggestion(
ref_span.shrink_to_lo(),
"if it should not be a footnote, escape it",
"\\",
Applicability::MaybeIncorrect,
);
}),
);
}
}

fn scan_footnote_ref(dox: &str) -> Option<usize> {
let dox = dox.as_bytes();
let mut i = 0;
if dox.get(i) != Some(&b'[') {
return None;
}
i += 1;
if dox.get(i) != Some(&b'^') {
return None;
}
i += 1;
while let Some(&c) = dox.get(i) {
if c == b']' {
i += 1;
return Some(i);
}
if c == b'\r' || c == b'\n' || c == b'[' {
// Can't nest things like this.
break;
}
if c == b'\\' {
i += 1;
}
if dox.get(i) == Some(&b'\r') || dox.get(i) == Some(&b'\n') {
// Can't have line breaks in footnote refs
break;
}
i += 1;
}
None
}
2 changes: 1 addition & 1 deletion src/rustdoc-json-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "rustdoc-json-types"
version = "0.1.0"
edition = "2021"
edition = "2024"

[lib]
path = "lib.rs"
Expand Down
2 changes: 1 addition & 1 deletion src/tools/jsondocck/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "jsondocck"
version = "0.1.0"
edition = "2021"
edition = "2024"

[dependencies]
jsonpath-rust = "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion src/tools/jsondoclint/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "jsondoclint"
version = "0.1.0"
edition = "2021"
edition = "2024"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
44 changes: 44 additions & 0 deletions tests/rustdoc-ui/lints/broken-footnote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#![deny(rustdoc::broken_footnote)]

//! Footnote referenced [^1]. And [^2]. And [^bla].
//!
//! [^1]: footnote defined
//~^^^ ERROR: no footnote definition matching this footnote
//~| ERROR: no footnote definition matching this footnote

// Should not lint.
//! foo[^1]
//!
//! ```
//!
//! [^1]: bar
//!
//! ```

// Edge cases from https://pulldown-cmark.github.io/pulldown-cmark/specs/footnotes.html
/// The following are not footnote references:
///
/// \[^a]
///
/// [\^b]
///
/// [^c\]
///
/// [^d
/// e]
///
/// [^f\
/// g]
pub struct NotReferences;

/// The following are not footnote references:
///
/// [^a b]
//~^ ERROR: no footnote definition matching this footnote
///
/// [^1\.2]
//~^ ERROR: no footnote definition matching this footnote
///
/// [^*]
//~^ ERROR: no footnote definition matching this footnote
pub struct EdgeCases;
48 changes: 48 additions & 0 deletions tests/rustdoc-ui/lints/broken-footnote.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
error: no footnote definition matching this footnote
--> $DIR/broken-footnote.rs:3:45
|
LL | //! Footnote referenced [^1]. And [^2]. And [^bla].
| -^^^^^
| |
| help: if it should not be a footnote, escape it: `\`
|
note: the lint level is defined here
--> $DIR/broken-footnote.rs:1:9
|
LL | #![deny(rustdoc::broken_footnote)]
| ^^^^^^^^^^^^^^^^^^^^^^^^

error: no footnote definition matching this footnote
--> $DIR/broken-footnote.rs:3:35
|
LL | //! Footnote referenced [^1]. And [^2]. And [^bla].
| -^^^
| |
| help: if it should not be a footnote, escape it: `\`

error: no footnote definition matching this footnote
--> $DIR/broken-footnote.rs:39:5
|
LL | /// [^1\.2]
| -^^^^^^
| |
| help: if it should not be a footnote, escape it: `\`

error: no footnote definition matching this footnote
--> $DIR/broken-footnote.rs:42:5
|
LL | /// [^*]
| -^^^
| |
| help: if it should not be a footnote, escape it: `\`

error: no footnote definition matching this footnote
--> $DIR/broken-footnote.rs:36:5
|
LL | /// [^a b]
| -^^^^^
| |
| help: if it should not be a footnote, escape it: `\`

error: aborting due to 5 previous errors

9 changes: 9 additions & 0 deletions tests/rustdoc-ui/lints/unused-footnote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This test ensures that the `rustdoc::unused_footnote` lint is working as expected.

#![deny(rustdoc::unused_footnote_definition)]

//! Footnote referenced. [^2]
//!
//! [^1]: footnote defined
//! [^2]: footnote defined
//~^^ ERROR: unused_footnote_definition
14 changes: 14 additions & 0 deletions tests/rustdoc-ui/lints/unused-footnote.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
error: unused footnote definition
--> $DIR/unused-footnote.rs:7:6
|
LL | //! [^1]: footnote defined
| ^
|
note: the lint level is defined here
--> $DIR/unused-footnote.rs:3:9
|
LL | #![deny(rustdoc::unused_footnote_definition)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 1 previous error

6 changes: 6 additions & 0 deletions tests/ui/lint/auxiliary/lint_stability.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![crate_name="lint_stability"]
#![crate_type = "lib"]
#![feature(extern_types)]
#![feature(staged_api)]
#![feature(associated_type_defaults)]
#![stable(feature = "lint_stability", since = "1.0.0")]
Expand Down Expand Up @@ -186,3 +187,8 @@ macro_rules! macro_test_arg {
macro_rules! macro_test_arg_nested {
($func:ident) => (macro_test_arg!($func()));
}

extern "C" {
#[unstable(feature = "unstable_test_feature", issue = "none")]
pub type UnstableForeignType;
}
5 changes: 4 additions & 1 deletion tests/ui/lint/lint-stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#![allow(deprecated)]
#![allow(dead_code)]
#![feature(staged_api)]

#![feature(extern_types)]
#![stable(feature = "rust1", since = "1.0.0")]

#[macro_use]
Expand All @@ -18,6 +18,9 @@ mod cross_crate {

use lint_stability::*;

fn test_foreign_type(_: &mut UnstableForeignType) { //~ ERROR use of unstable library feature
}

fn test() {
type Foo = MethodTester;
let foo = MethodTester;
Expand Down
Loading
Loading