diff --git a/compiler/rustc_mir_transform/src/coverage/expansion.rs b/compiler/rustc_mir_transform/src/coverage/expansion.rs index 827df27da52c7..0288afd95990d 100644 --- a/compiler/rustc_mir_transform/src/coverage/expansion.rs +++ b/compiler/rustc_mir_transform/src/coverage/expansion.rs @@ -1,6 +1,6 @@ use rustc_data_structures::fx::{FxIndexMap, FxIndexSet, IndexEntry}; use rustc_middle::mir; -use rustc_middle::mir::coverage::BasicCoverageBlock; +use rustc_middle::mir::coverage::{BasicCoverageBlock, BranchSpan}; use rustc_span::{ExpnId, ExpnKind, Span}; use crate::coverage::from_mir; @@ -71,11 +71,21 @@ pub(crate) struct ExpnNode { /// This links an expansion node to its parent in the tree. pub(crate) call_site_expn_id: Option, + /// Holds the function signature span, if it belongs to this expansion. + /// Used by special-case code in span refinement. + pub(crate) fn_sig_span: Option, + /// Holds the function body span, if it belongs to this expansion. + /// Used by special-case code in span refinement. + pub(crate) body_span: Option, + /// Spans (and their associated BCBs) belonging to this expansion. pub(crate) spans: Vec, /// Expansions whose call-site is in this expansion. pub(crate) child_expn_ids: FxIndexSet, + /// Branch spans (recorded during MIR building) belonging to this expansion. + pub(crate) branch_spans: Vec, + /// Hole spans belonging to this expansion, to be carved out from the /// code spans during span refinement. pub(crate) hole_spans: Vec, @@ -95,9 +105,14 @@ impl ExpnNode { call_site, call_site_expn_id, + fn_sig_span: None, + body_span: None, + spans: vec![], child_expn_ids: FxIndexSet::default(), + branch_spans: vec![], + hole_spans: vec![], } } @@ -142,6 +157,20 @@ pub(crate) fn build_expn_tree( } } + // If we have a span for the function signature, associate it with the + // corresponding expansion tree node. + if let Some(fn_sig_span) = hir_info.fn_sig_span + && let Some(node) = nodes.get_mut(&fn_sig_span.ctxt().outer_expn()) + { + node.fn_sig_span = Some(fn_sig_span); + } + + // Also associate the body span with its expansion tree node. + let body_span = hir_info.body_span; + if let Some(node) = nodes.get_mut(&body_span.ctxt().outer_expn()) { + node.body_span = Some(body_span); + } + // Associate each hole span (extracted from HIR) with its corresponding // expansion tree node. for &hole_span in &hir_info.hole_spans { @@ -150,5 +179,15 @@ pub(crate) fn build_expn_tree( node.hole_spans.push(hole_span); } + // Associate each branch span (recorded during MIR building) with its + // corresponding expansion tree node. + if let Some(coverage_info_hi) = mir_body.coverage_info_hi.as_deref() { + for branch_span in &coverage_info_hi.branch_spans { + if let Some(node) = nodes.get_mut(&branch_span.span.ctxt().outer_expn()) { + node.branch_spans.push(BranchSpan::clone(branch_span)); + } + } + } + ExpnTree { nodes } } diff --git a/compiler/rustc_mir_transform/src/coverage/mappings.rs b/compiler/rustc_mir_transform/src/coverage/mappings.rs index 5347e1150e5de..56f2db90ff8cd 100644 --- a/compiler/rustc_mir_transform/src/coverage/mappings.rs +++ b/compiler/rustc_mir_transform/src/coverage/mappings.rs @@ -4,12 +4,12 @@ use rustc_middle::mir::coverage::{ }; use rustc_middle::mir::{self, BasicBlock, StatementKind}; use rustc_middle::ty::TyCtxt; +use rustc_span::ExpnKind; -use crate::coverage::expansion; +use crate::coverage::expansion::{self, ExpnTree}; use crate::coverage::graph::CoverageGraph; use crate::coverage::hir_info::ExtractedHirInfo; use crate::coverage::spans::extract_refined_covspans; -use crate::coverage::unexpand::unexpand_into_body_span; #[derive(Default)] pub(crate) struct ExtractedMappings { @@ -31,7 +31,7 @@ pub(crate) fn extract_mappings_from_mir<'tcx>( // Extract ordinary code mappings from MIR statement/terminator spans. extract_refined_covspans(tcx, hir_info, graph, &expn_tree, &mut mappings); - extract_branch_mappings(mir_body, hir_info, graph, &mut mappings); + extract_branch_mappings(mir_body, hir_info, graph, &expn_tree, &mut mappings); ExtractedMappings { mappings } } @@ -57,25 +57,25 @@ fn resolve_block_markers( block_markers } -pub(super) fn extract_branch_mappings( +fn extract_branch_mappings( mir_body: &mir::Body<'_>, hir_info: &ExtractedHirInfo, graph: &CoverageGraph, + expn_tree: &ExpnTree, mappings: &mut Vec, ) { let Some(coverage_info_hi) = mir_body.coverage_info_hi.as_deref() else { return }; - let block_markers = resolve_block_markers(coverage_info_hi, mir_body); - mappings.extend(coverage_info_hi.branch_spans.iter().filter_map( - |&BranchSpan { span: raw_span, true_marker, false_marker }| try { - // For now, ignore any branch span that was introduced by - // expansion. This makes things like assert macros less noisy. - if !raw_span.ctxt().outer_expn_data().is_root() { - return None; - } - let span = unexpand_into_body_span(raw_span, hir_info.body_span)?; + // For now, ignore any branch span that was introduced by + // expansion. This makes things like assert macros less noisy. + let Some(node) = expn_tree.get(hir_info.body_span.ctxt().outer_expn()) else { return }; + if node.expn_kind != ExpnKind::Root { + return; + } + mappings.extend(node.branch_spans.iter().filter_map( + |&BranchSpan { span, true_marker, false_marker }| try { let bcb_from_marker = |marker: BlockMarkerId| graph.bcb_from_bb(block_markers[marker]?); let true_bcb = bcb_from_marker(true_marker)?; diff --git a/compiler/rustc_mir_transform/src/coverage/mod.rs b/compiler/rustc_mir_transform/src/coverage/mod.rs index cc9d7c800a96b..24a61c9b4a1bd 100644 --- a/compiler/rustc_mir_transform/src/coverage/mod.rs +++ b/compiler/rustc_mir_transform/src/coverage/mod.rs @@ -17,7 +17,6 @@ pub(super) mod query; mod spans; #[cfg(test)] mod tests; -mod unexpand; /// Inserts `StatementKind::Coverage` statements that either instrument the binary with injected /// counters, via intrinsic `llvm.instrprof.increment`, and/or inject metadata used during codegen diff --git a/compiler/rustc_mir_transform/src/coverage/spans.rs b/compiler/rustc_mir_transform/src/coverage/spans.rs index 2a06b409e8c2d..bc26a8ccc47ac 100644 --- a/compiler/rustc_mir_transform/src/coverage/spans.rs +++ b/compiler/rustc_mir_transform/src/coverage/spans.rs @@ -26,11 +26,9 @@ pub(super) fn extract_refined_covspans<'tcx>( return; } - let &ExtractedHirInfo { body_span, .. } = hir_info; - // If there somehow isn't an expansion tree node corresponding to the // body span, return now and don't create any mappings. - let Some(node) = expn_tree.get(body_span.ctxt().outer_expn()) else { return }; + let Some(node) = expn_tree.get(hir_info.body_span.ctxt().outer_expn()) else { return }; let mut covspans = vec![]; @@ -47,27 +45,29 @@ pub(super) fn extract_refined_covspans<'tcx>( } } - covspans.retain(|covspan: &Covspan| { - let covspan_span = covspan.span; - // Discard any spans not contained within the function body span. - // Also discard any spans that fill the entire body, because they tend - // to represent compiler-inserted code, e.g. implicitly returning `()`. - if !body_span.contains(covspan_span) || body_span.source_equal(covspan_span) { - return false; - } + if let Some(body_span) = node.body_span { + covspans.retain(|covspan: &Covspan| { + let covspan_span = covspan.span; + // Discard any spans not contained within the function body span. + // Also discard any spans that fill the entire body, because they tend + // to represent compiler-inserted code, e.g. implicitly returning `()`. + if !body_span.contains(covspan_span) || body_span.source_equal(covspan_span) { + return false; + } - // Each pushed covspan should have the same context as the body span. - // If it somehow doesn't, discard the covspan, or panic in debug builds. - if !body_span.eq_ctxt(covspan_span) { - debug_assert!( - false, - "span context mismatch: body_span={body_span:?}, covspan.span={covspan_span:?}" - ); - return false; - } + // Each pushed covspan should have the same context as the body span. + // If it somehow doesn't, discard the covspan, or panic in debug builds. + if !body_span.eq_ctxt(covspan_span) { + debug_assert!( + false, + "span context mismatch: body_span={body_span:?}, covspan.span={covspan_span:?}" + ); + return false; + } - true - }); + true + }); + } // Only proceed if we found at least one usable span. if covspans.is_empty() { @@ -78,10 +78,9 @@ pub(super) fn extract_refined_covspans<'tcx>( // Otherwise, add a fake span at the start of the body, to avoid an ugly // gap between the start of the body and the first real span. // FIXME: Find a more principled way to solve this problem. - covspans.push(Covspan { - span: hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()), - bcb: START_BCB, - }); + if let Some(span) = node.fn_sig_span.or_else(|| try { node.body_span?.shrink_to_lo() }) { + covspans.push(Covspan { span, bcb: START_BCB }); + } let compare_covspans = |a: &Covspan, b: &Covspan| { compare_spans(a.span, b.span) diff --git a/compiler/rustc_mir_transform/src/coverage/unexpand.rs b/compiler/rustc_mir_transform/src/coverage/unexpand.rs deleted file mode 100644 index 922edd3cc4fee..0000000000000 --- a/compiler/rustc_mir_transform/src/coverage/unexpand.rs +++ /dev/null @@ -1,9 +0,0 @@ -use rustc_span::Span; - -/// Walks through the expansion ancestors of `original_span` to find a span that -/// is contained in `body_span` and has the same [syntax context] as `body_span`. -pub(crate) fn unexpand_into_body_span(original_span: Span, body_span: Span) -> Option { - // Because we don't need to return any extra ancestor information, - // we can just delegate directly to `find_ancestor_inside_same_ctxt`. - original_span.find_ancestor_inside_same_ctxt(body_span) -} diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 872770def1775..3c0a89b7c8a7f 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -393,13 +393,15 @@ pub fn has_primitive_or_keyword_or_attribute_docs(attrs: &[impl AttributeExt]) - } /// Simplified version of the corresponding function in rustdoc. -/// If the rustdoc version returns a successful result, this function must return the same result. -/// Otherwise this function may return anything. fn preprocess_link(link: &str) -> Box { + // IMPORTANT: To be kept in sync with the corresponding function in rustdoc. + // Namely, whenever the rustdoc function returns a successful result for a given input, + // this function *MUST* return a link that's equal to `PreprocessingInfo.path_str`! + let link = link.replace('`', ""); let link = link.split('#').next().unwrap(); let link = link.trim(); - let link = link.rsplit('@').next().unwrap(); + let link = link.split_once('@').map_or(link, |(_, rhs)| rhs); let link = link.trim_suffix("()"); let link = link.trim_suffix("{}"); let link = link.trim_suffix("[]"); diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 4e4f21bf926fe..3abf0fee3959a 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -901,14 +901,18 @@ pub(crate) struct PreprocessedMarkdownLink( /// Returns: /// - `None` if the link should be ignored. -/// - `Some(Err)` if the link should emit an error -/// - `Some(Ok)` if the link is valid +/// - `Some(Err(_))` if the link should emit an error +/// - `Some(Ok(_))` if the link is valid /// /// `link_buffer` is needed for lifetime reasons; it will always be overwritten and the contents ignored. fn preprocess_link( ori_link: &MarkdownLink, dox: &str, ) -> Option> { + // IMPORTANT: To be kept in sync with the corresponding function in `rustc_resolve::rustdoc`. + // Namely, whenever this function returns a successful result for a given input, + // the rustc counterpart *MUST* return a link that's equal to `PreprocessingInfo.path_str`! + // certain link kinds cannot have their path be urls, // so they should not be ignored, no matter how much they look like urls. // e.g. [https://example.com/] is not a link to example.com. diff --git a/tests/coverage/branch/fn-in-macro.cov-map b/tests/coverage/branch/fn-in-macro.cov-map new file mode 100644 index 0000000000000..7f77251d324df --- /dev/null +++ b/tests/coverage/branch/fn-in-macro.cov-map @@ -0,0 +1,44 @@ +Function name: fn_in_macro::branch_in_macro +Raw bytes (19): 0x[01, 01, 00, 03, 01, 22, 01, 00, 15, 01, 0b, 05, 00, 17, 01, 01, 01, 00, 02] +Number of files: 1 +- file 0 => $DIR/fn-in-macro.rs +Number of expressions: 0 +Number of file 0 mappings: 3 +- Code(Counter(0)) at (prev + 34, 1) to (start + 0, 21) +- Code(Counter(0)) at (prev + 11, 5) to (start + 0, 23) +- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2) +Highest counter ID seen: c0 + +Function name: fn_in_macro::fn_in_macro +Raw bytes (31): 0x[01, 01, 01, 01, 05, 05, 01, 0c, 09, 00, 19, 01, 01, 10, 00, 25, 05, 00, 2c, 02, 0e, 02, 02, 14, 02, 0e, 01, 03, 09, 00, 0a] +Number of files: 1 +- file 0 => $DIR/fn-in-macro.rs +Number of expressions: 1 +- expression 0 operands: lhs = Counter(0), rhs = Counter(1) +Number of file 0 mappings: 5 +- Code(Counter(0)) at (prev + 12, 9) to (start + 0, 25) +- Code(Counter(0)) at (prev + 1, 16) to (start + 0, 37) +- Code(Counter(1)) at (prev + 0, 44) to (start + 2, 14) +- Code(Expression(0, Sub)) at (prev + 2, 20) to (start + 2, 14) + = (c0 - c1) +- Code(Counter(0)) at (prev + 3, 9) to (start + 0, 10) +Highest counter ID seen: c1 + +Function name: fn_in_macro::fn_not_in_macro +Raw bytes (38): 0x[01, 01, 01, 01, 05, 06, 01, 19, 01, 00, 15, 01, 01, 08, 00, 1d, 20, 05, 02, 00, 08, 00, 23, 05, 00, 24, 02, 06, 02, 02, 0c, 02, 06, 01, 03, 01, 00, 02] +Number of files: 1 +- file 0 => $DIR/fn-in-macro.rs +Number of expressions: 1 +- expression 0 operands: lhs = Counter(0), rhs = Counter(1) +Number of file 0 mappings: 6 +- Code(Counter(0)) at (prev + 25, 1) to (start + 0, 21) +- Code(Counter(0)) at (prev + 1, 8) to (start + 0, 29) +- Branch { true: Counter(1), false: Expression(0, Sub) } at (prev + 0, 8) to (start + 0, 35) + true = c1 + false = (c0 - c1) +- Code(Counter(1)) at (prev + 0, 36) to (start + 2, 6) +- Code(Expression(0, Sub)) at (prev + 2, 12) to (start + 2, 6) + = (c0 - c1) +- Code(Counter(0)) at (prev + 3, 1) to (start + 0, 2) +Highest counter ID seen: c1 + diff --git a/tests/coverage/branch/fn-in-macro.coverage b/tests/coverage/branch/fn-in-macro.coverage new file mode 100644 index 0000000000000..aa6f2f198e5f4 --- /dev/null +++ b/tests/coverage/branch/fn-in-macro.coverage @@ -0,0 +1,62 @@ + LL| |#![feature(coverage_attribute)] + LL| |//@ edition: 2024 + LL| |//@ compile-flags: -Zcoverage-options=branch + LL| |//@ llvm-cov-flags: --show-branches=count + LL| | + LL| |// Snapshot test demonstrating how branch coverage interacts with code in macros. + LL| |// This test captures current behavior, which is not necessarily "correct". + LL| | + LL| |macro_rules! define_fn { + LL| | () => { + LL| | /// Function defined entirely within a macro. + LL| 1| fn fn_in_macro() { + LL| 1| if core::hint::black_box(true) { + LL| 1| say("true"); + LL| 1| } else { + LL| 0| say("false"); + LL| 0| } + LL| 1| } + LL| | }; + LL| |} + LL| | + LL| |define_fn!(); + LL| | + LL| |/// Function not in a macro at all, for comparison. + LL| 1|fn fn_not_in_macro() { + LL| 1| if core::hint::black_box(true) { + ------------------ + | Branch (LL:8): [True: 1, False: 0] + ------------------ + LL| 1| say("true"); + LL| 1| } else { + LL| 0| say("false"); + LL| 0| } + LL| 1|} + LL| | + LL| |/// Function that is not in a macro, containing a branch that is in a macro. + LL| 1|fn branch_in_macro() { + LL| | macro_rules! macro_with_branch { + LL| | () => {{ + LL| | if core::hint::black_box(true) { + LL| | say("true"); + LL| | } else { + LL| | say("false"); + LL| | } + LL| | }}; + LL| | } + LL| | + LL| 1| macro_with_branch!(); + LL| 1|} + LL| | + LL| |#[coverage(off)] + LL| |fn main() { + LL| | fn_in_macro(); + LL| | fn_not_in_macro(); + LL| | branch_in_macro(); + LL| |} + LL| | + LL| |#[coverage(off)] + LL| |fn say(message: &str) { + LL| | println!("{message}"); + LL| |} + diff --git a/tests/coverage/branch/fn-in-macro.rs b/tests/coverage/branch/fn-in-macro.rs new file mode 100644 index 0000000000000..9b1d195782883 --- /dev/null +++ b/tests/coverage/branch/fn-in-macro.rs @@ -0,0 +1,58 @@ +#![feature(coverage_attribute)] +//@ edition: 2024 +//@ compile-flags: -Zcoverage-options=branch +//@ llvm-cov-flags: --show-branches=count + +// Snapshot test demonstrating how branch coverage interacts with code in macros. +// This test captures current behavior, which is not necessarily "correct". + +macro_rules! define_fn { + () => { + /// Function defined entirely within a macro. + fn fn_in_macro() { + if core::hint::black_box(true) { + say("true"); + } else { + say("false"); + } + } + }; +} + +define_fn!(); + +/// Function not in a macro at all, for comparison. +fn fn_not_in_macro() { + if core::hint::black_box(true) { + say("true"); + } else { + say("false"); + } +} + +/// Function that is not in a macro, containing a branch that is in a macro. +fn branch_in_macro() { + macro_rules! macro_with_branch { + () => {{ + if core::hint::black_box(true) { + say("true"); + } else { + say("false"); + } + }}; + } + + macro_with_branch!(); +} + +#[coverage(off)] +fn main() { + fn_in_macro(); + fn_not_in_macro(); + branch_in_macro(); +} + +#[coverage(off)] +fn say(message: &str) { + println!("{message}"); +} diff --git a/tests/coverage/macros/call-site-body.cov-map b/tests/coverage/macros/call-site-body.cov-map new file mode 100644 index 0000000000000..89d1eb3a84108 --- /dev/null +++ b/tests/coverage/macros/call-site-body.cov-map @@ -0,0 +1,21 @@ +Function name: call_site_body::fn_with_call_site_body +Raw bytes (19): 0x[01, 01, 00, 03, 01, 15, 05, 00, 06, 01, 01, 09, 00, 0c, 01, 00, 0d, 00, 14] +Number of files: 1 +- file 0 => $DIR/call-site-body.rs +Number of expressions: 0 +Number of file 0 mappings: 3 +- Code(Counter(0)) at (prev + 21, 5) to (start + 0, 6) +- Code(Counter(0)) at (prev + 1, 9) to (start + 0, 12) +- Code(Counter(0)) at (prev + 0, 13) to (start + 0, 20) +Highest counter ID seen: c0 + +Function name: call_site_body::fn_with_call_site_inner (unused) +Raw bytes (14): 0x[01, 01, 00, 02, 00, 1e, 09, 02, 0f, 00, 05, 09, 00, 0a] +Number of files: 1 +- file 0 => $DIR/call-site-body.rs +Number of expressions: 0 +Number of file 0 mappings: 2 +- Code(Zero) at (prev + 30, 9) to (start + 2, 15) +- Code(Zero) at (prev + 5, 9) to (start + 0, 10) +Highest counter ID seen: (none) + diff --git a/tests/coverage/macros/call-site-body.coverage b/tests/coverage/macros/call-site-body.coverage new file mode 100644 index 0000000000000..94a55cc710ae3 --- /dev/null +++ b/tests/coverage/macros/call-site-body.coverage @@ -0,0 +1,55 @@ + LL| |#![feature(coverage_attribute)] + LL| |//@ edition: 2024 + LL| | + LL| |// Snapshot test demonstrating how the function signature span and body span + LL| |// affect coverage instrumentation in the presence of macro expansion. + LL| |// This test captures current behaviour, which is not necessarily "correct". + LL| | + LL| |// This macro uses an argument token tree directly as a function body. + LL| |#[rustfmt::skip] + LL| |macro_rules! with_call_site_body { + LL| | ($body:tt) => { + LL| | fn + LL| | fn_with_call_site_body + LL| | () + LL| | $body + LL| | } + LL| |} + LL| | + LL| |with_call_site_body!( + LL| | // (force line break) + LL| 1| { + LL| 1| say("hello"); + LL| | } + LL| |); + LL| | + LL| |// This macro uses as an argument token tree as code within an explicit body. + LL| |#[rustfmt::skip] + LL| |macro_rules! with_call_site_inner { + LL| | ($inner:tt) => { + LL| 0| fn + LL| 0| fn_with_call_site_inner + LL| 0| () + LL| | { + LL| | $inner + LL| 0| } + LL| | }; + LL| |} + LL| | + LL| |with_call_site_inner!( + LL| | // (force line break) + LL| | { + LL| | say("hello"); + LL| | } + LL| |); + LL| | + LL| |#[coverage(off)] + LL| |fn main() { + LL| | fn_with_call_site_body(); + LL| |} + LL| | + LL| |#[coverage(off)] + LL| |fn say(message: &str) { + LL| | println!("{message}"); + LL| |} + diff --git a/tests/coverage/macros/call-site-body.rs b/tests/coverage/macros/call-site-body.rs new file mode 100644 index 0000000000000..f857a95fd57a4 --- /dev/null +++ b/tests/coverage/macros/call-site-body.rs @@ -0,0 +1,54 @@ +#![feature(coverage_attribute)] +//@ edition: 2024 + +// Snapshot test demonstrating how the function signature span and body span +// affect coverage instrumentation in the presence of macro expansion. +// This test captures current behaviour, which is not necessarily "correct". + +// This macro uses an argument token tree directly as a function body. +#[rustfmt::skip] +macro_rules! with_call_site_body { + ($body:tt) => { + fn + fn_with_call_site_body + () + $body + } +} + +with_call_site_body!( + // (force line break) + { + say("hello"); + } +); + +// This macro uses as an argument token tree as code within an explicit body. +#[rustfmt::skip] +macro_rules! with_call_site_inner { + ($inner:tt) => { + fn + fn_with_call_site_inner + () + { + $inner + } + }; +} + +with_call_site_inner!( + // (force line break) + { + say("hello"); + } +); + +#[coverage(off)] +fn main() { + fn_with_call_site_body(); +} + +#[coverage(off)] +fn say(message: &str) { + println!("{message}"); +} diff --git a/tests/rustdoc-ui/intra-doc/empty-associated-items.rs b/tests/rustdoc-ui/intra-doc/empty-associated-items.rs deleted file mode 100644 index ea94cb349ad29..0000000000000 --- a/tests/rustdoc-ui/intra-doc/empty-associated-items.rs +++ /dev/null @@ -1,8 +0,0 @@ -// This test ensures that an empty associated item will not crash rustdoc. -// This is a regression test for . - -#[deny(rustdoc::broken_intra_doc_links)] - -/// [`String::`] -//~^ ERROR -pub struct Foo; diff --git a/tests/rustdoc-ui/intra-doc/malformed-paths.rs b/tests/rustdoc-ui/intra-doc/malformed-paths.rs new file mode 100644 index 0000000000000..1f08d6d8efca9 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/malformed-paths.rs @@ -0,0 +1,17 @@ +// This test ensures that (syntactically) malformed paths will not crash rustdoc. +#![deny(rustdoc::broken_intra_doc_links)] + +// This is a regression test for . +//! [`Type::`] +//~^ ERROR + +// This is a regression test for . +//! [`struct@Type@field`] +//~^ ERROR + +//! [Type&content] +//~^ ERROR +//! [`Type::field%extra`] +//~^ ERROR + +pub struct Type { pub field: () } diff --git a/tests/rustdoc-ui/intra-doc/malformed-paths.stderr b/tests/rustdoc-ui/intra-doc/malformed-paths.stderr new file mode 100644 index 0000000000000..d38e7c2a73e48 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/malformed-paths.stderr @@ -0,0 +1,36 @@ +error: unresolved link to `Type::` + --> $DIR/malformed-paths.rs:5:7 + | +LL | //! [`Type::`] + | ^^^^^^ the struct `Type` has no field or associated item named `` + | +note: the lint level is defined here + --> $DIR/malformed-paths.rs:2:9 + | +LL | #![deny(rustdoc::broken_intra_doc_links)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unresolved link to `Type@field` + --> $DIR/malformed-paths.rs:9:7 + | +LL | //! [`struct@Type@field`] + | ^^^^^^^^^^^^^^^^^ no item named `Type@field` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` + +error: unresolved link to `Type&content` + --> $DIR/malformed-paths.rs:12:6 + | +LL | //! [Type&content] + | ^^^^^^^^^^^^ no item named `Type&content` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` + +error: unresolved link to `Type::field%extra` + --> $DIR/malformed-paths.rs:14:7 + | +LL | //! [`Type::field%extra`] + | ^^^^^^^^^^^^^^^^^ the struct `Type` has no field or associated item named `field%extra` + +error: aborting due to 4 previous errors +