rustdoc/passes/lint/
redundant_explicit_links.rs

1use std::ops::Range;
2
3use pulldown_cmark::{
4    BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, OffsetIter, Parser, Tag,
5};
6use rustc_ast::NodeId;
7use rustc_errors::SuggestionStyle;
8use rustc_hir::HirId;
9use rustc_hir::def::{DefKind, DocLinkResMap, Namespace, Res};
10use rustc_lint_defs::Applicability;
11use rustc_resolve::rustdoc::{prepare_to_doc_link_resolution, source_span_for_markdown_range};
12use rustc_span::Symbol;
13use rustc_span::def_id::DefId;
14
15use crate::clean::Item;
16use crate::clean::utils::{find_nearest_parent_module, inherits_doc_hidden};
17use crate::core::DocContext;
18use crate::html::markdown::main_body_opts;
19
20#[derive(Debug)]
21struct LinkData {
22    resolvable_link: Option<String>,
23    resolvable_link_range: Option<Range<usize>>,
24    display_link: String,
25}
26
27pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId) {
28    let hunks = prepare_to_doc_link_resolution(&item.attrs.doc_strings);
29    for (item_id, doc) in hunks {
30        if let Some(item_id) = item_id.or(item.def_id())
31            && !doc.is_empty()
32        {
33            check_redundant_explicit_link_for_did(cx, item, item_id, hir_id, &doc);
34        }
35    }
36}
37
38fn check_redundant_explicit_link_for_did(
39    cx: &DocContext<'_>,
40    item: &Item,
41    did: DefId,
42    hir_id: HirId,
43    doc: &str,
44) {
45    let Some(local_item_id) = did.as_local() else {
46        return;
47    };
48
49    let is_hidden = !cx.render_options.document_hidden
50        && (item.is_doc_hidden() || inherits_doc_hidden(cx.tcx, local_item_id, None));
51    if is_hidden {
52        return;
53    }
54    let is_private = !cx.render_options.document_private
55        && !cx.cache.effective_visibilities.is_directly_public(cx.tcx, did);
56    if is_private {
57        return;
58    }
59
60    let module_id = match cx.tcx.def_kind(did) {
61        DefKind::Mod if item.inner_docs(cx.tcx) => did,
62        _ => find_nearest_parent_module(cx.tcx, did).unwrap(),
63    };
64
65    let Some(resolutions) =
66        cx.tcx.resolutions(()).doc_link_resolutions.get(&module_id.expect_local())
67    else {
68        // If there's no resolutions in this module,
69        // then we skip resolution querying to
70        // avoid from panicking.
71        return;
72    };
73
74    check_redundant_explicit_link(cx, item, hir_id, doc, resolutions);
75}
76
77fn check_redundant_explicit_link<'md>(
78    cx: &DocContext<'_>,
79    item: &Item,
80    hir_id: HirId,
81    doc: &'md str,
82    resolutions: &DocLinkResMap,
83) -> Option<()> {
84    let mut broken_line_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
85    let mut offset_iter = Parser::new_with_broken_link_callback(
86        doc,
87        main_body_opts(),
88        Some(&mut broken_line_callback),
89    )
90    .into_offset_iter();
91
92    while let Some((event, link_range)) = offset_iter.next() {
93        if let Event::Start(Tag::Link { link_type, dest_url, .. }) = event {
94            let link_data = collect_link_data(&mut offset_iter);
95
96            if let Some(resolvable_link) = link_data.resolvable_link.as_ref() {
97                if &link_data.display_link.replace('`', "") != resolvable_link {
98                    // Skips if display link does not match to actual
99                    // resolvable link, usually happens if display link
100                    // has several segments, e.g.
101                    // [this is just an `Option`](Option)
102                    continue;
103                }
104            }
105
106            let explicit_link = dest_url.to_string();
107            let display_link = link_data.resolvable_link.clone()?;
108
109            if explicit_link.ends_with(&display_link) || display_link.ends_with(&explicit_link) {
110                match link_type {
111                    LinkType::Inline | LinkType::ReferenceUnknown => {
112                        check_inline_or_reference_unknown_redundancy(
113                            cx,
114                            item,
115                            hir_id,
116                            doc,
117                            resolutions,
118                            link_range,
119                            dest_url.to_string(),
120                            link_data,
121                            if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') },
122                        );
123                    }
124                    LinkType::Reference => {
125                        check_reference_redundancy(
126                            cx,
127                            item,
128                            hir_id,
129                            doc,
130                            resolutions,
131                            link_range,
132                            &dest_url,
133                            link_data,
134                        );
135                    }
136                    _ => {}
137                }
138            }
139        }
140    }
141
142    None
143}
144
145/// FIXME(ChAoSUnItY): Too many arguments.
146fn check_inline_or_reference_unknown_redundancy(
147    cx: &DocContext<'_>,
148    item: &Item,
149    hir_id: HirId,
150    doc: &str,
151    resolutions: &DocLinkResMap,
152    link_range: Range<usize>,
153    dest: String,
154    link_data: LinkData,
155    (open, close): (u8, u8),
156) -> Option<()> {
157    let (resolvable_link, resolvable_link_range) =
158        (&link_data.resolvable_link?, &link_data.resolvable_link_range?);
159    let (dest_res, display_res) =
160        (find_resolution(resolutions, &dest)?, find_resolution(resolutions, resolvable_link)?);
161
162    if dest_res == display_res {
163        let link_span =
164            source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
165                .unwrap_or(item.attr_span(cx.tcx));
166        let explicit_span = source_span_for_markdown_range(
167            cx.tcx,
168            doc,
169            &offset_explicit_range(doc, link_range, open, close),
170            &item.attrs.doc_strings,
171        )?;
172        let display_span = source_span_for_markdown_range(
173            cx.tcx,
174            doc,
175            resolvable_link_range,
176            &item.attrs.doc_strings,
177        )?;
178
179        cx.tcx.node_span_lint(crate::lint::REDUNDANT_EXPLICIT_LINKS, hir_id, explicit_span, |lint| {
180            lint.primary_message("redundant explicit link target")
181                .span_label(explicit_span, "explicit target is redundant")
182                .span_label(display_span, "because label contains path that resolves to same destination")
183                .note("when a link's destination is not specified,\nthe label is used to resolve intra-doc links")
184                .span_suggestion_with_style(link_span, "remove explicit link target", format!("[{}]", link_data.display_link), Applicability::MaybeIncorrect, SuggestionStyle::ShowAlways);
185        });
186    }
187
188    None
189}
190
191/// FIXME(ChAoSUnItY): Too many arguments.
192fn check_reference_redundancy(
193    cx: &DocContext<'_>,
194    item: &Item,
195    hir_id: HirId,
196    doc: &str,
197    resolutions: &DocLinkResMap,
198    link_range: Range<usize>,
199    dest: &CowStr<'_>,
200    link_data: LinkData,
201) -> Option<()> {
202    let (resolvable_link, resolvable_link_range) =
203        (&link_data.resolvable_link?, &link_data.resolvable_link_range?);
204    let (dest_res, display_res) =
205        (find_resolution(resolutions, dest)?, find_resolution(resolutions, resolvable_link)?);
206
207    if dest_res == display_res {
208        let link_span =
209            source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
210                .unwrap_or(item.attr_span(cx.tcx));
211        let explicit_span = source_span_for_markdown_range(
212            cx.tcx,
213            doc,
214            &offset_explicit_range(doc, link_range.clone(), b'[', b']'),
215            &item.attrs.doc_strings,
216        )?;
217        let display_span = source_span_for_markdown_range(
218            cx.tcx,
219            doc,
220            resolvable_link_range,
221            &item.attrs.doc_strings,
222        )?;
223        let def_span = source_span_for_markdown_range(
224            cx.tcx,
225            doc,
226            &offset_reference_def_range(doc, dest, link_range),
227            &item.attrs.doc_strings,
228        )?;
229
230        cx.tcx.node_span_lint(crate::lint::REDUNDANT_EXPLICIT_LINKS, hir_id, explicit_span, |lint| {
231            lint.primary_message("redundant explicit link target")
232            .span_label(explicit_span, "explicit target is redundant")
233                .span_label(display_span, "because label contains path that resolves to same destination")
234                .span_note(def_span, "referenced explicit link target defined here")
235                .note("when a link's destination is not specified,\nthe label is used to resolve intra-doc links")
236                .span_suggestion_with_style(link_span, "remove explicit link target", format!("[{}]", link_data.display_link), Applicability::MaybeIncorrect, SuggestionStyle::ShowAlways);
237        });
238    }
239
240    None
241}
242
243fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId>> {
244    [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS]
245        .into_iter()
246        .find_map(|ns| resolutions.get(&(Symbol::intern(path), ns)).copied().flatten())
247}
248
249/// Collects all necessary data of link.
250fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
251    offset_iter: &mut OffsetIter<'input, F>,
252) -> LinkData {
253    let mut resolvable_link = None;
254    let mut resolvable_link_range = None;
255    let mut display_link = String::new();
256    let mut is_resolvable = true;
257
258    for (event, range) in offset_iter.by_ref() {
259        match event {
260            Event::Text(code) => {
261                let code = code.to_string();
262                display_link.push_str(&code);
263                resolvable_link = Some(code);
264                resolvable_link_range = Some(range);
265            }
266            Event::Code(code) => {
267                let code = code.to_string();
268                display_link.push('`');
269                display_link.push_str(&code);
270                display_link.push('`');
271                resolvable_link = Some(code);
272                resolvable_link_range = Some(range);
273            }
274            Event::Start(_) => {
275                // If there is anything besides backticks, it's not considered as an intra-doc link
276                // so we ignore it.
277                is_resolvable = false;
278            }
279            Event::End(_) => {
280                break;
281            }
282            _ => {}
283        }
284    }
285
286    if !is_resolvable {
287        resolvable_link_range = None;
288        resolvable_link = None;
289    }
290
291    LinkData { resolvable_link, resolvable_link_range, display_link }
292}
293
294fn offset_explicit_range(md: &str, link_range: Range<usize>, open: u8, close: u8) -> Range<usize> {
295    let mut open_brace = !0;
296    let mut close_brace = !0;
297    for (i, b) in md.as_bytes()[link_range.clone()].iter().copied().enumerate().rev() {
298        let i = i + link_range.start;
299        if b == close {
300            close_brace = i;
301            break;
302        }
303    }
304
305    if close_brace < link_range.start || close_brace >= link_range.end {
306        return link_range;
307    }
308
309    let mut nesting = 1;
310
311    for (i, b) in md.as_bytes()[link_range.start..close_brace].iter().copied().enumerate().rev() {
312        let i = i + link_range.start;
313        if b == close {
314            nesting += 1;
315        }
316        if b == open {
317            nesting -= 1;
318        }
319        if nesting == 0 {
320            open_brace = i;
321            break;
322        }
323    }
324
325    assert!(open_brace != close_brace);
326
327    if open_brace < link_range.start || open_brace >= link_range.end {
328        return link_range;
329    }
330    // do not actually include braces in the span
331    (open_brace + 1)..close_brace
332}
333
334fn offset_reference_def_range(
335    md: &str,
336    dest: &CowStr<'_>,
337    link_range: Range<usize>,
338) -> Range<usize> {
339    // For diagnostics, we want to underline the link's definition but `span` will point at
340    // where the link is used. This is a problem for reference-style links, where the definition
341    // is separate from the usage.
342
343    match dest {
344        // `Borrowed` variant means the string (the link's destination) may come directly from
345        // the markdown text and we can locate the original link destination.
346        // NOTE: LinkReplacer also provides `Borrowed` but possibly from other sources,
347        // so `locate()` can fall back to use `span`.
348        CowStr::Borrowed(s) => {
349            // FIXME: remove this function once pulldown_cmark can provide spans for link definitions.
350            unsafe {
351                let s_start = dest.as_ptr();
352                let s_end = s_start.add(s.len());
353                let md_start = md.as_ptr();
354                let md_end = md_start.add(md.len());
355                if md_start <= s_start && s_end <= md_end {
356                    let start = s_start.offset_from(md_start) as usize;
357                    let end = s_end.offset_from(md_start) as usize;
358                    start..end
359                } else {
360                    link_range
361                }
362            }
363        }
364
365        // For anything else, we can only use the provided range.
366        CowStr::Boxed(_) | CowStr::Inlined(_) => link_range,
367    }
368}