diff --git a/compiler/rustc_error_messages/locales/en-US/rustdoc.ftl b/compiler/rustc_error_messages/locales/en-US/rustdoc.ftl new file mode 100644 index 0000000000000..3cabbe45d0fb8 --- /dev/null +++ b/compiler/rustc_error_messages/locales/en-US/rustdoc.ftl @@ -0,0 +1,169 @@ +rustdoc_compilation_failed = + Compilation failed, aborting rustdoc + +rustdoc_couldnt_generate_documentation = + couldn't generate documentation: {$error} + .note = failed to create or modify "{$file}" + +rustdoc_missing_crate_level_docs = + no documentation found for this crate's top-level module + .help = The following guide may be of use: + {$doc_rust_lang_org_channel}/rustdoc/how-to-write-documentation.html + +rustdoc_deprecated_attr = + the `#![doc({$attr_name})]` attribute is deprecated + .note = see issue #44136 for more information + +rustdoc_deprecated_attr_no_default_passes = + `#![doc(no_default_passes)]` no longer functions; you may want to use `#![doc(document_private_items)]` + +rustdoc_deprecated_attr_passes = + `#![doc(passes = "...")]` no longer functions; you may want to use `#![doc(document_private_items)]` + +rustdoc_deprecated_attr_plugins = + `#![doc(plugins = "...")]` no longer functions; see CVE-2018-1000622 + +rustdoc_could_not_resolve_path = + failed to resolve: could not resolve path `{$path}` + .label = could not resolve path `{$path}` + .note = this error was originally ignored because you are running `rustdoc` + .help = try running again with `rustc` or `cargo check` and you may get a more detailed error + +rustdoc_unrecognized_emission_type = + unrecognized emission type: {$kind} + +rustdoc_invalid_extern_html_root_url = + --extern-html-root-url must be of the form name=url + +rustdoc_missing_file_operand = + missing file operand + +rustdoc_too_many_file_operands = + too many file operands + +rustdoc_no_run_flag_without_test_flag = + the `--test` flag must be passed to enable `--no-run` + +rustdoc_cannot_use_out_dir_and_output_flags = + cannot use both 'out-dir' and 'output' at once + +rustdoc_option_extend_css_arg_not_file = + option --extend-css argument must be a file + +rustdoc_theme_arg_not_file = + invalid argument: "{$theme_arg}" + .help = arguments to --theme must be files + +rustdoc_theme_arg_not_css_file = + invalid argument: "{$theme_arg}" + .help = arguments to --theme must have a .css extension + +rustdoc_error_loading_theme_file = + error loading theme file: "{$theme_arg}" + +rustdoc_theme_file_missing_default_theme_css_rules = + theme file "{$theme_arg}" is missing CSS rules from the default theme + .warn = the theme may appear incorrect when loaded + .help = "to see what rules are missing, call `rustdoc --check-theme "{$theme_arg}"` + +rustdoc_unknown_input_format = + unkown input format: {$theme_arg} + +rustdoc_index_page_arg_not_file = + option `--index-page` argument must be a file + +rustdoc_unknown_crate_type = + unknown crate type: {$error} + +rustdoc_html_output_not_supported_with_show_coverage_flag = + html output format isn't supported for the --show-coverage option + +rustdoc_generate_link_to_definition_flag_not_with_html_output_format = + --generate-link-to-definition option can only be used with HTML output format + +rustdoc_scrape_examples_output_path_and_target_crate_not_used_together = + must use --scrape-examples-output-path and --scrape-examples-target-crate together + +rustdoc_scrape_tests_not_with_scrape_examples_output_path_and_target_crate = + must use --scrape-examples-output-path and --scrape-examples-target-crate with --scrape-tests + +rustdoc_flag_deprecated = + the `{$flag}` flag is deprecated + .note = see issue #44136 for more information + +rustdoc_flag_removed = + the `{$flag}` flag no longer functions + .note = see issue #44136 for more information + +rustdoc_use_document_private_items_flag = + you may want to use --document-private-items + +rustdoc_see_rustdoc_plugins_cve = + see CVE-2018-1000622 + +rustdoc_error_reading_file = + error reading `{$file_path}`: {$error} + +rustdoc_error_reading_file_not_utf8 = + error reading `{$file_path}`: not UTF-8 + +rustdoc_error_loading_examples = + failed to load examples: {$error} (for path {$path}) + +rustdoc_anonymous_imports_cannot_be_inlined = + anonymous imports cannot be inlined + .import_span = anonymous import + +rustdoc_invalid_codeblock_attribute = + unknown attribute `{$attr_name}`. Did you mean `{$suggested_attr_name}`? + .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 + .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 + .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) + .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 + +rustdoc_failed_to_read_file = + failed to read file {$path}: {$error} + +rustdoc_bare_url_not_hyperlink = + this URL is not a hyperlink + .note = bare URLs are not automatically turned into clickable links + .suggestion = use an automatic link instead + +rustdoc_missing_doc_code_examples = + missing code example in this documentation + +rustdoc_private_doc_tests = + documentation test in private item + +rustdoc_cfg_unexpected_literal = + unexpected literal + +rustdoc_cfg_expected_single_identifier = + expected a single identifier + +rustdoc_cfg_option_value_not_string_literal = + value of cfg option should be a string literal + +rustdoc_cfg_expected_one_cfg_pattern = + expected 1 cfg-pattern + +rustdoc_cfg_invalid_predicate = + invalid predicate + +rustdoc_unclosed_html_tag = + unclosed HTML tag `{$tag}` + +rustdoc_unclosed_html_comment = + Unclosed HTML comment + +rustdoc_mark_source_code = + try marking as source code + +rustdoc_unopened_html_tag = + unopened HTML tag `{$tag_name}` + +rustdoc_unclosed_quoted_html_attribute = + unclosed quoted HTML attribute on tag `{$tag_name}` + +rustdoc_invalid_self_closing_html_tag = + invalid self-closing HTML tag `{$tag_name}` diff --git a/compiler/rustc_error_messages/src/lib.rs b/compiler/rustc_error_messages/src/lib.rs index 37a51980a0888..2b84f541a632d 100644 --- a/compiler/rustc_error_messages/src/lib.rs +++ b/compiler/rustc_error_messages/src/lib.rs @@ -66,6 +66,7 @@ fluent_messages! { privacy => "../locales/en-US/privacy.ftl", query_system => "../locales/en-US/query_system.ftl", resolve => "../locales/en-US/resolve.ftl", + rustdoc => "../locales/en-US/rustdoc.ftl", save_analysis => "../locales/en-US/save_analysis.ftl", session => "../locales/en-US/session.ftl", symbol_mangling => "../locales/en-US/symbol_mangling.ftl", diff --git a/src/librustdoc/clean/cfg.rs b/src/librustdoc/clean/cfg.rs index f1853f3697df2..872130b4da805 100644 --- a/src/librustdoc/clean/cfg.rs +++ b/src/librustdoc/clean/cfg.rs @@ -13,8 +13,7 @@ use rustc_feature::Features; use rustc_session::parse::ParseSess; use rustc_span::symbol::{sym, Symbol}; -use rustc_span::Span; - +use crate::errors::InvalidCfgError; use crate::html::escape::Escape; #[cfg(test)] @@ -36,12 +35,6 @@ pub(crate) enum Cfg { All(Vec), } -#[derive(PartialEq, Debug)] -pub(crate) struct InvalidCfgError { - pub(crate) msg: &'static str, - pub(crate) span: Span, -} - impl Cfg { /// Parses a `NestedMetaItem` into a `Cfg`. fn parse_nested( @@ -51,7 +44,7 @@ impl Cfg { match nested_cfg { NestedMetaItem::MetaItem(ref cfg) => Cfg::parse_without(cfg, exclude), NestedMetaItem::Lit(ref lit) => { - Err(InvalidCfgError { msg: "unexpected literal", span: lit.span }) + Err(InvalidCfgError::UnexpectedLiteral { span: lit.span }) } } } @@ -63,10 +56,7 @@ impl Cfg { let name = match cfg.ident() { Some(ident) => ident.name, None => { - return Err(InvalidCfgError { - msg: "expected a single identifier", - span: cfg.span, - }); + return Err(InvalidCfgError::ExpectedSingleIdentifier { span: cfg.span }); } }; match cfg.kind { @@ -79,12 +69,9 @@ impl Cfg { let cfg = Cfg::Cfg(name, Some(value)); if exclude.contains(&cfg) { Ok(None) } else { Ok(Some(cfg)) } } - _ => Err(InvalidCfgError { - // FIXME: if the main #[cfg] syntax decided to support non-string literals, - // this should be changed as well. - msg: "value of cfg option should be a string literal", - span: lit.span, - }), + // FIXME: if the main #[cfg] syntax decided to support non-string literals, this + // should be changed as well. + _ => Err(InvalidCfgError::OptionValueNotStringLiteral { span: lit.span }), }, MetaItemKind::List(ref items) => { let orig_len = items.len(); @@ -102,15 +89,12 @@ impl Cfg { return Ok(None); } } else { - Err(InvalidCfgError { msg: "expected 1 cfg-pattern", span: cfg.span }) + Err(InvalidCfgError::ExpectedOneCfgPattern { span: cfg.span }) } } - _ => Err(InvalidCfgError { msg: "invalid predicate", span: cfg.span }), + _ => Err(InvalidCfgError::InvalidPredicate { span: cfg.span }), }; - match ret { - Ok(c) => Ok(Some(c)), - Err(e) => Err(e), - } + ret.map(|c| Some(c)) } } } diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 415e7d5a360d0..da484c4c8c1fb 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -38,6 +38,7 @@ use std::mem; use thin_vec::ThinVec; use crate::core::{self, DocContext, ImplTraitParam}; +use crate::errors::AnonymousImportsCannotBeInlined; use crate::formats::item_type::ItemType; use crate::visit_ast::Module as DocModule; @@ -2405,14 +2406,10 @@ fn clean_use_statement_inner<'tcx>( if pub_underscore { if let Some(ref inline) = inline_attr { - rustc_errors::struct_span_err!( - cx.tcx.sess, - inline.span(), - E0780, - "anonymous imports cannot be inlined" - ) - .span_label(import.span, "anonymous import") - .emit(); + cx.tcx.sess.emit_err(AnonymousImportsCannotBeInlined { + inline_span: inline.span(), + import_span: import.span, + }); } } diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 87de41fde63c5..0053ea05d44ee 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -971,7 +971,7 @@ impl AttributesExt for [ast::Attribute] { match Cfg::parse(cfg_mi) { Ok(new_cfg) => cfg &= new_cfg, Err(e) => { - sess.span_err(e.span, e.msg); + sess.emit_err(e); } } } diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 56b40d8c66baf..8980f4f27e52a 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use rustc_data_structures::fx::FxHashMap; use rustc_driver::print_flag_list; +use rustc_errors::ErrorGuaranteed; use rustc_session::config::{ self, parse_crate_types_from_list, parse_externs, parse_target_triple, CrateType, }; @@ -21,6 +22,14 @@ use rustc_span::edition::Edition; use rustc_target::spec::TargetTriple; use crate::core::new_handler; +use crate::errors::{ + CannotUseOutDirAndOutputFlags, ErrorLoadingThemeFile, ExtendCssArgNotFile, FlagDeprecated, + FlagRemoved, FlagRemovedSuggestion, GenerateLinkToDefinitionFlagNotWithHtmlOutputFormat, + HtmlOutputNotSupportedWithShowCoverageFlag, IndexPageArgNotFile, InvalidExternHtmlRootUrl, + MissingFileOperand, NoRunFlagWithoutTestFlag, ThemeArgNotCssFile, ThemeArgNotFile, + ThemeFileMissingDefaultThemeCssRules, TooManyFileOperands, UnknownCrateType, + UnknownInputFormat, UnrecognizedEmissionType, +}; use crate::externalfiles::ExternalHtml; use crate::html; use crate::html::markdown::IdMap; @@ -396,7 +405,7 @@ impl Options { match kind.parse() { Ok(kind) => emit.push(kind), Err(()) => { - diag.err(&format!("unrecognized emission type: {}", kind)); + diag.emit_err(UnrecognizedEmissionType { kind }); return Err(1); } } @@ -452,10 +461,10 @@ impl Options { let input = PathBuf::from(if describe_lints { "" // dummy, this won't be used } else if matches.free.is_empty() { - diag.struct_err("missing file operand").emit(); + diag.emit_err(MissingFileOperand); return Err(1); } else if matches.free.len() > 1 { - diag.struct_err("too many file operands").emit(); + diag.emit_err(TooManyFileOperands); return Err(1); } else { &matches.free[0] @@ -467,13 +476,7 @@ impl Options { .map(|s| SearchPath::from_cli_opt(s, error_format)) .collect(); let externs = parse_externs(matches, &unstable_opts, error_format); - let extern_html_root_urls = match parse_extern_html_roots(matches) { - Ok(ex) => ex, - Err(err) => { - diag.struct_err(err).emit(); - return Err(1); - } - }; + let extern_html_root_urls = parse_extern_html_roots(matches, &diag).map_err(|_| 1)?; let default_settings: Vec> = vec![ matches @@ -529,7 +532,7 @@ impl Options { let no_run = matches.opt_present("no-run"); if !should_test && no_run { - diag.err("the `--test` flag must be passed to enable `--no-run`"); + diag.emit_err(NoRunFlagWithoutTestFlag); return Err(1); } @@ -537,7 +540,7 @@ impl Options { let output = matches.opt_str("output").map(|s| PathBuf::from(&s)); let output = match (out_dir, output) { (Some(_), Some(_)) => { - diag.struct_err("cannot use both 'out-dir' and 'output' at once").emit(); + diag.emit_err(CannotUseOutDirAndOutputFlags); return Err(1); } (Some(out_dir), None) => out_dir, @@ -552,7 +555,7 @@ impl Options { if let Some(ref p) = extension_css { if !p.is_file() { - diag.struct_err("option --extend-css argument must be a file").emit(); + diag.emit_err(ExtendCssArgNotFile); return Err(1); } } @@ -573,32 +576,19 @@ impl Options { matches.opt_strs("theme").iter().map(|s| (PathBuf::from(&s), s.to_owned())) { if !theme_file.is_file() { - diag.struct_err(&format!("invalid argument: \"{}\"", theme_s)) - .help("arguments to --theme must be files") - .emit(); + diag.emit_err(ThemeArgNotFile { theme_arg: &theme_s }); return Err(1); } if theme_file.extension() != Some(OsStr::new("css")) { - diag.struct_err(&format!("invalid argument: \"{}\"", theme_s)) - .help("arguments to --theme must have a .css extension") - .emit(); + diag.emit_err(ThemeArgNotCssFile { theme_arg: &theme_s }); return Err(1); } let (success, ret) = theme::test_theme_against(&theme_file, &paths, &diag); if !success { - diag.struct_err(&format!("error loading theme file: \"{}\"", theme_s)).emit(); + diag.emit_err(ErrorLoadingThemeFile { theme_arg: &theme_s }); return Err(1); } else if !ret.is_empty() { - diag.struct_warn(&format!( - "theme file \"{}\" is missing CSS rules from the default theme", - theme_s - )) - .warn("the theme may appear incorrect when loaded") - .help(&format!( - "to see what rules are missing, call `rustdoc --check-theme \"{}\"`", - theme_s - )) - .emit(); + diag.emit_warning(ThemeFileMissingDefaultThemeCssRules { theme_arg: &theme_s }); } themes.push(StylePath { path: theme_file }); } @@ -625,7 +615,7 @@ impl Options { match matches.opt_str("r").as_deref() { Some("rust") | None => {} Some(s) => { - diag.struct_err(&format!("unknown input format: {}", s)).emit(); + diag.emit_err(UnknownInputFormat { input_format_arg: s }); return Err(1); } } @@ -633,7 +623,7 @@ impl Options { let index_page = matches.opt_str("index-page").map(|s| PathBuf::from(&s)); if let Some(ref index_page) = index_page { if !index_page.is_file() { - diag.struct_err("option `--index-page` argument must be a file").emit(); + diag.emit_err(IndexPageArgNotFile); return Err(1); } } @@ -644,8 +634,8 @@ impl Options { let crate_types = match parse_crate_types_from_list(matches.opt_strs("crate-type")) { Ok(types) => types, - Err(e) => { - diag.struct_err(&format!("unknown crate type: {}", e)).emit(); + Err(error) => { + diag.emit_err(UnknownCrateType { error }); return Err(1); } }; @@ -654,10 +644,7 @@ impl Options { Some(s) => match OutputFormat::try_from(s.as_str()) { Ok(out_fmt) => { if !out_fmt.is_json() && show_coverage { - diag.struct_err( - "html output format isn't supported for the --show-coverage option", - ) - .emit(); + diag.emit_err(HtmlOutputNotSupportedWithShowCoverageFlag); return Err(1); } out_fmt @@ -707,10 +694,7 @@ impl Options { matches.opt_present("extern-html-root-takes-precedence"); if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) { - diag.struct_err( - "--generate-link-to-definition option can only be used with HTML output format", - ) - .emit(); + diag.emit_err(GenerateLinkToDefinitionFlagNotWithHtmlOutputFormat); return Err(1); } @@ -804,12 +788,7 @@ fn check_deprecated_options(matches: &getopts::Matches, diag: &rustc_errors::Han for &flag in deprecated_flags.iter() { if matches.opt_present(flag) { - diag.struct_warn(&format!("the `{}` flag is deprecated", flag)) - .note( - "see issue #44136 \ - for more information", - ) - .emit(); + diag.emit_warning(FlagDeprecated { flag }); } } @@ -817,19 +796,15 @@ fn check_deprecated_options(matches: &getopts::Matches, diag: &rustc_errors::Han for &flag in removed_flags.iter() { if matches.opt_present(flag) { - let mut err = diag.struct_warn(&format!("the `{}` flag no longer functions", flag)); - err.note( - "see issue #44136 \ - for more information", - ); - - if flag == "no-defaults" || flag == "passes" { - err.help("you may want to use --document-private-items"); + let suggestion = if flag == "no-defaults" || flag == "passes" { + Some(FlagRemovedSuggestion::DocumentPrivateItems) } else if flag == "plugins" || flag == "plugin-path" { - err.warn("see CVE-2018-1000622"); - } + Some(FlagRemovedSuggestion::SeeRustdocPluginsCve) + } else { + None + }; - err.emit(); + diag.emit_warning(FlagRemoved { flag, suggestion }); } } } @@ -839,11 +814,12 @@ fn check_deprecated_options(matches: &getopts::Matches, diag: &rustc_errors::Han /// describing the issue. fn parse_extern_html_roots( matches: &getopts::Matches, -) -> Result, &'static str> { + diag: &rustc_errors::Handler, +) -> Result, ErrorGuaranteed> { let mut externs = BTreeMap::new(); for arg in &matches.opt_strs("extern-html-root-url") { let (name, url) = - arg.split_once('=').ok_or("--extern-html-root-url must be of the form name=url")?; + arg.split_once('=').ok_or_else(|| diag.emit_err(InvalidExternHtmlRootUrl))?; externs.insert(name.to_string(), url.to_string()); } Ok(externs) diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index da0df596c41e3..0de7b220e73a3 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -27,6 +27,9 @@ use std::sync::LazyLock; use crate::clean::inline::build_external_trait; use crate::clean::{self, ItemId}; use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions}; +use crate::errors::{ + CouldNotResolvePath, DeprecatedAttr, DeprecatedAttrKind, MissingCrateLevelDocs, +}; use crate::formats::cache::Cache; use crate::passes::collect_intra_doc_links::PreprocessedMarkdownLink; use crate::passes::{self, Condition::*}; @@ -382,36 +385,25 @@ pub(crate) fn run_global_ctxt( let mut krate = tcx.sess.time("clean_crate", || clean::krate(&mut ctxt)); if krate.module.doc_value().map(|d| d.is_empty()).unwrap_or(true) { - let help = format!( - "The following guide may be of use:\n\ - {}/rustdoc/how-to-write-documentation.html", - crate::DOC_RUST_LANG_ORG_CHANNEL - ); - tcx.struct_lint_node( + tcx.emit_lint( crate::lint::MISSING_CRATE_LEVEL_DOCS, DocContext::as_local_hir_id(tcx, krate.module.item_id).unwrap(), - "no documentation found for this crate's top-level module", - |lint| lint.help(help), + MissingCrateLevelDocs { doc_rust_lang_org_channel: crate::DOC_RUST_LANG_ORG_CHANNEL }, ); } - fn report_deprecated_attr(name: &str, diag: &rustc_errors::Handler, sp: Span) { - let mut msg = - diag.struct_span_warn(sp, &format!("the `#![doc({})]` attribute is deprecated", name)); - msg.note( - "see issue #44136 \ - for more information", - ); - - if name == "no_default_passes" { - msg.help("`#![doc(no_default_passes)]` no longer functions; you may want to use `#![doc(document_private_items)]`"); - } else if name.starts_with("passes") { - msg.help("`#![doc(passes = \"...\")]` no longer functions; you may want to use `#![doc(document_private_items)]`"); - } else if name.starts_with("plugins") { - msg.warn("`#![doc(plugins = \"...\")]` no longer functions; see CVE-2018-1000622 "); - } + fn report_deprecated_attr(attr_name: &str, diag: &rustc_errors::Handler, span: Span) { + let kind = if attr_name == "no_default_passes" { + Some(DeprecatedAttrKind::NoDefaultPasses) + } else if attr_name.starts_with("passes") { + Some(DeprecatedAttrKind::Passes) + } else if attr_name.starts_with("plugins") { + Some(DeprecatedAttrKind::Plugins) + } else { + None + }; - msg.emit(); + diag.emit_warning(DeprecatedAttr { span, attr_name, kind }); } // Process all of the crate attributes, extracting plugin metadata along @@ -495,25 +487,13 @@ impl<'tcx> Visitor<'tcx> for EmitIgnoredResolutionErrors<'tcx> { // We have less context here than in rustc_resolve, // so we can only emit the name and span. // However we can give a hint that rustc_resolve will have more info. - let label = format!( - "could not resolve path `{}`", - path.segments - .iter() - .map(|segment| segment.ident.as_str()) - .intersperse("::") - .collect::() - ); - let mut err = rustc_errors::struct_span_err!( - self.tcx.sess, - path.span, - E0433, - "failed to resolve: {}", - label - ); - err.span_label(path.span, label); - err.note("this error was originally ignored because you are running `rustdoc`"); - err.note("try running again with `rustc` or `cargo check` and you may get a more detailed error"); - err.emit(); + let path_display = path + .segments + .iter() + .map(|segment| segment.ident.as_str()) + .intersperse("::") + .collect::(); + self.tcx.sess.emit_err(CouldNotResolvePath { span: path.span, path: path_display }); } // We could have an outer resolution that succeeded, // but with generic parameters that failed. diff --git a/src/librustdoc/errors.rs b/src/librustdoc/errors.rs new file mode 100644 index 0000000000000..c19239c48b965 --- /dev/null +++ b/src/librustdoc/errors.rs @@ -0,0 +1,326 @@ +use rustc_errors::{DecorateLint, DiagnosticMessage}; +use rustc_macros::{Diagnostic, LintDiagnostic, Subdiagnostic}; +use rustc_span::Span; + +use std::io; +use std::path::Path; + +#[derive(Diagnostic)] +#[diag(rustdoc_compilation_failed)] +pub struct CompilationFailed; + +#[derive(LintDiagnostic)] +#[help] +#[diag(rustdoc_missing_crate_level_docs)] +pub struct MissingCrateLevelDocs { + pub doc_rust_lang_org_channel: &'static str, +} + +#[derive(Diagnostic)] +#[note] +#[diag(rustdoc_deprecated_attr)] +pub struct DeprecatedAttr<'a> { + #[primary_span] + pub span: Span, + pub attr_name: &'a str, + #[subdiagnostic] + pub kind: Option, +} + +#[derive(Subdiagnostic)] +pub enum DeprecatedAttrKind { + #[help(rustdoc_deprecated_attr_no_default_passes)] + NoDefaultPasses, + #[help(rustdoc_deprecated_attr_passes)] + Passes, + #[warning(rustdoc_deprecated_attr_plugins)] + Plugins, +} + +#[derive(Diagnostic)] +#[note] +#[help] +#[diag(rustdoc_could_not_resolve_path, code = "E0433")] +pub struct CouldNotResolvePath { + #[primary_span] + #[label] + pub span: Span, + pub path: String, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_unrecognized_emission_type)] +pub struct UnrecognizedEmissionType<'a> { + pub kind: &'a str, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_invalid_extern_html_root_url)] +pub struct InvalidExternHtmlRootUrl; + +#[derive(Diagnostic)] +#[diag(rustdoc_missing_file_operand)] +pub struct MissingFileOperand; + +#[derive(Diagnostic)] +#[diag(rustdoc_too_many_file_operands)] +pub struct TooManyFileOperands; + +#[derive(Diagnostic)] +#[diag(rustdoc_no_run_flag_without_test_flag)] +pub struct NoRunFlagWithoutTestFlag; + +#[derive(Diagnostic)] +#[diag(rustdoc_cannot_use_out_dir_and_output_flags)] +pub struct CannotUseOutDirAndOutputFlags; + +#[derive(Diagnostic)] +#[diag(rustdoc_option_extend_css_arg_not_file)] +pub struct ExtendCssArgNotFile; + +#[derive(Diagnostic)] +#[help] +#[diag(rustdoc_theme_arg_not_file)] +pub struct ThemeArgNotFile<'a> { + pub theme_arg: &'a str, +} + +#[derive(Diagnostic)] +#[help] +#[diag(rustdoc_theme_arg_not_css_file)] +pub struct ThemeArgNotCssFile<'a> { + pub theme_arg: &'a str, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_error_loading_theme_file)] +pub struct ErrorLoadingThemeFile<'a> { + pub theme_arg: &'a str, +} + +#[derive(Diagnostic)] +#[help] +#[diag(rustdoc_theme_file_missing_default_theme_css_rules)] +pub struct ThemeFileMissingDefaultThemeCssRules<'a> { + pub theme_arg: &'a str, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_unknown_input_format)] +pub struct UnknownInputFormat<'a> { + pub input_format_arg: &'a str, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_index_page_arg_not_file)] +pub struct IndexPageArgNotFile; + +#[derive(Diagnostic)] +#[diag(rustdoc_unknown_crate_type)] +pub struct UnknownCrateType { + pub error: String, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_html_output_not_supported_with_show_coverage_flag)] +pub struct HtmlOutputNotSupportedWithShowCoverageFlag; + +#[derive(Diagnostic)] +#[diag(rustdoc_generate_link_to_definition_flag_not_with_html_output_format)] +pub struct GenerateLinkToDefinitionFlagNotWithHtmlOutputFormat; + +#[derive(Diagnostic)] +#[diag(rustdoc_scrape_examples_output_path_and_target_crate_not_used_together)] +pub struct ScrapeExamplesOutputPathAndTargetCrateNotTogether; + +#[derive(Diagnostic)] +#[diag(rustdoc_scrape_tests_not_with_scrape_examples_output_path_and_target_crate)] +pub struct ScrapeTestsNotWithScrapeExamplesOutputPathAndTargetCrate; + +#[derive(Diagnostic)] +#[note] +#[diag(rustdoc_flag_deprecated)] +pub struct FlagDeprecated<'a> { + pub flag: &'a str, +} + +#[derive(Diagnostic)] +#[note] +#[diag(rustdoc_flag_removed)] +pub struct FlagRemoved<'a> { + pub flag: &'a str, + #[subdiagnostic] + pub suggestion: Option, +} + +#[derive(Subdiagnostic)] +pub enum FlagRemovedSuggestion { + #[help(rustdoc_use_document_private_items_flag)] + DocumentPrivateItems, + #[warning(rustdoc_see_rustdoc_plugins_cve)] + SeeRustdocPluginsCve, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_error_reading_file)] +pub struct ErrorReadingFile<'a> { + pub file_path: &'a Path, + pub error: io::Error, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_error_reading_file_not_utf8)] +pub struct ErrorReadingFileNotUtf8<'a> { + pub file_path: &'a Path, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_error_loading_examples)] +pub struct ErrorLoadingExamples { + pub error: io::Error, + pub path: String, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_anonymous_imports_cannot_be_inlined, code = "E0780")] +pub struct AnonymousImportsCannotBeInlined { + #[primary_span] + pub inline_span: Span, + #[label(import_span)] + pub import_span: Span, +} + +pub struct InvalidCodeblockAttribute<'a> { + pub attr_name: &'a str, + pub suggested_attr_kind: CodeblockAttributeKind, +} + +impl<'a> DecorateLint<'a, ()> for InvalidCodeblockAttribute<'_> { + fn decorate_lint<'b>( + self, + diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>, + ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> { + use CodeblockAttributeKind::*; + + diag.set_arg("attr_name", self.attr_name); + let suggested_attr_name = match self.suggested_attr_kind { + CompileFail => "compile_fail", + ShouldPanic => "should_panic", + NoRun => "no_run", + TestHarness => "test_harness", + }; + diag.set_arg("suggested_attr_name", suggested_attr_name); + diag.subdiagnostic(self.suggested_attr_kind); + diag + } + + fn msg(&self) -> DiagnosticMessage { + rustc_errors::fluent::rustdoc_invalid_codeblock_attribute + } +} + +#[derive(Subdiagnostic)] +pub enum CodeblockAttributeKind { + #[help(compile_fail)] + CompileFail, + #[help(should_panic)] + ShouldPanic, + #[help(no_run)] + NoRun, + #[help(test_harness)] + TestHarness, +} + +#[derive(Diagnostic)] +#[diag(rustdoc_failed_to_read_file)] +pub struct FailedToReadFile<'a> { + #[primary_span] + pub span: Span, + pub path: &'a Path, + pub error: io::Error, +} + +#[derive(LintDiagnostic)] +#[note] +#[diag(rustdoc_bare_url_not_hyperlink)] +pub struct BareUrlNotHyperlink<'a> { + #[suggestion(code = "<{url}>", applicability = "machine-applicable")] + pub span: Span, + pub url: &'a str, +} + +#[derive(LintDiagnostic)] +#[diag(rustdoc_missing_doc_code_examples)] +pub struct MissingDocCodeExamples; + +#[derive(LintDiagnostic)] +#[diag(rustdoc_private_doc_tests)] +pub struct PrivateDocTests; + +#[derive(Diagnostic)] +pub enum InvalidCfgError { + #[diag(rustdoc_cfg_unexpected_literal)] + UnexpectedLiteral { + #[primary_span] + span: Span, + }, + #[diag(rustdoc_cfg_expected_single_identifier)] + ExpectedSingleIdentifier { + #[primary_span] + span: Span, + }, + #[diag(rustdoc_cfg_option_value_not_string_literal)] + OptionValueNotStringLiteral { + #[primary_span] + span: Span, + }, + #[diag(rustdoc_cfg_expected_one_cfg_pattern)] + ExpectedOneCfgPattern { + #[primary_span] + span: Span, + }, + #[diag(rustdoc_cfg_invalid_predicate)] + InvalidPredicate { + #[primary_span] + span: Span, + }, +} + +#[derive(LintDiagnostic)] +#[diag(rustdoc_unclosed_html_tag)] +pub struct UnclosedHtmlTag<'a> { + pub tag: &'a str, + #[subdiagnostic] + pub mark_source_code: Option, +} + +#[derive(Subdiagnostic)] +#[multipart_suggestion(rustdoc_mark_source_code, applicability = "maybe-incorrect")] +pub struct MarkSourceCode { + #[suggestion_part(code = "`")] + pub left: Span, + #[suggestion_part(code = "`")] + pub right: Span, +} + +#[derive(LintDiagnostic)] +#[diag(rustdoc_unclosed_html_comment)] +pub struct UnclosedHtmlComment; + +#[derive(LintDiagnostic)] +#[diag(rustdoc_unopened_html_tag)] +pub struct UnopenedHtmlTag { + pub tag_name: String, +} + +#[derive(LintDiagnostic)] +#[diag(rustdoc_unclosed_quoted_html_attribute)] +pub struct UnclosedQuotedHtmlAttribute<'a> { + pub tag_name: &'a str, +} + +#[derive(LintDiagnostic)] +#[diag(rustdoc_invalid_self_closing_html_tag)] +pub struct InvalidSelfClosingHtmlTag<'a> { + pub tag_name: &'a str, +} diff --git a/src/librustdoc/externalfiles.rs b/src/librustdoc/externalfiles.rs index 37fd909c93342..5d8d4a7ac6e47 100644 --- a/src/librustdoc/externalfiles.rs +++ b/src/librustdoc/externalfiles.rs @@ -1,3 +1,4 @@ +use crate::errors::{ErrorReadingFile, ErrorReadingFileNotUtf8}; use crate::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, Playground}; use crate::rustc_span::edition::Edition; use std::fs; @@ -82,15 +83,15 @@ pub(crate) fn load_string>( let file_path = file_path.as_ref(); let contents = match fs::read(file_path) { Ok(bytes) => bytes, - Err(e) => { - diag.struct_err(&format!("error reading `{}`: {}", file_path.display(), e)).emit(); + Err(error) => { + diag.emit_err(ErrorReadingFile { file_path, error }); return Err(LoadStringError::ReadFail); } }; match str::from_utf8(&contents) { Ok(s) => Ok(s.to_string()), Err(_) => { - diag.struct_err(&format!("error reading `{}`: not UTF-8", file_path.display())).emit(); + diag.emit_err(ErrorReadingFileNotUtf8 { file_path }); Err(LoadStringError::BadUtf8) } } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 4ff67fe1551dd..b3b1800e84f5a 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -43,6 +43,7 @@ use std::str; use crate::clean::RenderedLink; use crate::doctest; +use crate::errors::{CodeblockAttributeKind, InvalidCodeblockAttribute}; use crate::html::escape::Escape; use crate::html::format::Buffer; use crate::html::highlight; @@ -803,7 +804,7 @@ impl<'tcx> ExtraInfo<'tcx> { ExtraInfo { id: ExtraInfoId::Def(did), sp, tcx } } - fn error_invalid_codeblock_attr(&self, msg: &str, help: &str) { + fn error_invalid_codeblock_attr(&self, name: &str, suggested_kind: CodeblockAttributeKind) { let hir_id = match self.id { ExtraInfoId::Hir(hir_id) => hir_id, ExtraInfoId::Def(item_did) => { @@ -816,12 +817,11 @@ impl<'tcx> ExtraInfo<'tcx> { } } }; - self.tcx.struct_span_lint_hir( + self.tcx.emit_spanned_lint( crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, hir_id, self.sp, - msg, - |lint| lint.help(help), + InvalidCodeblockAttribute { attr_name: name, suggested_attr_kind: suggested_kind }, ); } } @@ -956,41 +956,21 @@ impl LangString { } x if extra.is_some() => { let s = x.to_lowercase(); - if let Some((flag, help)) = if s == "compile-fail" - || s == "compile_fail" - || s == "compilefail" + if let Some(kind) = + if s == "compile-fail" || s == "compile_fail" || s == "compilefail" { + Some(CodeblockAttributeKind::CompileFail) + } else if s == "should-panic" || s == "should_panic" || s == "shouldpanic" { + Some(CodeblockAttributeKind::ShouldPanic) + } else if s == "no-run" || s == "no_run" || s == "norun" { + Some(CodeblockAttributeKind::NoRun) + } else if s == "test-harness" || s == "test_harness" || s == "testharness" { + Some(CodeblockAttributeKind::TestHarness) + } else { + None + } { - 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 == "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 - } { if let Some(extra) = extra { - extra.error_invalid_codeblock_attr( - &format!("unknown attribute `{}`. Did you mean `{}`?", x, flag), - help, - ); + extra.error_invalid_codeblock_attr(x, kind); } } seen_other_tags = true; diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index f95d8e4303594..c697ee359334e 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -64,6 +64,7 @@ use serde::{Serialize, Serializer}; use crate::clean::{self, ItemId, RenderedLink, SelfTy}; use crate::error::Error; +use crate::errors::FailedToReadFile; use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::formats::{AssocItemRender, Impl, RenderMode}; @@ -2854,10 +2855,9 @@ fn render_call_locations(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Ite let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| -> bool { let contents = match fs::read_to_string(&path) { Ok(contents) => contents, - Err(err) => { + Err(error) => { let span = item.span(tcx).map_or(rustc_span::DUMMY_SP, |span| span.inner()); - tcx.sess - .span_err(span, &format!("failed to read file {}: {}", path.display(), err)); + tcx.sess.emit_err(FailedToReadFile { span, path, error }); return false; } }; diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 86454e1f2eb73..2b20459e140d0 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -82,6 +82,7 @@ use rustc_session::getopts; use rustc_session::{early_error, early_warn}; use crate::clean::utils::DOC_RUST_LANG_ORG_CHANNEL; +use crate::errors::CompilationFailed; use crate::passes::collect_intra_doc_links; /// A macro to create a FxHashMap. @@ -108,6 +109,7 @@ mod core; mod docfs; mod doctest; mod error; +mod errors; mod externalfiles; mod fold; mod formats; @@ -812,7 +814,7 @@ fn main_args(at_args: &[String]) -> MainResult { }; if sess.diagnostic().has_errors_or_lint_errors().is_some() { - sess.fatal("Compilation failed, aborting rustdoc"); + sess.emit_fatal(CompilationFailed); } let global_ctxt = abort_on_err(queries.global_ctxt(), sess); diff --git a/src/librustdoc/passes/check_doc_test_visibility.rs b/src/librustdoc/passes/check_doc_test_visibility.rs index 6aa2dda980cf3..54d97e1f81385 100644 --- a/src/librustdoc/passes/check_doc_test_visibility.rs +++ b/src/librustdoc/passes/check_doc_test_visibility.rs @@ -9,6 +9,7 @@ use super::Pass; use crate::clean; use crate::clean::*; use crate::core::DocContext; +use crate::errors::{MissingDocCodeExamples, PrivateDocTests}; use crate::html::markdown::{find_testable_code, ErrorCodes, Ignore, LangString}; use crate::visit::DocVisitor; use crate::visit_ast::inherits_doc_hidden; @@ -120,24 +121,22 @@ pub(crate) fn look_for_tests<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item if tests.found_tests == 0 && cx.tcx.features().rustdoc_missing_doc_code_examples { if should_have_doc_example(cx, item) { debug!("reporting error for {:?} (hir_id={:?})", item, hir_id); - let sp = item.attr_span(cx.tcx); - cx.tcx.struct_span_lint_hir( + let span = item.attr_span(cx.tcx); + cx.tcx.emit_spanned_lint( crate::lint::MISSING_DOC_CODE_EXAMPLES, hir_id, - sp, - "missing code example in this documentation", - |lint| lint, + span, + MissingDocCodeExamples, ); } } else if tests.found_tests > 0 && !cx.cache.effective_visibilities.is_exported(cx.tcx, item.item_id.expect_def_id()) { - cx.tcx.struct_span_lint_hir( + cx.tcx.emit_spanned_lint( crate::lint::PRIVATE_DOC_TESTS, hir_id, item.attr_span(cx.tcx), - "documentation test in private item", - |lint| lint, + PrivateDocTests, ); } } diff --git a/src/librustdoc/passes/lint/bare_urls.rs b/src/librustdoc/passes/lint/bare_urls.rs index 423230cfe381e..c2578bcb0068a 100644 --- a/src/librustdoc/passes/lint/bare_urls.rs +++ b/src/librustdoc/passes/lint/bare_urls.rs @@ -3,12 +3,12 @@ use crate::clean::*; use crate::core::DocContext; +use crate::errors::BareUrlNotHyperlink; use crate::html::markdown::main_body_opts; use crate::passes::source_span_for_markdown_range; use core::ops::Range; use pulldown_cmark::{Event, Parser, Tag}; use regex::Regex; -use rustc_errors::Applicability; use std::mem; use std::sync::LazyLock; @@ -20,25 +20,31 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &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 = source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs) - .unwrap_or_else(|| item.attr_span(cx.tcx)); - cx.tcx.struct_span_lint_hir(crate::lint::BARE_URLS, hir_id, sp, msg, |lint| { - lint.note("bare URLs are not automatically turned into clickable links") - .span_suggestion( - sp, - "use an automatic link instead", - format!("<{}>", url), - Applicability::MachineApplicable, - ) - }); - }; - let mut p = Parser::new_ext(&dox, main_body_opts()).into_offset_iter(); while let Some((event, range)) = p.next() { match event { - Event::Text(s) => find_raw_urls(cx, &s, range, &report_diag), + Event::Text(text) => { + trace!("looking for raw urls in {}", text); + // For now, we only check "full" URLs (meaning, starting with "http://" or "https://"). + for match_ in URL_REGEX.find_iter(&text) { + let url = match_.as_str(); + let url_range = match_.range(); + let range = Range { + start: range.start + url_range.start, + end: range.start + url_range.end, + }; + let span = + source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs) + .unwrap_or_else(|| item.attr_span(cx.tcx)); + cx.tcx.emit_spanned_lint( + crate::lint::BARE_URLS, + hir_id, + span, + BareUrlNotHyperlink { span, url }, + ); + } + } // We don't want to check the text inside code blocks or links. Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link(..))) => { while let Some((event, _)) = p.next() { @@ -67,23 +73,3 @@ static URL_REGEX: LazyLock = LazyLock::new(|| { )) .expect("failed to build regex") }); - -fn find_raw_urls( - cx: &DocContext<'_>, - text: &str, - range: Range, - f: &impl Fn(&DocContext<'_>, &str, &str, Range), -) { - trace!("looking for raw urls in {}", text); - // For now, we only check "full" URLs (meaning, starting with "http://" or "https://"). - for match_ in URL_REGEX.find_iter(text) { - let url = match_.as_str(); - let url_range = match_.range(); - f( - cx, - "this URL is not a hyperlink", - url, - Range { start: range.start + url_range.start, end: range.start + url_range.end }, - ); - } -} diff --git a/src/librustdoc/passes/lint/html_tags.rs b/src/librustdoc/passes/lint/html_tags.rs index 070c0aab5868b..a211ac4391342 100644 --- a/src/librustdoc/passes/lint/html_tags.rs +++ b/src/librustdoc/passes/lint/html_tags.rs @@ -1,199 +1,405 @@ //! Detects invalid HTML (like an unclosed ``) in doc comments. use crate::clean::*; use crate::core::DocContext; +use crate::errors::{ + InvalidSelfClosingHtmlTag, MarkSourceCode, UnclosedHtmlComment, UnclosedHtmlTag, + UnclosedQuotedHtmlAttribute, UnopenedHtmlTag, +}; use crate::html::markdown::main_body_opts; use crate::passes::source_span_for_markdown_range; use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag}; +use rustc_errors::DecorateLint; +use rustc_hir::HirId; use std::iter::Peekable; use std::ops::Range; use std::str::CharIndices; -pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) { - let tcx = cx.tcx; - let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) - // If non-local, no need to check anything. - else { return }; - let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); - if !dox.is_empty() { - let report_diag = |msg: &str, range: &Range, is_open_tag: bool| { - let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs) { - Some(sp) => sp, - None => item.attr_span(tcx), - }; - tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| { - use rustc_lint_defs::Applicability; - // If a tag looks like ``, it might actually be a generic. - // We don't try to detect stuff `` because that's not valid HTML, - // and we don't try to detect stuff `` because that's not valid Rust. - let mut generics_end = range.end; - if let Some(Some(mut generics_start)) = (is_open_tag - && dox[..generics_end].ends_with('>')) - .then(|| extract_path_backwards(&dox, range.start)) +pub(crate) fn visit_item(cx: &mut DocContext<'_>, item: &Item) { + HtmlTagsVisitor { cx }.visit_item(item) +} + +const ALLOWED_UNCLOSED: &[&str] = &[ + "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", + "source", "track", "wbr", +]; + +struct ItemInfo<'a> { + item: &'a Item, + hir_id: HirId, + dox: &'a str, +} + +struct HtmlTagsVisitor<'a, 'tcx> { + cx: &'a mut DocContext<'tcx>, +} + +impl<'a, 'tcx> HtmlTagsVisitor<'a, 'tcx> { + fn visit_item(&self, item: &Item) { + let tcx = self.cx.tcx; + // If non-local, no need to check anything. + let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else { return }; + let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); + if !dox.is_empty() { + let mut tags = Vec::new(); + let mut is_in_comment = None; + let mut in_code_block = false; + + let link_names = item.link_names(&self.cx.cache); + + let mut replacer = |broken_link: BrokenLink<'_>| { + if let Some(link) = + link_names.iter().find(|link| *link.original_text == *broken_link.reference) { - while generics_start != 0 - && generics_end < dox.len() - && dox.as_bytes()[generics_start - 1] == b'<' - && dox.as_bytes()[generics_end] == b'>' - { - generics_end += 1; - generics_start -= 1; - if let Some(new_start) = extract_path_backwards(&dox, generics_start) { - generics_start = new_start; - } - if let Some(new_end) = extract_path_forward(&dox, generics_end) { - generics_end = new_end; - } - } - if let Some(new_end) = extract_path_forward(&dox, generics_end) { - generics_end = new_end; - } - let generics_sp = match source_span_for_markdown_range( - tcx, - &dox, - &(generics_start..generics_end), - &item.attrs, - ) { - Some(sp) => sp, - None => item.attr_span(tcx), - }; - // Sometimes, we only extract part of a path. For example, consider this: - // - // <[u32] as IntoIter>::Item - // ^^^^^ unclosed HTML tag `u32` - // - // We don't have any code for parsing fully-qualified trait paths. - // In theory, we could add it, but doing it correctly would require - // parsing the entire path grammar, which is problematic because of - // overlap between the path grammar and Markdown. - // - // The example above shows that ambiguity. Is `[u32]` intended to be an - // intra-doc link to the u32 primitive, or is it intended to be a slice? - // - // If the below conditional were removed, we would suggest this, which is - // not what the user probably wants. - // - // <[u32] as `IntoIter`>::Item - // - // We know that the user actually wants to wrap the whole thing in a code - // block, but the only reason we know that is because `u32` does not, in - // fact, implement IntoIter. If the example looks like this: - // - // <[Vec] as IntoIter::Item - // - // The ideal fix would be significantly different. - if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<') - || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>') - { - return lint; - } - // multipart form is chosen here because ``Vec`` would be confusing. - lint.multipart_suggestion( - "try marking as source code", - vec![ - (generics_sp.shrink_to_lo(), String::from("`")), - (generics_sp.shrink_to_hi(), String::from("`")), - ], - Applicability::MaybeIncorrect, - ); + Some((link.href.as_str().into(), link.new_text.as_str().into())) + } else if matches!( + &broken_link.link_type, + LinkType::Reference | LinkType::ReferenceUnknown + ) { + // If the link is shaped [like][this], suppress any broken HTML in the [this] part. + // The `broken_intra_doc_links` will report typos in there anyway. + Some(( + broken_link.reference.to_string().into(), + broken_link.reference.to_string().into(), + )) + } else { + None } + }; - lint - }); - }; + let p = + Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer)) + .into_offset_iter(); + + let item_info = ItemInfo { item, hir_id, dox: &dox }; - let mut tags = Vec::new(); - let mut is_in_comment = None; - let mut in_code_block = false; + for (event, range) in p { + match event { + Event::Start(Tag::CodeBlock(_)) => in_code_block = true, + Event::Html(text) if !in_code_block => { + self.extract_tags(&mut tags, &text, range, &mut is_in_comment, &item_info) + } + Event::End(Tag::CodeBlock(_)) => in_code_block = false, + _ => {} + } + } - let link_names = item.link_names(&cx.cache); + for (tag, range) in tags.iter().filter(|(t, _)| { + let t = t.to_lowercase(); + !ALLOWED_UNCLOSED.contains(&t.as_str()) + }) { + self.report_diag( + UnclosedHtmlTag { + tag, + mark_source_code: self.mark_source_code(range, &item_info), + }, + range, + &item_info, + ); + } - let mut replacer = |broken_link: BrokenLink<'_>| { - if let Some(link) = - link_names.iter().find(|link| *link.original_text == *broken_link.reference) - { - Some((link.href.as_str().into(), link.new_text.as_str().into())) - } else if matches!( - &broken_link.link_type, - LinkType::Reference | LinkType::ReferenceUnknown - ) { - // If the link is shaped [like][this], suppress any broken HTML in the [this] part. - // The `broken_intra_doc_links` will report typos in there anyway. - Some(( - broken_link.reference.to_string().into(), - broken_link.reference.to_string().into(), - )) - } else { - None + if let Some(range) = is_in_comment { + self.report_diag(UnclosedHtmlComment, &range, &item_info); } + } + } + + fn report_diag( + &self, + diag: impl for<'b> DecorateLint<'b, ()>, + range: &Range, + item_info: &ItemInfo<'_>, + ) { + let sp = match source_span_for_markdown_range( + self.cx.tcx, + item_info.dox, + range, + &item_info.item.attrs, + ) { + Some(sp) => sp, + None => item_info.item.attr_span(self.cx.tcx), }; + self.cx.tcx.emit_spanned_lint(crate::lint::INVALID_HTML_TAGS, item_info.hir_id, sp, diag); + } - let p = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer)) - .into_offset_iter(); + fn mark_source_code( + &self, + range: &Range, + item_info: &ItemInfo<'_>, + ) -> Option { + let ItemInfo { item, hir_id: _, dox } = item_info; - for (event, range) in p { - match event { - Event::Start(Tag::CodeBlock(_)) => in_code_block = true, - Event::Html(text) if !in_code_block => { - extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag) + // If a tag looks like ``, it might actually be a generic. + // We don't try to detect stuff `` because that's not valid HTML, + // and we don't try to detect stuff `` because that's not valid Rust. + let mut generics_end = range.end; + if let Some(Some(mut generics_start)) = + (dox[..generics_end].ends_with('>')).then(|| extract_path_backwards(&dox, range.start)) + { + while generics_start != 0 + && generics_end < dox.len() + && dox.as_bytes()[generics_start - 1] == b'<' + && dox.as_bytes()[generics_end] == b'>' + { + generics_end += 1; + generics_start -= 1; + if let Some(new_start) = extract_path_backwards(&dox, generics_start) { + generics_start = new_start; + } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; } - Event::End(Tag::CodeBlock(_)) => in_code_block = false, - _ => {} } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; + } + let generics_sp = match source_span_for_markdown_range( + self.cx.tcx, + &dox, + &(generics_start..generics_end), + &item.attrs, + ) { + Some(sp) => sp, + None => item.attr_span(self.cx.tcx), + }; + // Sometimes, we only extract part of a path. For example, consider this: + // + // <[u32] as IntoIter>::Item + // ^^^^^ unclosed HTML tag `u32` + // + // We don't have any code for parsing fully-qualified trait paths. + // In theory, we could add it, but doing it correctly would require + // parsing the entire path grammar, which is problematic because of + // overlap between the path grammar and Markdown. + // + // The example above shows that ambiguity. Is `[u32]` intended to be an + // intra-doc link to the u32 primitive, or is it intended to be a slice? + // + // If the below conditional were removed, we would suggest this, which is + // not what the user probably wants. + // + // <[u32] as `IntoIter`>::Item + // + // We know that the user actually wants to wrap the whole thing in a code + // block, but the only reason we know that is because `u32` does not, in + // fact, implement IntoIter. If the example looks like this: + // + // <[Vec] as IntoIter::Item + // + // The ideal fix would be significantly different. + if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<') + || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>') + { + return None; + } + // multipart form is chosen here because ``Vec`` would be confusing. + Some(MarkSourceCode { + left: generics_sp.shrink_to_lo(), + right: generics_sp.shrink_to_hi(), + }) + } else { + None } + } - for (tag, range) in tags.iter().filter(|(t, _)| { - let t = t.to_lowercase(); - !ALLOWED_UNCLOSED.contains(&t.as_str()) - }) { - report_diag(&format!("unclosed HTML tag `{}`", tag), range, true); + fn drop_tag( + &self, + tags: &mut Vec<(String, Range)>, + tag_name: String, + range: Range, + item_info: &ItemInfo<'_>, + ) { + let tag_name_low = tag_name.to_lowercase(); + if let Some(pos) = tags.iter().rposition(|(t, _)| t.to_lowercase() == tag_name_low) { + // If the tag is nested inside a "` (the `h2` tag isn't required + // but it helps for the visualization). + self.report_diag(UnopenedHtmlTag { tag_name }, &range, item_info); } + } - if let Some(range) = is_in_comment { - report_diag("Unclosed HTML comment", &range, false); + fn extract_html_tag( + &self, + tags: &mut Vec<(String, Range)>, + text: &str, + range: &Range, + start_pos: usize, + iter: &mut Peekable>, + item_info: &ItemInfo<'_>, + ) { + let mut tag_name = String::new(); + let mut is_closing = false; + let mut prev_pos = start_pos; + + loop { + let (pos, c) = match iter.peek() { + Some((pos, c)) => (*pos, *c), + // In case we reached the of the doc comment, we want to check that it's an + // unclosed HTML tag. For example "/// (prev_pos, '\0'), + }; + prev_pos = pos; + // Checking if this is a closing tag (like `` for ``). + if c == '/' && tag_name.is_empty() { + is_closing = true; + } else if is_valid_for_html_tag_name(c, tag_name.is_empty()) { + tag_name.push(c); + } else { + if !tag_name.is_empty() { + let mut r = Range { start: range.start + start_pos, end: range.start + pos }; + if c == '>' { + // In case we have a tag without attribute, we can consider the span to + // refer to it fully. + r.end += 1; + } + if is_closing { + // In case we have "" or even "". + if c != '>' { + if !c.is_whitespace() { + // It seems like it's not a valid HTML tag. + break; + } + let mut found = false; + for (new_pos, c) in text[pos..].char_indices() { + if !c.is_whitespace() { + if c == '>' { + r.end = range.start + new_pos + 1; + found = true; + } + break; + } + } + if !found { + break; + } + } + self.drop_tag(tags, tag_name, r, item_info); + } else { + let mut is_self_closing = false; + let mut quote_pos = None; + if c != '>' { + let mut quote = None; + let mut after_eq = false; + for (i, c) in text[pos..].char_indices() { + if !c.is_whitespace() { + if let Some(q) = quote { + if c == q { + quote = None; + quote_pos = None; + after_eq = false; + } + } else if c == '>' { + break; + } else if c == '/' && !after_eq { + is_self_closing = true; + } else { + if is_self_closing { + is_self_closing = false; + } + if (c == '"' || c == '\'') && after_eq { + quote = Some(c); + quote_pos = Some(pos + i); + } else if c == '=' { + after_eq = true; + } + } + } else if quote.is_none() { + after_eq = false; + } + } + } + if let Some(quote_pos) = quote_pos { + let qr = Range { start: quote_pos, end: quote_pos }; + self.report_diag( + UnclosedQuotedHtmlAttribute { tag_name: &tag_name }, + &qr, + item_info, + ); + } + if is_self_closing { + // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus + let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..]) + || tags.iter().take(pos + 1).any(|(at, _)| { + let at = at.to_lowercase(); + at == "svg" || at == "math" + }); + if !valid { + self.report_diag( + InvalidSelfClosingHtmlTag { tag_name: &tag_name }, + &r, + item_info, + ); + } + } else { + tags.push((tag_name, r)); + } + } + } + break; + } + iter.next(); } } -} -const ALLOWED_UNCLOSED: &[&str] = &[ - "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", - "source", "track", "wbr", -]; + fn extract_tags( + &self, + tags: &mut Vec<(String, Range)>, + text: &str, + range: Range, + is_in_comment: &mut Option>, + item_info: &ItemInfo<'_>, + ) { + let mut iter = text.char_indices().peekable(); -fn drop_tag( - tags: &mut Vec<(String, Range)>, - tag_name: String, - range: Range, - f: &impl Fn(&str, &Range, bool), -) { - let tag_name_low = tag_name.to_lowercase(); - if let Some(pos) = tags.iter().rposition(|(t, _)| t.to_lowercase() == tag_name_low) { - // If the tag is nested inside a "` (the `h2` tag isn't required - // but it helps for the visualization). - f(&format!("unopened HTML tag `{}`", tag_name), &range, false); } } @@ -256,152 +462,3 @@ fn is_valid_for_html_tag_name(c: char, is_empty: bool) -> bool { // > hyphens (-). c.is_ascii_alphabetic() || !is_empty && (c == '-' || c.is_ascii_digit()) } - -fn extract_html_tag( - tags: &mut Vec<(String, Range)>, - text: &str, - range: &Range, - start_pos: usize, - iter: &mut Peekable>, - f: &impl Fn(&str, &Range, bool), -) { - let mut tag_name = String::new(); - let mut is_closing = false; - let mut prev_pos = start_pos; - - loop { - let (pos, c) = match iter.peek() { - Some((pos, c)) => (*pos, *c), - // In case we reached the of the doc comment, we want to check that it's an - // unclosed HTML tag. For example "/// (prev_pos, '\0'), - }; - prev_pos = pos; - // Checking if this is a closing tag (like `` for ``). - if c == '/' && tag_name.is_empty() { - is_closing = true; - } else if is_valid_for_html_tag_name(c, tag_name.is_empty()) { - tag_name.push(c); - } else { - if !tag_name.is_empty() { - let mut r = Range { start: range.start + start_pos, end: range.start + pos }; - if c == '>' { - // In case we have a tag without attribute, we can consider the span to - // refer to it fully. - r.end += 1; - } - if is_closing { - // In case we have "" or even "". - if c != '>' { - if !c.is_whitespace() { - // It seems like it's not a valid HTML tag. - break; - } - let mut found = false; - for (new_pos, c) in text[pos..].char_indices() { - if !c.is_whitespace() { - if c == '>' { - r.end = range.start + new_pos + 1; - found = true; - } - break; - } - } - if !found { - break; - } - } - drop_tag(tags, tag_name, r, f); - } else { - let mut is_self_closing = false; - let mut quote_pos = None; - if c != '>' { - let mut quote = None; - let mut after_eq = false; - for (i, c) in text[pos..].char_indices() { - if !c.is_whitespace() { - if let Some(q) = quote { - if c == q { - quote = None; - quote_pos = None; - after_eq = false; - } - } else if c == '>' { - break; - } else if c == '/' && !after_eq { - is_self_closing = true; - } else { - if is_self_closing { - is_self_closing = false; - } - if (c == '"' || c == '\'') && after_eq { - quote = Some(c); - quote_pos = Some(pos + i); - } else if c == '=' { - after_eq = true; - } - } - } else if quote.is_none() { - after_eq = false; - } - } - } - if let Some(quote_pos) = quote_pos { - let qr = Range { start: quote_pos, end: quote_pos }; - f( - &format!("unclosed quoted HTML attribute on tag `{}`", tag_name), - &qr, - false, - ); - } - if is_self_closing { - // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus - let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..]) - || tags.iter().take(pos + 1).any(|(at, _)| { - let at = at.to_lowercase(); - at == "svg" || at == "math" - }); - if !valid { - f(&format!("invalid self-closing HTML tag `{}`", tag_name), &r, false); - } - } else { - tags.push((tag_name, r)); - } - } - } - break; - } - iter.next(); - } -} - -fn extract_tags( - tags: &mut Vec<(String, Range)>, - text: &str, - range: Range, - is_in_comment: &mut Option>, - f: &impl Fn(&str, &Range, bool), -) { - let mut iter = text.char_indices().peekable(); - - while let Some((start_pos, c)) = iter.next() { - if is_in_comment.is_some() { - if text[start_pos..].starts_with("-->") { - *is_in_comment = None; - } - } else if c == '<' { - if text[start_pos..].starts_with(" $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 | @@ -51,30 +27,6 @@ error: unknown attribute `should-panic`. Did you mean `should_panic`? | = 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 | @@ -87,30 +39,6 @@ error: unknown attribute `no-run`. Did you mean `no_run`? | = 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 `test-harness`. Did you mean `test_harness`? --> $DIR/check-attr-test.rs:26:1 | @@ -123,29 +51,5 @@ error: unknown attribute `test-harness`. Did you mean `test_harness`? | = 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:26:1 - | -26 | / /// b -27 | | /// -28 | | /// ```test-harness,testharness,tesT_harness -29 | | /// boo -30 | | /// ``` - | |_______^ - | - = 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:26:1 - | -26 | / /// b -27 | | /// -28 | | /// ```test-harness,testharness,tesT_harness -29 | | /// boo -30 | | /// ``` - | |_______^ - | - = 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 12 previous errors +error: aborting due to 4 previous errors diff --git a/tests/rustdoc-ui/scrape-examples-fail-if-type-error.stderr b/tests/rustdoc-ui/scrape-examples-fail-if-type-error.stderr index 750aa32071944..bc30baf629c87 100644 --- a/tests/rustdoc-ui/scrape-examples-fail-if-type-error.stderr +++ b/tests/rustdoc-ui/scrape-examples-fail-if-type-error.stderr @@ -5,7 +5,7 @@ LL | INVALID_FUNC(); | ^^^^^^^^^^^^ could not resolve path `INVALID_FUNC` | = note: this error was originally ignored because you are running `rustdoc` - = note: try running again with `rustc` or `cargo check` and you may get a more detailed error + = help: try running again with `rustc` or `cargo check` and you may get a more detailed error error: Compilation failed, aborting rustdoc