diff --git a/Cargo.lock b/Cargo.lock index 5ca804cb739eb..a7202348a4594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4233,6 +4233,7 @@ dependencies = [ "itertools 0.9.0", "minifier", "pulldown-cmark 0.8.0", + "regex", "rustc-rayon", "serde", "serde_json", diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs index 1db59bfc39dce..7e97b20e8ac59 100644 --- a/compiler/rustc_lint/src/lib.rs +++ b/compiler/rustc_lint/src/lib.rs @@ -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, }; @@ -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, diff --git a/compiler/rustc_session/src/lint/builtin.rs b/compiler/rustc_session/src/lint/builtin.rs index fef3164de59be..63f5766d50dde 100644 --- a/compiler/rustc_session/src/lint/builtin.rs +++ b/compiler/rustc_session/src/lint/builtin.rs @@ -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]. @@ -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, diff --git a/library/core/src/intrinsics.rs b/library/core/src/intrinsics.rs index 2a7c105e807f4..122f795570432 100644 --- a/library/core/src/intrinsics.rs +++ b/library/core/src/intrinsics.rs @@ -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 to //! `compiler/rustc_mir/src/interpret/intrinsics.rs` and add a //! `#[rustc_const_unstable(feature = "foo", issue = "01234")]` to the intrinsic. //! diff --git a/library/core/src/lib.rs b/library/core/src/lib.rs index 97f27566eb0f4..89750b6bd9a39 100644 --- a/library/core/src/lib.rs +++ b/library/core/src/lib.rs @@ -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)] diff --git a/library/core/src/num/dec2flt/mod.rs b/library/core/src/num/dec2flt/mod.rs index 6f3a3a867450d..039112e9f3468 100644 --- a/library/core/src/num/dec2flt/mod.rs +++ b/library/core/src/num/dec2flt/mod.rs @@ -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: //! //! 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 diff --git a/library/core/src/slice/sort.rs b/library/core/src/slice/sort.rs index 71d2c2c9b2f4c..2a7693d27efa2 100644 --- a/library/core/src/slice/sort.rs +++ b/library/core/src/slice/sort.rs @@ -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: //! //! Unstable sorting is compatible with libcore because it doesn't allocate memory, unlike our //! stable sorting implementation. diff --git a/src/doc/rustdoc/src/lints.md b/src/doc/rustdoc/src/lints.md index cb9099cd50bee..68ea828bfc1e5 100644 --- a/src/doc/rustdoc/src/lints.md +++ b/src/doc/rustdoc/src/lints.md @@ -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: `` + | + +warning: unneeded long form for URL + --> foo.rs:4:5 + | +4 | /// [http://a.com](http://a.com) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +warning: unneeded long form for URL + --> foo.rs:5:5 + | +5 | /// [http://b.com] + | ^^^^^^^^^^^^^^ help: use an automatic link instead: `` +``` diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml index a40a44fe27da3..b0f5bac6abd0f 100644 --- a/src/librustdoc/Cargo.toml +++ b/src/librustdoc/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0" smallvec = "1.0" tempfile = "3" itertools = "0.9" +regex = "1" [dev-dependencies] expect-test = "1.0" diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 3b72cf5c4f97a..32b3f69ecd4f0 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -14,6 +14,7 @@ use rustc_ast::{self as ast, AttrStyle}; use rustc_ast::{FloatTy, IntTy, UintTy}; use rustc_attr::{Stability, StabilityLevel}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_feature::UnstableFeatures; use rustc_hir as hir; use rustc_hir::def::Res; use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE}; @@ -679,9 +680,13 @@ impl Attributes { "../".repeat(depth) } Some(&(_, _, ExternalLocation::Remote(ref s))) => s.to_string(), - Some(&(_, _, ExternalLocation::Unknown)) | None => { - String::from("https://doc.rust-lang.org/nightly") - } + Some(&(_, _, ExternalLocation::Unknown)) | None => String::from( + if UnstableFeatures::from_environment().is_nightly_build() { + "https://doc.rust-lang.org/nightly" + } else { + "https://doc.rust-lang.org" + }, + ), }; // This is a primitive so the url is done "by hand". let tail = fragment.find('#').unwrap_or_else(|| fragment.len()); diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index 45a84c4fb30d3..f834be84d4c59 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -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(), diff --git a/src/librustdoc/html/static/main.js b/src/librustdoc/html/static/main.js index 1b3eb2011afdc..67e50bba1f2e0 100644 --- a/src/librustdoc/html/static/main.js +++ b/src/librustdoc/html/static/main.js @@ -2716,10 +2716,6 @@ function defocusSearchBar() { }; } - window.onresize = function() { - hideSidebar(); - }; - if (main) { onEachLazy(main.getElementsByClassName("loading-content"), function(e) { e.remove(); diff --git a/src/librustdoc/passes/automatic_links.rs b/src/librustdoc/passes/automatic_links.rs new file mode 100644 index 0000000000000..816d2fd15eee5 --- /dev/null +++ b/src/librustdoc/passes/automatic_links.rs @@ -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, + f: &impl Fn(&DocContext<'_>, &str, &str, Range), + ) { + // 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 { + 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| { + 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) + } +} diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs index 2591650e3f97f..48dc52c955006 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -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; @@ -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. @@ -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. diff --git a/src/test/rustdoc-ui/automatic-links.rs b/src/test/rustdoc-ui/automatic-links.rs new file mode 100644 index 0000000000000..27eb4e4a646ce --- /dev/null +++ b/src/test/rustdoc-ui/automatic-links.rs @@ -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() {} + +/// +/// [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() {} +} diff --git a/src/test/rustdoc-ui/automatic-links.stderr b/src/test/rustdoc-ui/automatic-links.stderr new file mode 100644 index 0000000000000..00e210aaaa22d --- /dev/null +++ b/src/test/rustdoc-ui/automatic-links.stderr @@ -0,0 +1,122 @@ +error: unneeded long form for URL + --> $DIR/automatic-links.rs:3:5 + | +LL | /// [http://a.com](http://a.com) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + | +note: the lint level is defined here + --> $DIR/automatic-links.rs:1:9 + | +LL | #![deny(automatic_links)] + | ^^^^^^^^^^^^^^^ + +error: unneeded long form for URL + --> $DIR/automatic-links.rs:5:5 + | +LL | /// [http://b.com] + | ^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:13:5 + | +LL | /// https://somewhere.com + | ^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:15:5 + | +LL | /// https://somewhere.com/a + | ^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:17:5 + | +LL | /// https://www.somewhere.com + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:19:5 + | +LL | /// https://www.somewhere.com/a + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:21:5 + | +LL | /// https://subdomain.example.com + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:23:5 + | +LL | /// https://somewhere.com? + | ^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:25:5 + | +LL | /// https://somewhere.com/a? + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:27:5 + | +LL | /// https://somewhere.com?hello=12 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:29:5 + | +LL | /// https://somewhere.com/a?hello=12 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:31:5 + | +LL | /// https://example.com?hello=12#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:33:5 + | +LL | /// https://example.com/a?hello=12#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:35:5 + | +LL | /// https://example.com#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:37:5 + | +LL | /// https://example.com/a#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:39:5 + | +LL | /// https://somewhere.com?hello=12&bye=11 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:41:5 + | +LL | /// https://somewhere.com/a?hello=12&bye=11 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:43:5 + | +LL | /// https://somewhere.com?hello=12&bye=11#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: this URL is not a hyperlink + --> $DIR/automatic-links.rs:45:10 + | +LL | /// hey! https://somewhere.com/a?hello=12&bye=11#xyz + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: aborting due to 19 previous errors +