Skip to content

Commit

Permalink
Rollup merge of rust-lang#77856 - GuillaumeGomez:automatic-links-lint…
Browse files Browse the repository at this point in the history
…, r=jyn514

Add automatic_links lint

Fixes rust-lang#77501.

r? @jyn514
  • Loading branch information
GuillaumeGomez committed Oct 16, 2020
2 parents 6c5dbcb + d7272ea commit 6db16ff
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4233,6 +4233,7 @@ dependencies = [
"itertools 0.9.0",
"minifier",
"pulldown-cmark 0.8.0",
"regex",
"rustc-rayon",
"serde",
"serde_json",
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ use rustc_hir::def_id::LocalDefId;
use rustc_middle::ty::query::Providers;
use rustc_middle::ty::TyCtxt;
use rustc_session::lint::builtin::{
BARE_TRAIT_OBJECTS, BROKEN_INTRA_DOC_LINKS, ELIDED_LIFETIMES_IN_PATHS,
AUTOMATIC_LINKS, BARE_TRAIT_OBJECTS, BROKEN_INTRA_DOC_LINKS, ELIDED_LIFETIMES_IN_PATHS,
EXPLICIT_OUTLIVES_REQUIREMENTS, INVALID_CODEBLOCK_ATTRIBUTES, INVALID_HTML_TAGS,
MISSING_DOC_CODE_EXAMPLES, PRIVATE_DOC_TESTS,
};
Expand Down Expand Up @@ -307,6 +307,7 @@ fn register_builtins(store: &mut LintStore, no_interleave_lints: bool) {

add_lint_group!(
"rustdoc",
AUTOMATIC_LINKS,
BROKEN_INTRA_DOC_LINKS,
PRIVATE_INTRA_DOC_LINKS,
INVALID_CODEBLOCK_ATTRIBUTES,
Expand Down
12 changes: 12 additions & 0 deletions compiler/rustc_session/src/lint/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,17 @@ declare_lint! {
"detects invalid HTML tags in doc comments"
}

declare_lint! {
/// The `automatic_links` lint detects when a URL could be written using
/// only angle brackets. This is a `rustdoc` only lint, see the
/// documentation in the [rustdoc book].
///
/// [rustdoc book]: ../../../rustdoc/lints.html#automatic_links
pub AUTOMATIC_LINKS,
Warn,
"detects URLs that could be written using only angle brackets"
}

declare_lint! {
/// The `where_clauses_object_safety` lint detects for [object safety] of
/// [where clauses].
Expand Down Expand Up @@ -2711,6 +2722,7 @@ declare_lint_pass! {
MISSING_DOC_CODE_EXAMPLES,
INVALID_HTML_TAGS,
PRIVATE_DOC_TESTS,
AUTOMATIC_LINKS,
WHERE_CLAUSES_OBJECT_SAFETY,
PROC_MACRO_DERIVE_RESOLUTION_FALLBACK,
MACRO_USE_EXTERN_CRATE,
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! This includes changes in the stability of the constness.
//!
//! In order to make an intrinsic usable at compile-time, one needs to copy the implementation
//! from https://github.com/rust-lang/miri/blob/master/src/shims/intrinsics.rs to
//! from <https://github.com/rust-lang/miri/blob/master/src/shims/intrinsics.rs> to
//! `compiler/rustc_mir/src/interpret/intrinsics.rs` and add a
//! `#[rustc_const_unstable(feature = "foo", issue = "01234")]` to the intrinsic.
//!
Expand Down
1 change: 1 addition & 0 deletions library/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ pub mod primitive;
unused_imports,
unsafe_op_in_unsafe_fn
)]
#[cfg_attr(not(bootstrap), allow(automatic_links))]
// FIXME: This annotation should be moved into rust-lang/stdarch after clashing_extern_declarations is
// merged. It currently cannot because bootstrap fails as the lint hasn't been defined yet.
#[allow(clashing_extern_declarations)]
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/num/dec2flt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
//!
//! Primarily, this module and its children implement the algorithms described in:
//! "How to Read Floating Point Numbers Accurately" by William D. Clinger,
//! available online: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4152
//! available online: <http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4152>
//!
//! In addition, there are numerous helper functions that are used in the paper but not available
//! in Rust (or at least in core). Our version is additionally complicated by the need to handle
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/slice/sort.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Slice sorting
//!
//! This module contains a sorting algorithm based on Orson Peters' pattern-defeating quicksort,
//! published at: https://github.com/orlp/pdqsort
//! published at: <https://github.com/orlp/pdqsort>
//!
//! Unstable sorting is compatible with libcore because it doesn't allocate memory, unlike our
//! stable sorting implementation.
Expand Down
37 changes: 37 additions & 0 deletions src/doc/rustdoc/src/lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,40 @@ warning: unclosed HTML tag `h1`
warning: 2 warnings emitted
```

## automatic_links

This lint is **nightly-only** and **warns by default**. It detects links which
could use the "automatic" link syntax. For example:

```rust
/// http://hello.rs
/// [http://a.com](http://a.com)
/// [http://b.com]
///
/// [http://b.com]: http://b.com
pub fn foo() {}
```

Which will give:

```text
warning: this URL is not a hyperlink
--> foo.rs:3:5
|
3 | /// http://hello.rs
| ^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://hello.rs>`
|
warning: unneeded long form for URL
--> foo.rs:4:5
|
4 | /// [http://a.com](http://a.com)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://a.com>`
warning: unneeded long form for URL
--> foo.rs:5:5
|
5 | /// [http://b.com]
| ^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://b.com>`
```
1 change: 1 addition & 0 deletions src/librustdoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde_json = "1.0"
smallvec = "1.0"
tempfile = "3"
itertools = "0.9"
regex = "1"

[dev-dependencies]
expect-test = "1.0"
2 changes: 2 additions & 0 deletions src/librustdoc/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,13 @@ pub fn run_core(
let invalid_codeblock_attributes_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTES.name;
let invalid_html_tags = rustc_lint::builtin::INVALID_HTML_TAGS.name;
let renamed_and_removed_lints = rustc_lint::builtin::RENAMED_AND_REMOVED_LINTS.name;
let automatic_links = rustc_lint::builtin::AUTOMATIC_LINKS.name;
let unknown_lints = rustc_lint::builtin::UNKNOWN_LINTS.name;

// In addition to those specific lints, we also need to allow those given through
// command line, otherwise they'll get ignored and we don't want that.
let lints_to_show = vec![
automatic_links.to_owned(),
intra_link_resolution_failure_name.to_owned(),
missing_docs.to_owned(),
missing_doc_example.to_owned(),
Expand Down
127 changes: 127 additions & 0 deletions src/librustdoc/passes/automatic_links.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use super::{span_of_attrs, Pass};
use crate::clean::*;
use crate::core::DocContext;
use crate::fold::DocFolder;
use crate::html::markdown::opts;
use core::ops::Range;
use pulldown_cmark::{Event, LinkType, Parser, Tag};
use regex::Regex;
use rustc_errors::Applicability;
use rustc_feature::UnstableFeatures;
use rustc_session::lint;

pub const CHECK_AUTOMATIC_LINKS: Pass = Pass {
name: "check-automatic-links",
run: check_automatic_links,
description: "detects URLS that could be written using angle brackets",
};

const URL_REGEX: &str = concat!(
r"https?://", // url scheme
r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", // one or more subdomains
r"[a-zA-Z]{2,4}", // root domain
r"\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)" // optional query or url fragments
);

struct AutomaticLinksLinter<'a, 'tcx> {
cx: &'a DocContext<'tcx>,
regex: Regex,
}

impl<'a, 'tcx> AutomaticLinksLinter<'a, 'tcx> {
fn new(cx: &'a DocContext<'tcx>) -> Self {
AutomaticLinksLinter { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") }
}

fn find_raw_urls(
&self,
text: &str,
range: Range<usize>,
f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
) {
// For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
for match_ in self.regex.find_iter(&text) {
let url = match_.as_str();
let url_range = match_.range();
f(
self.cx,
"this URL is not a hyperlink",
url,
Range { start: range.start + url_range.start, end: range.start + url_range.end },
);
}
}
}

pub fn check_automatic_links(krate: Crate, cx: &DocContext<'_>) -> Crate {
if !UnstableFeatures::from_environment().is_nightly_build() {
krate
} else {
let mut coll = AutomaticLinksLinter::new(cx);

coll.fold_crate(krate)
}
}

impl<'a, 'tcx> DocFolder for AutomaticLinksLinter<'a, 'tcx> {
fn fold_item(&mut self, item: Item) -> Option<Item> {
let hir_id = match self.cx.as_local_hir_id(item.def_id) {
Some(hir_id) => hir_id,
None => {
// If non-local, no need to check anything.
return self.fold_item_recur(item);
}
};
let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
if !dox.is_empty() {
let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
let sp = super::source_span_for_markdown_range(cx, &dox, &range, &item.attrs)
.or_else(|| span_of_attrs(&item.attrs))
.unwrap_or(item.source.span());
cx.tcx.struct_span_lint_hir(lint::builtin::AUTOMATIC_LINKS, hir_id, sp, |lint| {
lint.build(msg)
.span_suggestion(
sp,
"use an automatic link instead",
format!("<{}>", url),
Applicability::MachineApplicable,
)
.emit()
});
};

let p = Parser::new_ext(&dox, opts()).into_offset_iter();

let mut title = String::new();
let mut in_link = false;
let mut ignore = false;

for (event, range) in p {
match event {
Event::Start(Tag::Link(kind, _, _)) => {
in_link = true;
ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
}
Event::End(Tag::Link(_, url, _)) => {
in_link = false;
// NOTE: links cannot be nested, so we don't need to check `kind`
if url.as_ref() == title && !ignore {
report_diag(self.cx, "unneeded long form for URL", &url, range);
}
title.clear();
ignore = false;
}
Event::Text(s) if in_link => {
if !ignore {
title.push_str(&s);
}
}
Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
_ => {}
}
}
}

self.fold_item_recur(item)
}
}
5 changes: 5 additions & 0 deletions src/librustdoc/passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use crate::core::DocContext;
mod stripper;
pub use stripper::*;

mod automatic_links;
pub use self::automatic_links::CHECK_AUTOMATIC_LINKS;

mod collapse_docs;
pub use self::collapse_docs::COLLAPSE_DOCS;

Expand Down Expand Up @@ -90,6 +93,7 @@ pub const PASSES: &[Pass] = &[
COLLECT_TRAIT_IMPLS,
CALCULATE_DOC_COVERAGE,
CHECK_INVALID_HTML_TAGS,
CHECK_AUTOMATIC_LINKS,
];

/// The list of passes run by default.
Expand All @@ -105,6 +109,7 @@ pub const DEFAULT_PASSES: &[ConditionalPass] = &[
ConditionalPass::always(CHECK_CODE_BLOCK_SYNTAX),
ConditionalPass::always(CHECK_INVALID_HTML_TAGS),
ConditionalPass::always(PROPAGATE_DOC_CFG),
ConditionalPass::always(CHECK_AUTOMATIC_LINKS),
];

/// The list of default passes run when `--doc-coverage` is passed to rustdoc.
Expand Down
60 changes: 60 additions & 0 deletions src/test/rustdoc-ui/automatic-links.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#![deny(automatic_links)]

/// [http://a.com](http://a.com)
//~^ ERROR unneeded long form for URL
/// [http://b.com]
//~^ ERROR unneeded long form for URL
///
/// [http://b.com]: http://b.com
///
/// [http://c.com][http://c.com]
pub fn a() {}

/// https://somewhere.com
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com/a
//~^ ERROR this URL is not a hyperlink
/// https://www.somewhere.com
//~^ ERROR this URL is not a hyperlink
/// https://www.somewhere.com/a
//~^ ERROR this URL is not a hyperlink
/// https://subdomain.example.com
//~^ ERROR not a hyperlink
/// https://somewhere.com?
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com/a?
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com?hello=12
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com/a?hello=12
//~^ ERROR this URL is not a hyperlink
/// https://example.com?hello=12#xyz
//~^ ERROR this URL is not a hyperlink
/// https://example.com/a?hello=12#xyz
//~^ ERROR this URL is not a hyperlink
/// https://example.com#xyz
//~^ ERROR this URL is not a hyperlink
/// https://example.com/a#xyz
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com?hello=12&bye=11
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com/a?hello=12&bye=11
//~^ ERROR this URL is not a hyperlink
/// https://somewhere.com?hello=12&bye=11#xyz
//~^ ERROR this URL is not a hyperlink
/// hey! https://somewhere.com/a?hello=12&bye=11#xyz
//~^ ERROR this URL is not a hyperlink
pub fn c() {}

/// <https://somewhere.com>
/// [a](http://a.com)
/// [b]
///
/// [b]: http://b.com
pub fn everything_is_fine_here() {}

#[allow(automatic_links)]
pub mod foo {
/// https://somewhere.com/a?hello=12&bye=11#xyz
pub fn bar() {}
}
Loading

0 comments on commit 6db16ff

Please sign in to comment.