1use std::ops::Range;
2
3use rustc_ast::NodeId;
4use rustc_errors::{Diag, DiagCtxtHandle, Diagnostic, Level, SuggestionStyle};
5use rustc_hir::HirId;
6use rustc_hir::def::{DefKind, Namespace, Res};
7use rustc_lint::Applicability;
8use rustc_middle::middle::resolve::DocLinkResMap;
9use rustc_resolve::rustdoc::pulldown_cmark::{
10 BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, OffsetIter, Parser, Tag,
11};
12use rustc_resolve::rustdoc::{prepare_to_doc_link_resolution, source_span_for_markdown_range};
13use rustc_span::def_id::{DefId, ModId};
14use rustc_span::{Span, Symbol};
15
16use crate::clean::utils::{find_nearest_parent_module, inherits_doc_hidden};
17use crate::clean::{Item, inline};
18use crate::core::DocContext;
19use crate::formats::item_type::ItemType;
20use crate::html::format::href_relative_parts;
21use crate::html::markdown::main_body_opts;
22
23#[derive(Debug)]
24struct LinkData {
25 resolvable_link: Option<String>,
26 resolvable_link_range: Option<Range<usize>>,
27 display_link: String,
28}
29
30pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId) {
31 let hunks = prepare_to_doc_link_resolution(&item.attrs.doc_strings);
32 for (item_id, doc) in hunks {
33 if let Some(item_id) = item_id.or(item.def_id()) {
34 if !doc.outer.is_empty() || !doc.inner.is_empty() {
35 let doc = format!("{}{}", doc.outer, doc.inner);
36 check_redundant_explicit_link_for_did(cx, item, item_id, hir_id, &doc);
37 }
38 }
39 }
40}
41
42fn check_redundant_explicit_link_for_did(
43 cx: &DocContext<'_>,
44 item: &Item,
45 did: DefId,
46 hir_id: HirId,
47 doc: &str,
48) {
49 let Some(local_item_id) = did.as_local() else {
50 return;
51 };
52
53 let is_hidden = !cx.document_hidden()
54 && (item.is_doc_hidden() || inherits_doc_hidden(cx.tcx, local_item_id, None));
55 if is_hidden {
56 return;
57 }
58 let is_private =
59 !cx.document_private() && !cx.cache.effective_visibilities.is_directly_public(cx.tcx, did);
60 if is_private {
61 return;
62 }
63
64 let module_id = match cx.tcx.def_kind(did) {
65 DefKind::Mod if item.inner_docs(cx.tcx) => ModId::new_unchecked(did),
66 _ => find_nearest_parent_module(cx.tcx, did).unwrap(),
67 };
68
69 let Some(resolutions) =
70 cx.tcx.resolutions(()).doc_link_resolutions.get(&module_id.expect_local())
71 else {
72 return;
76 };
77
78 check_redundant_explicit_link(cx, item, module_id.into(), hir_id, doc, resolutions);
79}
80
81fn check_redundant_explicit_link<'md>(
82 cx: &DocContext<'_>,
83 item: &Item,
84 module_id: DefId,
85 hir_id: HirId,
86 doc: &'md str,
87 resolutions: &DocLinkResMap,
88) {
89 let mut broken_line_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
90 let mut offset_iter = Parser::new_with_broken_link_callback(
91 doc,
92 main_body_opts(),
93 Some(&mut broken_line_callback),
94 )
95 .into_offset_iter();
96
97 while let Some((event, link_range)) = offset_iter.next() {
98 if let Event::Start(Tag::Link { link_type, dest_url, title, .. }) = event {
99 if !title.is_empty() {
100 continue;
104 }
105
106 let link_data = collect_link_data(&mut offset_iter);
107
108 let Some(resolvable_link) = link_data.resolvable_link.as_ref() else {
109 continue;
112 };
113
114 if &link_data.display_link.replace('`', "") != resolvable_link {
115 continue;
120 }
121
122 let check_result = match link_type {
123 LinkType::Inline | LinkType::ReferenceUnknown => {
124 check_inline_or_reference_unknown_redundancy(
125 cx,
126 item,
127 module_id,
128 hir_id,
129 doc,
130 resolutions,
131 link_range,
132 dest_url.to_string(),
133 link_data,
134 if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') },
135 )
136 }
137 LinkType::Reference => check_reference_redundancy(
138 cx,
139 item,
140 module_id,
141 hir_id,
142 doc,
143 resolutions,
144 link_range,
145 &dest_url,
146 link_data,
147 ),
148 _ => Ok(()),
149 };
150 if let Err(lint) = check_result {
151 cx.tcx.emit_node_span_lint(
152 crate::lint::REDUNDANT_EXPLICIT_LINKS,
153 hir_id,
154 item.attr_span(cx.tcx),
155 lint,
156 );
157 }
158 }
159 }
160}
161
162struct RedundantExplicitLinksWithoutSuggestion {
163 attr_span: Span,
164 display_link: String,
165 dest_link: String,
166}
167
168impl<'a> Diagnostic<'a> for RedundantExplicitLinksWithoutSuggestion {
169 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a> {
170 let Self { attr_span, display_link, dest_link } = self;
171
172 Diag::new(dcx, level, "redundant explicit link target")
173 .with_span_label(
174 attr_span,
175 format!("explicit target `{dest_link}` is redundant because label `{display_link}` resolves to same destination")
176 )
177 .with_note(
178 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
179 )
180 }
181}
182
183fn check_inline_or_reference_unknown_redundancy(
185 cx: &DocContext<'_>,
186 item: &Item,
187 module_id: DefId,
188 hir_id: HirId,
189 doc: &str,
190 resolutions: &DocLinkResMap,
191 link_range: Range<usize>,
192 dest: String,
193 link_data: LinkData,
194 (open, close): (u8, u8),
195) -> Result<(), RedundantExplicitLinksWithoutSuggestion> {
196 struct RedundantExplicitLinks {
197 explicit_span: Span,
198 display_span: Span,
199 link_span: Span,
200 display_link: String,
201 }
202
203 impl<'a> Diagnostic<'a> for RedundantExplicitLinks {
204 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a> {
205 let Self { explicit_span, display_span, link_span, display_link } = self;
206
207 Diag::new(dcx, level, "redundant explicit link target")
208 .with_span_label(
209 explicit_span,
210 "explicit target is redundant",
211 )
212 .with_span_label(
213 display_span,
214 "because label contains path that resolves to same destination",
215 )
216 .with_note(
217 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
218 )
219 .with_span_suggestion_with_style(
221 link_span,
222 "remove explicit link target",
223 format!("[{}]", display_link),
224 Applicability::MaybeIncorrect,
225 SuggestionStyle::ShowAlways,
226 )
227 }
228 }
229
230 let (Some(resolvable_link), Some(resolvable_link_range)) =
231 (&link_data.resolvable_link, &link_data.resolvable_link_range)
232 else {
233 return Ok(());
234 };
235
236 if explicit_link_is_redundant(cx, module_id, resolutions, &dest, resolvable_link) {
237 let attr_span = item.attr_span(cx.tcx);
238 let link_span =
239 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
240 {
241 Some((sp, from_expansion)) => {
242 if from_expansion {
243 return Ok(());
244 }
245 sp
246 }
247 None => attr_span,
248 };
249 let explicit_span = match source_span_for_markdown_range(
250 cx.tcx,
251 doc,
252 &offset_explicit_range(doc, link_range, open, close),
253 &item.attrs.doc_strings,
254 ) {
255 Some((explicit_span, false)) => explicit_span,
256 Some((_, true)) => return Ok(()),
258 None => {
260 return Err(RedundantExplicitLinksWithoutSuggestion {
261 display_link: resolvable_link.clone(),
262 dest_link: dest.to_string(),
263 attr_span,
264 });
265 }
266 };
267 let display_span = match source_span_for_markdown_range(
268 cx.tcx,
269 doc,
270 resolvable_link_range,
271 &item.attrs.doc_strings,
272 ) {
273 Some((display_span, false)) => display_span,
274 Some((_, true)) => return Ok(()),
276 None => {
278 return Err(RedundantExplicitLinksWithoutSuggestion {
279 display_link: resolvable_link.clone(),
280 dest_link: dest.to_string(),
281 attr_span,
282 });
283 }
284 };
285
286 cx.tcx.emit_node_span_lint(
287 crate::lint::REDUNDANT_EXPLICIT_LINKS,
288 hir_id,
289 explicit_span,
290 RedundantExplicitLinks {
291 explicit_span,
292 display_span,
293 link_span,
294 display_link: link_data.display_link,
295 },
296 );
297 }
298
299 Ok(())
300}
301
302fn check_reference_redundancy(
304 cx: &DocContext<'_>,
305 item: &Item,
306 module_id: DefId,
307 hir_id: HirId,
308 doc: &str,
309 resolutions: &DocLinkResMap,
310 link_range: Range<usize>,
311 dest: &CowStr<'_>,
312 link_data: LinkData,
313) -> Result<(), RedundantExplicitLinksWithoutSuggestion> {
314 struct RedundantExplicitLinkTarget {
315 explicit_span: Span,
316 display_span: Span,
317 def_span: Span,
318 link_span: Span,
319 display_link: String,
320 }
321
322 impl<'a> Diagnostic<'a> for RedundantExplicitLinkTarget {
323 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a> {
324 let Self { explicit_span, display_span, def_span, link_span, display_link } = self;
325
326 Diag::new(dcx, level, "redundant explicit link target")
327 .with_span_label(explicit_span, "explicit target is redundant")
328 .with_span_label(
329 display_span,
330 "because label contains path that resolves to same destination",
331 )
332 .with_span_note(def_span, "referenced explicit link target defined here")
333 .with_note(
334 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
335 )
336 .with_span_suggestion_with_style(
338 link_span,
339 "remove explicit link target",
340 format!("[{}]", display_link),
341 Applicability::MaybeIncorrect,
342 SuggestionStyle::ShowAlways,
343 )
344 }
345 }
346
347 let (Some(resolvable_link), Some(resolvable_link_range)) =
348 (&link_data.resolvable_link, &link_data.resolvable_link_range)
349 else {
350 return Ok(());
351 };
352
353 if explicit_link_is_redundant(cx, module_id, resolutions, dest, resolvable_link) {
354 let attr_span = item.attr_span(cx.tcx);
355 let link_span =
356 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
357 {
358 Some((sp, from_expansion)) => {
359 if from_expansion {
360 return Ok(());
362 }
363 sp
364 }
365 None => attr_span,
366 };
367 let explicit_span = match source_span_for_markdown_range(
368 cx.tcx,
369 doc,
370 &offset_explicit_range(doc, link_range.clone(), b'[', b']'),
371 &item.attrs.doc_strings,
372 ) {
373 Some((explicit_span, false)) => explicit_span,
374 Some((_, true)) => return Ok(()),
376 None => {
378 return Err(RedundantExplicitLinksWithoutSuggestion {
379 display_link: resolvable_link.clone(),
380 dest_link: dest.to_string(),
381 attr_span,
382 });
383 }
384 };
385 let display_span = match source_span_for_markdown_range(
386 cx.tcx,
387 doc,
388 resolvable_link_range,
389 &item.attrs.doc_strings,
390 ) {
391 Some((display_span, false)) => display_span,
392 Some((_, true)) => return Ok(()),
394 None => {
396 return Err(RedundantExplicitLinksWithoutSuggestion {
397 display_link: resolvable_link.clone(),
398 dest_link: dest.to_string(),
399 attr_span,
400 });
401 }
402 };
403 let def_span = match source_span_for_markdown_range(
404 cx.tcx,
405 doc,
406 &offset_reference_def_range(doc, dest, link_range),
407 &item.attrs.doc_strings,
408 ) {
409 Some((def_span, _)) => def_span,
410 None => {
412 return Err(RedundantExplicitLinksWithoutSuggestion {
413 display_link: resolvable_link.clone(),
414 dest_link: dest.to_string(),
415 attr_span,
416 });
417 }
418 };
419
420 cx.tcx.emit_node_span_lint(
421 crate::lint::REDUNDANT_EXPLICIT_LINKS,
422 hir_id,
423 explicit_span,
424 RedundantExplicitLinkTarget {
425 explicit_span,
426 display_span,
427 def_span,
428 link_span,
429 display_link: link_data.display_link,
430 },
431 );
432 }
433
434 Ok(())
435}
436
437fn explicit_link_is_redundant(
438 cx: &DocContext<'_>,
439 module_id: DefId,
440 resolutions: &DocLinkResMap,
441 dest: &str,
442 resolvable_link: &str,
443) -> bool {
444 let Some(display_res) = find_resolution(resolutions, resolvable_link) else {
445 return false;
446 };
447
448 if (dest.ends_with(resolvable_link) || resolvable_link.ends_with(dest))
449 && find_resolution(resolutions, dest).is_some_and(|dest_res| dest_res == display_res)
450 {
451 return true;
452 }
453
454 if dest.contains('#') || !dest.ends_with(".html") {
455 return false;
456 }
457
458 local_href_for_res(cx, module_id, display_res).is_some_and(|href| href == dest)
459}
460
461fn local_href_for_res(cx: &DocContext<'_>, module_id: DefId, res: Res<NodeId>) -> Option<String> {
462 let mut did = res.opt_def_id()?;
463 if matches!(cx.tcx.def_kind(did), DefKind::Ctor(..)) {
464 did = cx.tcx.parent(did);
465 }
466
467 if matches!(
468 cx.tcx.def_kind(did),
469 DefKind::AssocTy | DefKind::AssocFn | DefKind::AssocConst | DefKind::Variant
470 ) || !did.is_local()
471 {
472 return None;
473 }
474
475 let item_type = ItemType::from_def_id(did, cx.tcx);
476 let fqp = inline::get_item_path(cx.tcx, did, item_type);
477 let module_fqp = if item_type == ItemType::Module { &fqp[..] } else { &fqp[..fqp.len() - 1] };
478 let current_fqp = inline::get_item_path(cx.tcx, module_id, ItemType::Module);
479
480 let mut url_parts = href_relative_parts(module_fqp, ¤t_fqp);
481 match item_type {
482 ItemType::Module => url_parts.push("index.html"),
483 _ => url_parts.push_fmt(format_args!(
484 "{}.{last}.html",
485 item_type.as_str(),
486 last = fqp.last()?
487 )),
488 }
489 Some(url_parts.finish())
490}
491
492fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId>> {
493 [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS]
494 .into_iter()
495 .find_map(|ns| resolutions.get(&(Symbol::intern(path), ns)).copied().flatten())
496}
497
498fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
500 offset_iter: &mut OffsetIter<'input, F>,
501) -> LinkData {
502 let mut resolvable_link = None;
503 let mut resolvable_link_range = None;
504 let mut display_link = String::new();
505 let mut is_resolvable = true;
506
507 for (event, range) in offset_iter.by_ref() {
508 match event {
509 Event::Text(code) => {
510 let code = code.to_string();
511 display_link.push_str(&code);
512 resolvable_link = Some(code);
513 resolvable_link_range = Some(range);
514 }
515 Event::Code(code) => {
516 let code = code.to_string();
517 display_link.push('`');
518 display_link.push_str(&code);
519 display_link.push('`');
520 resolvable_link = Some(code);
521 resolvable_link_range = Some(range);
522 }
523 Event::Start(_) => {
524 is_resolvable = false;
527 }
528 Event::End(_) => {
529 break;
530 }
531 _ => {}
532 }
533 }
534
535 if !is_resolvable {
536 resolvable_link_range = None;
537 resolvable_link = None;
538 }
539
540 LinkData { resolvable_link, resolvable_link_range, display_link }
541}
542
543fn offset_explicit_range(md: &str, link_range: Range<usize>, open: u8, close: u8) -> Range<usize> {
544 let mut open_brace = !0;
545 let mut close_brace = !0;
546 for (i, b) in md.as_bytes()[link_range.clone()].iter().copied().enumerate().rev() {
547 let i = i + link_range.start;
548 if b == close {
549 close_brace = i;
550 break;
551 }
552 }
553
554 if close_brace < link_range.start || close_brace >= link_range.end {
555 return link_range;
556 }
557
558 let mut nesting = 1;
559
560 for (i, b) in md.as_bytes()[link_range.start..close_brace].iter().copied().enumerate().rev() {
561 let i = i + link_range.start;
562 if b == close {
563 nesting += 1;
564 }
565 if b == open {
566 nesting -= 1;
567 }
568 if nesting == 0 {
569 open_brace = i;
570 break;
571 }
572 }
573
574 assert!(open_brace != close_brace);
575
576 if open_brace < link_range.start || open_brace >= link_range.end {
577 return link_range;
578 }
579 (open_brace + 1)..close_brace
581}
582
583fn offset_reference_def_range(
584 md: &str,
585 dest: &CowStr<'_>,
586 link_range: Range<usize>,
587) -> Range<usize> {
588 match dest {
593 CowStr::Borrowed(s) => {
598 unsafe {
600 let s_start = dest.as_ptr();
601 let s_end = s_start.add(s.len());
602 let md_start = md.as_ptr();
603 let md_end = md_start.add(md.len());
604 if md_start <= s_start && s_end <= md_end {
605 let start = s_start.offset_from(md_start) as usize;
606 let end = s_end.offset_from(md_start) as usize;
607 start..end
608 } else {
609 link_range
610 }
611 }
612 }
613
614 CowStr::Boxed(_) | CowStr::Inlined(_) => link_range,
616 }
617}