about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDylan DPC <dylan.dpc@gmail.com>2020-04-23 20:35:01 +0200
committerGitHub <noreply@github.com>2020-04-23 20:35:01 +0200
commit0959802a57987e4ba052f5b8d0fa110aa918ef1c (patch)
tree41bab280870ee0e95655a4c13dee1451b72392b1
parent2a1cd440f2264d0ca33393d2f71f5d851b5c3aa6 (diff)
parent977603cd1f62c24bfa788fb6ade53660bdf2f2c2 (diff)
downloadrust-0959802a57987e4ba052f5b8d0fa110aa918ef1c.tar.gz
rust-0959802a57987e4ba052f5b8d0fa110aa918ef1c.zip
Rollup merge of #71408 - GuillaumeGomez:check-code-blocks-tags, r=kinnison
Check code blocks tags

Fixes #71347.

Explanations here: I realized recently that it was a common issue to confuse/misspell tags on code blocks. This is actually quite a big issue since it generally ends up in a code blocks being ignored since it's not being considered as a rust one. With this new warning, users will at least be notified about it.

PS: some improvements can be done on the error rendering but considering how big the PR already is, I think it's better to do it afterwards.

r? @ollie27

cc @rust-lang/rustdoc
-rw-r--r--src/librustc_lint/lib.rs4
-rw-r--r--src/librustc_session/lint/builtin.rs7
-rw-r--r--src/librustdoc/core.rs7
-rw-r--r--src/librustdoc/html/markdown.rs118
-rw-r--r--src/librustdoc/html/markdown/tests.rs2
-rw-r--r--src/librustdoc/markdown.rs2
-rw-r--r--src/librustdoc/passes/check_code_block_syntax.rs6
-rw-r--r--src/librustdoc/passes/mod.rs2
-rw-r--r--src/librustdoc/test.rs97
-rw-r--r--src/test/rustdoc-ui/check-attr-test.rs38
-rw-r--r--src/test/rustdoc-ui/check-attr-test.stderr187
-rw-r--r--src/test/rustdoc-ui/check-attr.rs51
-rw-r--r--src/test/rustdoc-ui/check-attr.stderr217
13 files changed, 711 insertions, 27 deletions
diff --git a/src/librustc_lint/lib.rs b/src/librustc_lint/lib.rs
index 8f67fe294eb..bcd59702289 100644
--- a/src/librustc_lint/lib.rs
+++ b/src/librustc_lint/lib.rs
@@ -61,7 +61,8 @@ use rustc_middle::ty::query::Providers;
 use rustc_middle::ty::TyCtxt;
 use rustc_session::lint::builtin::{
     BARE_TRAIT_OBJECTS, ELIDED_LIFETIMES_IN_PATHS, EXPLICIT_OUTLIVES_REQUIREMENTS,
-    INTRA_DOC_LINK_RESOLUTION_FAILURE, MISSING_DOC_CODE_EXAMPLES, PRIVATE_DOC_TESTS,
+    INTRA_DOC_LINK_RESOLUTION_FAILURE, INVALID_CODEBLOCK_ATTRIBUTE, MISSING_DOC_CODE_EXAMPLES,
+    PRIVATE_DOC_TESTS,
 };
 use rustc_span::Span;
 
@@ -299,6 +300,7 @@ fn register_builtins(store: &mut LintStore, no_interleave_lints: bool) {
     add_lint_group!(
         "rustdoc",
         INTRA_DOC_LINK_RESOLUTION_FAILURE,
+        INVALID_CODEBLOCK_ATTRIBUTE,
         MISSING_DOC_CODE_EXAMPLES,
         PRIVATE_DOC_TESTS
     );
diff --git a/src/librustc_session/lint/builtin.rs b/src/librustc_session/lint/builtin.rs
index ce09fd299fe..3ace9ecbd60 100644
--- a/src/librustc_session/lint/builtin.rs
+++ b/src/librustc_session/lint/builtin.rs
@@ -387,6 +387,12 @@ declare_lint! {
 }
 
 declare_lint! {
+    pub INVALID_CODEBLOCK_ATTRIBUTE,
+    Warn,
+    "codeblock attribute looks a lot like a known one"
+}
+
+declare_lint! {
     pub MISSING_CRATE_LEVEL_DOCS,
     Allow,
     "detects crates with no crate-level documentation"
@@ -553,6 +559,7 @@ declare_lint_pass! {
         UNSTABLE_NAME_COLLISIONS,
         IRREFUTABLE_LET_PATTERNS,
         INTRA_DOC_LINK_RESOLUTION_FAILURE,
+        INVALID_CODEBLOCK_ATTRIBUTE,
         MISSING_CRATE_LEVEL_DOCS,
         MISSING_DOC_CODE_EXAMPLES,
         PRIVATE_DOC_TESTS,
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index fe2727c962c..f83bb9b1162 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -253,6 +253,7 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
     let missing_doc_example = rustc_lint::builtin::MISSING_DOC_CODE_EXAMPLES.name;
     let private_doc_tests = rustc_lint::builtin::PRIVATE_DOC_TESTS.name;
     let no_crate_level_docs = rustc_lint::builtin::MISSING_CRATE_LEVEL_DOCS.name;
+    let invalid_codeblock_attribute_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTE.name;
 
     // In addition to those specific lints, we also need to whitelist those given through
     // command line, otherwise they'll get ignored and we don't want that.
@@ -263,6 +264,7 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
         missing_doc_example.to_owned(),
         private_doc_tests.to_owned(),
         no_crate_level_docs.to_owned(),
+        invalid_codeblock_attribute_name.to_owned(),
     ];
 
     whitelisted_lints.extend(lint_opts.iter().map(|(lint, _)| lint).cloned());
@@ -275,7 +277,10 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
 
     let lint_opts = lints()
         .filter_map(|lint| {
-            if lint.name == warnings_lint_name || lint.name == intra_link_resolution_failure_name {
+            if lint.name == warnings_lint_name
+                || lint.name == intra_link_resolution_failure_name
+                || lint.name == invalid_codeblock_attribute_name
+            {
                 None
             } else {
                 Some((lint.name_lower(), lint::Allow))
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 941e3a5fa98..862c20dd16a 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -20,7 +20,12 @@
 #![allow(non_camel_case_types)]
 
 use rustc_data_structures::fx::FxHashMap;
+use rustc_hir::def_id::DefId;
+use rustc_hir::HirId;
+use rustc_middle::ty::TyCtxt;
+use rustc_session::lint;
 use rustc_span::edition::Edition;
+use rustc_span::Span;
 use std::borrow::Cow;
 use std::cell::RefCell;
 use std::collections::VecDeque;
@@ -192,7 +197,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
         if let Some(Event::Start(Tag::CodeBlock(kind))) = event {
             let parse_result = match kind {
                 CodeBlockKind::Fenced(ref lang) => {
-                    LangString::parse(&lang, self.check_error_codes, false)
+                    LangString::parse_without_check(&lang, self.check_error_codes, false)
                 }
                 CodeBlockKind::Indented => LangString::all_false(),
             };
@@ -560,6 +565,7 @@ pub fn find_testable_code<T: test::Tester>(
     tests: &mut T,
     error_codes: ErrorCodes,
     enable_per_target_ignores: bool,
+    extra_info: Option<&ExtraInfo<'_, '_>>,
 ) {
     let mut parser = Parser::new(doc).into_offset_iter();
     let mut prev_offset = 0;
@@ -573,7 +579,12 @@ pub fn find_testable_code<T: test::Tester>(
                         if lang.is_empty() {
                             LangString::all_false()
                         } else {
-                            LangString::parse(lang, error_codes, enable_per_target_ignores)
+                            LangString::parse(
+                                lang,
+                                error_codes,
+                                enable_per_target_ignores,
+                                extra_info,
+                            )
                         }
                     }
                     CodeBlockKind::Indented => LangString::all_false(),
@@ -615,6 +626,49 @@ pub fn find_testable_code<T: test::Tester>(
     }
 }
 
+pub struct ExtraInfo<'a, 'b> {
+    hir_id: Option<HirId>,
+    item_did: Option<DefId>,
+    sp: Span,
+    tcx: &'a TyCtxt<'b>,
+}
+
+impl<'a, 'b> ExtraInfo<'a, 'b> {
+    pub fn new(tcx: &'a TyCtxt<'b>, hir_id: HirId, sp: Span) -> ExtraInfo<'a, 'b> {
+        ExtraInfo { hir_id: Some(hir_id), item_did: None, sp, tcx }
+    }
+
+    pub fn new_did(tcx: &'a TyCtxt<'b>, did: DefId, sp: Span) -> ExtraInfo<'a, 'b> {
+        ExtraInfo { hir_id: None, item_did: Some(did), sp, tcx }
+    }
+
+    fn error_invalid_codeblock_attr(&self, msg: &str, help: &str) {
+        let hir_id = match (self.hir_id, self.item_did) {
+            (Some(h), _) => h,
+            (None, Some(item_did)) => {
+                match self.tcx.hir().as_local_hir_id(item_did) {
+                    Some(hir_id) => hir_id,
+                    None => {
+                        // If non-local, no need to check anything.
+                        return;
+                    }
+                }
+            }
+            (None, None) => return,
+        };
+        self.tcx.struct_span_lint_hir(
+            lint::builtin::INVALID_CODEBLOCK_ATTRIBUTE,
+            hir_id,
+            self.sp,
+            |lint| {
+                let mut diag = lint.build(msg);
+                diag.help(help);
+                diag.emit();
+            },
+        );
+    }
+}
+
 #[derive(Eq, PartialEq, Clone, Debug)]
 pub struct LangString {
     original: String,
@@ -652,10 +706,19 @@ impl LangString {
         }
     }
 
+    fn parse_without_check(
+        string: &str,
+        allow_error_code_check: ErrorCodes,
+        enable_per_target_ignores: bool,
+    ) -> LangString {
+        Self::parse(string, allow_error_code_check, enable_per_target_ignores, None)
+    }
+
     fn parse(
         string: &str,
         allow_error_code_check: ErrorCodes,
         enable_per_target_ignores: bool,
+        extra: Option<&ExtraInfo<'_, '_>>,
     ) -> LangString {
         let allow_error_code_check = allow_error_code_check.as_bool();
         let mut seen_rust_tags = false;
@@ -715,6 +778,53 @@ impl LangString {
                         seen_other_tags = true;
                     }
                 }
+                x if extra.is_some() => {
+                    let s = x.to_lowercase();
+                    match if s == "compile-fail" || s == "compile_fail" || s == "compilefail" {
+                        Some((
+                            "compile_fail",
+                            "the code block will either not be tested if not marked as a rust one \
+                             or won't fail if it compiles successfully",
+                        ))
+                    } else if s == "should-panic" || s == "should_panic" || s == "shouldpanic" {
+                        Some((
+                            "should_panic",
+                            "the code block will either not be tested if not marked as a rust one \
+                             or won't fail if it doesn't panic when running",
+                        ))
+                    } else if s == "no-run" || s == "no_run" || s == "norun" {
+                        Some((
+                            "no_run",
+                            "the code block will either not be tested if not marked as a rust one \
+                             or will be run (which you might not want)",
+                        ))
+                    } else if s == "allow-fail" || s == "allow_fail" || s == "allowfail" {
+                        Some((
+                            "allow_fail",
+                            "the code block will either not be tested if not marked as a rust one \
+                             or will be run (which you might not want)",
+                        ))
+                    } else if s == "test-harness" || s == "test_harness" || s == "testharness" {
+                        Some((
+                            "test_harness",
+                            "the code block will either not be tested if not marked as a rust one \
+                             or the code will be wrapped inside a main function",
+                        ))
+                    } else {
+                        None
+                    } {
+                        Some((flag, help)) => {
+                            if let Some(ref extra) = extra {
+                                extra.error_invalid_codeblock_attr(
+                                    &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag),
+                                    help,
+                                );
+                            }
+                        }
+                        None => {}
+                    }
+                    seen_other_tags = true;
+                }
                 _ => seen_other_tags = true,
             }
         }
@@ -934,7 +1044,7 @@ crate struct RustCodeBlock {
 
 /// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or
 /// untagged (and assumed to be rust).
-crate fn rust_code_blocks(md: &str) -> Vec<RustCodeBlock> {
+crate fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_, '_>) -> Vec<RustCodeBlock> {
     let mut code_blocks = vec![];
 
     if md.is_empty() {
@@ -951,7 +1061,7 @@ crate fn rust_code_blocks(md: &str) -> Vec<RustCodeBlock> {
                     let lang_string = if syntax.is_empty() {
                         LangString::all_false()
                     } else {
-                        LangString::parse(&*syntax, ErrorCodes::Yes, false)
+                        LangString::parse(&*syntax, ErrorCodes::Yes, false, Some(extra_info))
                     };
                     if !lang_string.rust {
                         continue;
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 48231ce7b73..c871587a0a9 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -64,7 +64,7 @@ fn test_lang_string_parse() {
         edition: Option<Edition>,
     ) {
         assert_eq!(
-            LangString::parse(s, ErrorCodes::Yes, true),
+            LangString::parse(s, ErrorCodes::Yes, true, None),
             LangString {
                 should_panic,
                 no_run,
diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs
index 0a1b5f58815..b4c0f0ac4c5 100644
--- a/src/librustdoc/markdown.rs
+++ b/src/librustdoc/markdown.rs
@@ -153,7 +153,7 @@ pub fn test(mut options: Options, diag: &rustc_errors::Handler) -> i32 {
     collector.set_position(DUMMY_SP);
     let codes = ErrorCodes::from(UnstableFeatures::from_environment().is_nightly_build());
 
-    find_testable_code(&input_str, &mut collector, codes, options.enable_per_target_ignores);
+    find_testable_code(&input_str, &mut collector, codes, options.enable_per_target_ignores, None);
 
     options.test_args.insert(0, "rustdoctest".to_string());
     testing::test_main(
diff --git a/src/librustdoc/passes/check_code_block_syntax.rs b/src/librustdoc/passes/check_code_block_syntax.rs
index 3e0ff0b3d9a..d1f2c12ccd6 100644
--- a/src/librustdoc/passes/check_code_block_syntax.rs
+++ b/src/librustdoc/passes/check_code_block_syntax.rs
@@ -10,7 +10,7 @@ use crate::clean;
 use crate::core::DocContext;
 use crate::fold::DocFolder;
 use crate::html::markdown::{self, RustCodeBlock};
-use crate::passes::Pass;
+use crate::passes::{span_of_attrs, Pass};
 
 pub const CHECK_CODE_BLOCK_SYNTAX: Pass = Pass {
     name: "check-code-block-syntax",
@@ -114,7 +114,9 @@ impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> {
 impl<'a, 'tcx> DocFolder for SyntaxChecker<'a, 'tcx> {
     fn fold_item(&mut self, item: clean::Item) -> Option<clean::Item> {
         if let Some(dox) = &item.attrs.collapsed_doc_value() {
-            for code_block in markdown::rust_code_blocks(&dox) {
+            let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span());
+            let extra = crate::html::markdown::ExtraInfo::new_did(&self.cx.tcx, item.def_id, sp);
+            for code_block in markdown::rust_code_blocks(&dox, &extra) {
                 self.check_rust_syntax(&item, &dox, code_block);
             }
         }
diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs
index 1afa9c7a268..70366c90139 100644
--- a/src/librustdoc/passes/mod.rs
+++ b/src/librustdoc/passes/mod.rs
@@ -338,7 +338,7 @@ pub fn look_for_tests<'tcx>(
 
     let mut tests = Tests { found_tests: 0 };
 
-    find_testable_code(&dox, &mut tests, ErrorCodes::No, false);
+    find_testable_code(&dox, &mut tests, ErrorCodes::No, false, None);
 
     if check_missing_code && tests.found_tests == 0 {
         let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span());
diff --git a/src/librustdoc/test.rs b/src/librustdoc/test.rs
index afc1501d7b6..e2940efd49c 100644
--- a/src/librustdoc/test.rs
+++ b/src/librustdoc/test.rs
@@ -5,9 +5,11 @@ use rustc_errors::ErrorReported;
 use rustc_feature::UnstableFeatures;
 use rustc_hir as hir;
 use rustc_hir::intravisit;
+use rustc_hir::{HirId, CRATE_HIR_ID};
 use rustc_interface::interface;
 use rustc_middle::hir::map::Map;
-use rustc_session::{self, config, DiagnosticOutput, Session};
+use rustc_middle::ty::TyCtxt;
+use rustc_session::{self, config, lint, DiagnosticOutput, Session};
 use rustc_span::edition::Edition;
 use rustc_span::source_map::SourceMap;
 use rustc_span::symbol::sym;
@@ -25,6 +27,7 @@ use tempfile::Builder as TempFileBuilder;
 use crate::clean::Attributes;
 use crate::config::Options;
 use crate::html::markdown::{self, ErrorCodes, Ignore, LangString};
+use crate::passes::span_of_attrs;
 
 #[derive(Clone, Default)]
 pub struct TestOptions {
@@ -40,6 +43,45 @@ pub struct TestOptions {
 pub fn run(options: Options) -> i32 {
     let input = config::Input::File(options.input.clone());
 
+    let warnings_lint_name = lint::builtin::WARNINGS.name;
+    let invalid_codeblock_attribute_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTE.name;
+
+    // In addition to those specific lints, we also need to whitelist those given through
+    // command line, otherwise they'll get ignored and we don't want that.
+    let mut whitelisted_lints =
+        vec![warnings_lint_name.to_owned(), invalid_codeblock_attribute_name.to_owned()];
+
+    whitelisted_lints.extend(options.lint_opts.iter().map(|(lint, _)| lint).cloned());
+
+    let lints = || {
+        lint::builtin::HardwiredLints::get_lints()
+            .into_iter()
+            .chain(rustc_lint::SoftLints::get_lints().into_iter())
+    };
+
+    let lint_opts = lints()
+        .filter_map(|lint| {
+            if lint.name == warnings_lint_name || lint.name == invalid_codeblock_attribute_name {
+                None
+            } else {
+                Some((lint.name_lower(), lint::Allow))
+            }
+        })
+        .chain(options.lint_opts.clone().into_iter())
+        .collect::<Vec<_>>();
+
+    let lint_caps = lints()
+        .filter_map(|lint| {
+            // We don't want to whitelist *all* lints so let's
+            // ignore those ones.
+            if whitelisted_lints.iter().any(|l| lint.name == l) {
+                None
+            } else {
+                Some((lint::LintId::of(lint), lint::Allow))
+            }
+        })
+        .collect();
+
     let crate_types = if options.proc_macro_crate {
         vec![config::CrateType::ProcMacro]
     } else {
@@ -50,10 +92,11 @@ pub fn run(options: Options) -> i32 {
         maybe_sysroot: options.maybe_sysroot.clone(),
         search_paths: options.libs.clone(),
         crate_types,
+        lint_opts: if !options.display_warnings { lint_opts } else { vec![] },
+        lint_cap: Some(options.lint_cap.clone().unwrap_or_else(|| lint::Forbid)),
         cg: options.codegen_options.clone(),
         externs: options.externs.clone(),
         unstable_features: UnstableFeatures::from_environment(),
-        lint_cap: Some(rustc_session::lint::Level::Allow),
         actually_rustdoc: true,
         debugging_opts: config::DebuggingOptions { ..config::basic_debugging_options() },
         edition: options.edition,
@@ -75,7 +118,7 @@ pub fn run(options: Options) -> i32 {
         diagnostic_output: DiagnosticOutput::Default,
         stderr: None,
         crate_name: options.crate_name.clone(),
-        lint_caps: Default::default(),
+        lint_caps,
         register_lints: None,
         override_queries: None,
         registry: rustc_driver::diagnostics_registry(),
@@ -105,6 +148,7 @@ pub fn run(options: Options) -> i32 {
 
             global_ctxt.enter(|tcx| {
                 let krate = tcx.hir().krate();
+
                 let mut hir_collector = HirCollector {
                     sess: compiler.session(),
                     collector: &mut collector,
@@ -112,10 +156,17 @@ pub fn run(options: Options) -> i32 {
                     codes: ErrorCodes::from(
                         compiler.session().opts.unstable_features.is_nightly_build(),
                     ),
+                    tcx,
                 };
-                hir_collector.visit_testable("".to_string(), &krate.item.attrs, |this| {
-                    intravisit::walk_crate(this, krate);
-                });
+                hir_collector.visit_testable(
+                    "".to_string(),
+                    &krate.item.attrs,
+                    CRATE_HIR_ID,
+                    krate.item.span,
+                    |this| {
+                        intravisit::walk_crate(this, krate);
+                    },
+                );
             });
             compiler.session().abort_if_errors();
 
@@ -881,18 +932,21 @@ impl Tester for Collector {
     }
 }
 
-struct HirCollector<'a, 'hir> {
+struct HirCollector<'a, 'hir, 'tcx> {
     sess: &'a Session,
     collector: &'a mut Collector,
     map: Map<'hir>,
     codes: ErrorCodes,
+    tcx: TyCtxt<'tcx>,
 }
 
-impl<'a, 'hir> HirCollector<'a, 'hir> {
+impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> {
     fn visit_testable<F: FnOnce(&mut Self)>(
         &mut self,
         name: String,
         attrs: &[ast::Attribute],
+        hir_id: HirId,
+        sp: Span,
         nested: F,
     ) {
         let mut attrs = Attributes::from_ast(self.sess.diagnostic(), attrs);
@@ -918,6 +972,11 @@ impl<'a, 'hir> HirCollector<'a, 'hir> {
                 self.collector,
                 self.codes,
                 self.collector.enable_per_target_ignores,
+                Some(&crate::html::markdown::ExtraInfo::new(
+                    &self.tcx,
+                    hir_id,
+                    span_of_attrs(&attrs).unwrap_or(sp),
+                )),
             );
         }
 
@@ -929,7 +988,7 @@ impl<'a, 'hir> HirCollector<'a, 'hir> {
     }
 }
 
-impl<'a, 'hir> intravisit::Visitor<'hir> for HirCollector<'a, 'hir> {
+impl<'a, 'hir, 'tcx> intravisit::Visitor<'hir> for HirCollector<'a, 'hir, 'tcx> {
     type Map = Map<'hir>;
 
     fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap<Self::Map> {
@@ -943,25 +1002,25 @@ impl<'a, 'hir> intravisit::Visitor<'hir> for HirCollector<'a, 'hir> {
             item.ident.to_string()
         };
 
-        self.visit_testable(name, &item.attrs, |this| {
+        self.visit_testable(name, &item.attrs, item.hir_id, item.span, |this| {
             intravisit::walk_item(this, item);
         });
     }
 
     fn visit_trait_item(&mut self, item: &'hir hir::TraitItem) {
-        self.visit_testable(item.ident.to_string(), &item.attrs, |this| {
+        self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| {
             intravisit::walk_trait_item(this, item);
         });
     }
 
     fn visit_impl_item(&mut self, item: &'hir hir::ImplItem) {
-        self.visit_testable(item.ident.to_string(), &item.attrs, |this| {
+        self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| {
             intravisit::walk_impl_item(this, item);
         });
     }
 
     fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem) {
-        self.visit_testable(item.ident.to_string(), &item.attrs, |this| {
+        self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| {
             intravisit::walk_foreign_item(this, item);
         });
     }
@@ -972,19 +1031,25 @@ impl<'a, 'hir> intravisit::Visitor<'hir> for HirCollector<'a, 'hir> {
         g: &'hir hir::Generics,
         item_id: hir::HirId,
     ) {
-        self.visit_testable(v.ident.to_string(), &v.attrs, |this| {
+        self.visit_testable(v.ident.to_string(), &v.attrs, v.id, v.span, |this| {
             intravisit::walk_variant(this, v, g, item_id);
         });
     }
 
     fn visit_struct_field(&mut self, f: &'hir hir::StructField) {
-        self.visit_testable(f.ident.to_string(), &f.attrs, |this| {
+        self.visit_testable(f.ident.to_string(), &f.attrs, f.hir_id, f.span, |this| {
             intravisit::walk_struct_field(this, f);
         });
     }
 
     fn visit_macro_def(&mut self, macro_def: &'hir hir::MacroDef) {
-        self.visit_testable(macro_def.ident.to_string(), &macro_def.attrs, |_| ());
+        self.visit_testable(
+            macro_def.ident.to_string(),
+            &macro_def.attrs,
+            macro_def.hir_id,
+            macro_def.span,
+            |_| (),
+        );
     }
 }
 
diff --git a/src/test/rustdoc-ui/check-attr-test.rs b/src/test/rustdoc-ui/check-attr-test.rs
new file mode 100644
index 00000000000..c4140bbb70a
--- /dev/null
+++ b/src/test/rustdoc-ui/check-attr-test.rs
@@ -0,0 +1,38 @@
+// compile-flags:--test
+
+#![deny(invalid_codeblock_attribute)]
+
+/// foo
+///
+/// ```compile-fail,compilefail,comPile_fail
+/// boo
+/// ```
+pub fn foo() {}
+
+/// bar
+///
+/// ```should-panic,shouldpanic,shOuld_panic
+/// boo
+/// ```
+pub fn bar() {}
+
+/// foobar
+///
+/// ```no-run,norun,nO_run
+/// boo
+/// ```
+pub fn foobar() {}
+
+/// barfoo
+///
+/// ```allow-fail,allowfail,allOw_fail
+/// boo
+/// ```
+pub fn barfoo() {}
+
+/// b
+///
+/// ```test-harness,testharness,tesT_harness
+/// boo
+/// ```
+pub fn b() {}
diff --git a/src/test/rustdoc-ui/check-attr-test.stderr b/src/test/rustdoc-ui/check-attr-test.stderr
new file mode 100644
index 00000000000..45a2d6ec15e
--- /dev/null
+++ b/src/test/rustdoc-ui/check-attr-test.stderr
@@ -0,0 +1,187 @@
+error: unknown attribute `compile-fail`. Did you mean `compile_fail`?
+ --> $DIR/check-attr-test.rs:5:1
+  |
+5 | / /// foo
+6 | | ///
+7 | | /// ```compile-fail,compilefail,comPile_fail
+8 | | /// boo
+9 | | /// ```
+  | |_______^
+  |
+note: the lint level is defined here
+ --> $DIR/check-attr-test.rs:3:9
+  |
+3 | #![deny(invalid_codeblock_attribute)]
+  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `compilefail`. Did you mean `compile_fail`?
+ --> $DIR/check-attr-test.rs:5:1
+  |
+5 | / /// foo
+6 | | ///
+7 | | /// ```compile-fail,compilefail,comPile_fail
+8 | | /// boo
+9 | | /// ```
+  | |_______^
+  |
+  = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `comPile_fail`. Did you mean `compile_fail`?
+ --> $DIR/check-attr-test.rs:5:1
+  |
+5 | / /// foo
+6 | | ///
+7 | | /// ```compile-fail,compilefail,comPile_fail
+8 | | /// boo
+9 | | /// ```
+  | |_______^
+  |
+  = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `should-panic`. Did you mean `should_panic`?
+  --> $DIR/check-attr-test.rs:12:1
+   |
+12 | / /// bar
+13 | | ///
+14 | | /// ```should-panic,shouldpanic,shOuld_panic
+15 | | /// boo
+16 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `shouldpanic`. Did you mean `should_panic`?
+  --> $DIR/check-attr-test.rs:12:1
+   |
+12 | / /// bar
+13 | | ///
+14 | | /// ```should-panic,shouldpanic,shOuld_panic
+15 | | /// boo
+16 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `shOuld_panic`. Did you mean `should_panic`?
+  --> $DIR/check-attr-test.rs:12:1
+   |
+12 | / /// bar
+13 | | ///
+14 | | /// ```should-panic,shouldpanic,shOuld_panic
+15 | | /// boo
+16 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `no-run`. Did you mean `no_run`?
+  --> $DIR/check-attr-test.rs:19:1
+   |
+19 | / /// foobar
+20 | | ///
+21 | | /// ```no-run,norun,nO_run
+22 | | /// boo
+23 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `norun`. Did you mean `no_run`?
+  --> $DIR/check-attr-test.rs:19:1
+   |
+19 | / /// foobar
+20 | | ///
+21 | | /// ```no-run,norun,nO_run
+22 | | /// boo
+23 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `nO_run`. Did you mean `no_run`?
+  --> $DIR/check-attr-test.rs:19:1
+   |
+19 | / /// foobar
+20 | | ///
+21 | | /// ```no-run,norun,nO_run
+22 | | /// boo
+23 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `allow-fail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr-test.rs:26:1
+   |
+26 | / /// barfoo
+27 | | ///
+28 | | /// ```allow-fail,allowfail,allOw_fail
+29 | | /// boo
+30 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `allowfail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr-test.rs:26:1
+   |
+26 | / /// barfoo
+27 | | ///
+28 | | /// ```allow-fail,allowfail,allOw_fail
+29 | | /// boo
+30 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `allOw_fail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr-test.rs:26:1
+   |
+26 | / /// barfoo
+27 | | ///
+28 | | /// ```allow-fail,allowfail,allOw_fail
+29 | | /// boo
+30 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `test-harness`. Did you mean `test_harness`?
+  --> $DIR/check-attr-test.rs:33:1
+   |
+33 | / /// b
+34 | | ///
+35 | | /// ```test-harness,testharness,tesT_harness
+36 | | /// boo
+37 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: unknown attribute `testharness`. Did you mean `test_harness`?
+  --> $DIR/check-attr-test.rs:33:1
+   |
+33 | / /// b
+34 | | ///
+35 | | /// ```test-harness,testharness,tesT_harness
+36 | | /// boo
+37 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: unknown attribute `tesT_harness`. Did you mean `test_harness`?
+  --> $DIR/check-attr-test.rs:33:1
+   |
+33 | / /// b
+34 | | ///
+35 | | /// ```test-harness,testharness,tesT_harness
+36 | | /// boo
+37 | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: aborting due to 15 previous errors
+
diff --git a/src/test/rustdoc-ui/check-attr.rs b/src/test/rustdoc-ui/check-attr.rs
new file mode 100644
index 00000000000..a93ec291319
--- /dev/null
+++ b/src/test/rustdoc-ui/check-attr.rs
@@ -0,0 +1,51 @@
+#![deny(invalid_codeblock_attribute)]
+
+/// foo
+//~^ ERROR
+//~^^ ERROR
+//~^^^ ERROR
+///
+/// ```compile-fail,compilefail,comPile_fail
+/// boo
+/// ```
+pub fn foo() {}
+
+/// bar
+//~^ ERROR
+//~^^ ERROR
+//~^^^ ERROR
+///
+/// ```should-panic,shouldpanic,sHould_panic
+/// boo
+/// ```
+pub fn bar() {}
+
+/// foobar
+//~^ ERROR
+//~^^ ERROR
+//~^^^ ERROR
+///
+/// ```no-run,norun,no_Run
+/// boo
+/// ```
+pub fn foobar() {}
+
+/// barfoo
+//~^ ERROR
+//~^^ ERROR
+//~^^^ ERROR
+///
+/// ```allow-fail,allowfail,alLow_fail
+/// boo
+/// ```
+pub fn barfoo() {}
+
+/// b
+//~^ ERROR
+//~^^ ERROR
+//~^^^ ERROR
+///
+/// ```test-harness,testharness,teSt_harness
+/// boo
+/// ```
+pub fn b() {}
diff --git a/src/test/rustdoc-ui/check-attr.stderr b/src/test/rustdoc-ui/check-attr.stderr
new file mode 100644
index 00000000000..5d6939bd092
--- /dev/null
+++ b/src/test/rustdoc-ui/check-attr.stderr
@@ -0,0 +1,217 @@
+error: unknown attribute `compile-fail`. Did you mean `compile_fail`?
+  --> $DIR/check-attr.rs:3:1
+   |
+LL | / /// foo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+note: the lint level is defined here
+  --> $DIR/check-attr.rs:1:9
+   |
+LL | #![deny(invalid_codeblock_attribute)]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `compilefail`. Did you mean `compile_fail`?
+  --> $DIR/check-attr.rs:3:1
+   |
+LL | / /// foo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `comPile_fail`. Did you mean `compile_fail`?
+  --> $DIR/check-attr.rs:3:1
+   |
+LL | / /// foo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it compiles successfully
+
+error: unknown attribute `should-panic`. Did you mean `should_panic`?
+  --> $DIR/check-attr.rs:13:1
+   |
+LL | / /// bar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `shouldpanic`. Did you mean `should_panic`?
+  --> $DIR/check-attr.rs:13:1
+   |
+LL | / /// bar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `sHould_panic`. Did you mean `should_panic`?
+  --> $DIR/check-attr.rs:13:1
+   |
+LL | / /// bar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or won't fail if it doesn't panic when running
+
+error: unknown attribute `no-run`. Did you mean `no_run`?
+  --> $DIR/check-attr.rs:23:1
+   |
+LL | / /// foobar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `norun`. Did you mean `no_run`?
+  --> $DIR/check-attr.rs:23:1
+   |
+LL | / /// foobar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `no_Run`. Did you mean `no_run`?
+  --> $DIR/check-attr.rs:23:1
+   |
+LL | / /// foobar
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `allow-fail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr.rs:33:1
+   |
+LL | / /// barfoo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `allowfail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr.rs:33:1
+   |
+LL | / /// barfoo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `alLow_fail`. Did you mean `allow_fail`?
+  --> $DIR/check-attr.rs:33:1
+   |
+LL | / /// barfoo
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or will be run (which you might not want)
+
+error: unknown attribute `test-harness`. Did you mean `test_harness`?
+  --> $DIR/check-attr.rs:43:1
+   |
+LL | / /// b
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: unknown attribute `testharness`. Did you mean `test_harness`?
+  --> $DIR/check-attr.rs:43:1
+   |
+LL | / /// b
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: unknown attribute `teSt_harness`. Did you mean `test_harness`?
+  --> $DIR/check-attr.rs:43:1
+   |
+LL | / /// b
+LL | |
+LL | |
+LL | |
+...  |
+LL | | /// boo
+LL | | /// ```
+   | |_______^
+   |
+   = help: the code block will either not be tested if not marked as a rust one or the code will be wrapped inside a main function
+
+error: aborting due to 15 previous errors
+