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, DocLinkResMap, Namespace, Res};
7use rustc_lint_defs::Applicability;
8use rustc_resolve::rustdoc::pulldown_cmark::{
9 BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, OffsetIter, Parser, Tag,
10};
11use rustc_resolve::rustdoc::{prepare_to_doc_link_resolution, source_span_for_markdown_range};
12use rustc_span::def_id::{DefId, ModId};
13use rustc_span::{Span, Symbol};
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.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 =
55 !cx.document_private() && !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) => ModId::new_unchecked(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 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) {
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, title, .. }) = event {
94 if !title.is_empty() {
95 continue;
99 }
100
101 let link_data = collect_link_data(&mut offset_iter);
102
103 let Some(resolvable_link) = link_data.resolvable_link.as_ref() else {
104 continue;
107 };
108
109 if &link_data.display_link.replace('`', "") != resolvable_link {
110 continue;
115 }
116
117 if dest_url.ends_with(resolvable_link) || resolvable_link.ends_with(&*dest_url) {
118 let check_result = match link_type {
119 LinkType::Inline | LinkType::ReferenceUnknown => {
120 check_inline_or_reference_unknown_redundancy(
121 cx,
122 item,
123 hir_id,
124 doc,
125 resolutions,
126 link_range,
127 dest_url.to_string(),
128 link_data,
129 if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') },
130 )
131 }
132 LinkType::Reference => check_reference_redundancy(
133 cx,
134 item,
135 hir_id,
136 doc,
137 resolutions,
138 link_range,
139 &dest_url,
140 link_data,
141 ),
142 _ => Ok(()),
143 };
144 if let Err(lint) = check_result {
145 cx.tcx.emit_node_span_lint(
146 crate::lint::REDUNDANT_EXPLICIT_LINKS,
147 hir_id,
148 item.attr_span(cx.tcx),
149 lint,
150 );
151 }
152 }
153 }
154 }
155}
156
157struct RedundantExplicitLinksWithoutSuggestion {
158 attr_span: Span,
159 display_link: String,
160 dest_link: String,
161}
162
163impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinksWithoutSuggestion {
164 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
165 let Self { attr_span, display_link, dest_link } = self;
166
167 Diag::new(dcx, level, "redundant explicit link target")
168 .with_span_label(
169 attr_span,
170 format!("explicit target `{dest_link}` is redundant because label `{display_link}` resolves to same destination")
171 )
172 .with_note(
173 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
174 )
175 }
176}
177
178fn check_inline_or_reference_unknown_redundancy(
180 cx: &DocContext<'_>,
181 item: &Item,
182 hir_id: HirId,
183 doc: &str,
184 resolutions: &DocLinkResMap,
185 link_range: Range<usize>,
186 dest: String,
187 link_data: LinkData,
188 (open, close): (u8, u8),
189) -> Result<(), RedundantExplicitLinksWithoutSuggestion> {
190 struct RedundantExplicitLinks {
191 explicit_span: Span,
192 display_span: Span,
193 link_span: Span,
194 display_link: String,
195 }
196
197 impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinks {
198 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
199 let Self { explicit_span, display_span, link_span, display_link } = self;
200
201 Diag::new(dcx, level, "redundant explicit link target")
202 .with_span_label(
203 explicit_span,
204 "explicit target is redundant",
205 )
206 .with_span_label(
207 display_span,
208 "because label contains path that resolves to same destination",
209 )
210 .with_note(
211 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
212 )
213 .with_span_suggestion_with_style(
215 link_span,
216 "remove explicit link target",
217 format!("[{}]", display_link),
218 Applicability::MaybeIncorrect,
219 SuggestionStyle::ShowAlways,
220 )
221 }
222 }
223
224 let (Some(resolvable_link), Some(resolvable_link_range)) =
225 (&link_data.resolvable_link, &link_data.resolvable_link_range)
226 else {
227 return Ok(());
228 };
229 let (Some(dest_res), Some(display_res)) =
230 (find_resolution(resolutions, &dest), find_resolution(resolutions, resolvable_link))
231 else {
232 return Ok(());
233 };
234
235 if dest_res == display_res {
236 let attr_span = item.attr_span(cx.tcx);
237 let link_span =
238 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
239 {
240 Some((sp, from_expansion)) => {
241 if from_expansion {
242 return Ok(());
243 }
244 sp
245 }
246 None => attr_span,
247 };
248 let explicit_span = match source_span_for_markdown_range(
249 cx.tcx,
250 doc,
251 &offset_explicit_range(doc, link_range, open, close),
252 &item.attrs.doc_strings,
253 ) {
254 Some((explicit_span, false)) => explicit_span,
255 Some((_, true)) => return Ok(()),
257 None => {
259 return Err(RedundantExplicitLinksWithoutSuggestion {
260 display_link: resolvable_link.clone(),
261 dest_link: dest.to_string(),
262 attr_span,
263 });
264 }
265 };
266 let display_span = match source_span_for_markdown_range(
267 cx.tcx,
268 doc,
269 resolvable_link_range,
270 &item.attrs.doc_strings,
271 ) {
272 Some((display_span, false)) => display_span,
273 Some((_, true)) => return Ok(()),
275 None => {
277 return Err(RedundantExplicitLinksWithoutSuggestion {
278 display_link: resolvable_link.clone(),
279 dest_link: dest.to_string(),
280 attr_span,
281 });
282 }
283 };
284
285 cx.tcx.emit_node_span_lint(
286 crate::lint::REDUNDANT_EXPLICIT_LINKS,
287 hir_id,
288 explicit_span,
289 RedundantExplicitLinks {
290 explicit_span,
291 display_span,
292 link_span,
293 display_link: link_data.display_link,
294 },
295 );
296 }
297
298 Ok(())
299}
300
301fn check_reference_redundancy(
303 cx: &DocContext<'_>,
304 item: &Item,
305 hir_id: HirId,
306 doc: &str,
307 resolutions: &DocLinkResMap,
308 link_range: Range<usize>,
309 dest: &CowStr<'_>,
310 link_data: LinkData,
311) -> Result<(), RedundantExplicitLinksWithoutSuggestion> {
312 struct RedundantExplicitLinkTarget {
313 explicit_span: Span,
314 display_span: Span,
315 def_span: Span,
316 link_span: Span,
317 display_link: String,
318 }
319
320 impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinkTarget {
321 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
322 let Self { explicit_span, display_span, def_span, link_span, display_link } = self;
323
324 Diag::new(dcx, level, "redundant explicit link target")
325 .with_span_label(explicit_span, "explicit target is redundant")
326 .with_span_label(
327 display_span,
328 "because label contains path that resolves to same destination",
329 )
330 .with_span_note(def_span, "referenced explicit link target defined here")
331 .with_note(
332 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
333 )
334 .with_span_suggestion_with_style(
336 link_span,
337 "remove explicit link target",
338 format!("[{}]", display_link),
339 Applicability::MaybeIncorrect,
340 SuggestionStyle::ShowAlways,
341 )
342 }
343 }
344
345 let (Some(resolvable_link), Some(resolvable_link_range)) =
346 (&link_data.resolvable_link, &link_data.resolvable_link_range)
347 else {
348 return Ok(());
349 };
350 let (Some(dest_res), Some(display_res)) =
351 (find_resolution(resolutions, dest), find_resolution(resolutions, resolvable_link))
352 else {
353 return Ok(());
354 };
355
356 if dest_res == display_res {
357 let attr_span = item.attr_span(cx.tcx);
358 let link_span =
359 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
360 {
361 Some((sp, from_expansion)) => {
362 if from_expansion {
363 return Ok(());
365 }
366 sp
367 }
368 None => attr_span,
369 };
370 let explicit_span = match source_span_for_markdown_range(
371 cx.tcx,
372 doc,
373 &offset_explicit_range(doc, link_range.clone(), b'[', b']'),
374 &item.attrs.doc_strings,
375 ) {
376 Some((explicit_span, false)) => explicit_span,
377 Some((_, true)) => return Ok(()),
379 None => {
381 return Err(RedundantExplicitLinksWithoutSuggestion {
382 display_link: resolvable_link.clone(),
383 dest_link: dest.to_string(),
384 attr_span,
385 });
386 }
387 };
388 let display_span = match source_span_for_markdown_range(
389 cx.tcx,
390 doc,
391 resolvable_link_range,
392 &item.attrs.doc_strings,
393 ) {
394 Some((display_span, false)) => display_span,
395 Some((_, true)) => return Ok(()),
397 None => {
399 return Err(RedundantExplicitLinksWithoutSuggestion {
400 display_link: resolvable_link.clone(),
401 dest_link: dest.to_string(),
402 attr_span,
403 });
404 }
405 };
406 let def_span = match source_span_for_markdown_range(
407 cx.tcx,
408 doc,
409 &offset_reference_def_range(doc, dest, link_range),
410 &item.attrs.doc_strings,
411 ) {
412 Some((def_span, _)) => def_span,
413 None => {
415 return Err(RedundantExplicitLinksWithoutSuggestion {
416 display_link: resolvable_link.clone(),
417 dest_link: dest.to_string(),
418 attr_span,
419 });
420 }
421 };
422
423 cx.tcx.emit_node_span_lint(
424 crate::lint::REDUNDANT_EXPLICIT_LINKS,
425 hir_id,
426 explicit_span,
427 RedundantExplicitLinkTarget {
428 explicit_span,
429 display_span,
430 def_span,
431 link_span,
432 display_link: link_data.display_link,
433 },
434 );
435 }
436
437 Ok(())
438}
439
440fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId>> {
441 [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS]
442 .into_iter()
443 .find_map(|ns| resolutions.get(&(Symbol::intern(path), ns)).copied().flatten())
444}
445
446fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
448 offset_iter: &mut OffsetIter<'input, F>,
449) -> LinkData {
450 let mut resolvable_link = None;
451 let mut resolvable_link_range = None;
452 let mut display_link = String::new();
453 let mut is_resolvable = true;
454
455 for (event, range) in offset_iter.by_ref() {
456 match event {
457 Event::Text(code) => {
458 let code = code.to_string();
459 display_link.push_str(&code);
460 resolvable_link = Some(code);
461 resolvable_link_range = Some(range);
462 }
463 Event::Code(code) => {
464 let code = code.to_string();
465 display_link.push('`');
466 display_link.push_str(&code);
467 display_link.push('`');
468 resolvable_link = Some(code);
469 resolvable_link_range = Some(range);
470 }
471 Event::Start(_) => {
472 is_resolvable = false;
475 }
476 Event::End(_) => {
477 break;
478 }
479 _ => {}
480 }
481 }
482
483 if !is_resolvable {
484 resolvable_link_range = None;
485 resolvable_link = None;
486 }
487
488 LinkData { resolvable_link, resolvable_link_range, display_link }
489}
490
491fn offset_explicit_range(md: &str, link_range: Range<usize>, open: u8, close: u8) -> Range<usize> {
492 let mut open_brace = !0;
493 let mut close_brace = !0;
494 for (i, b) in md.as_bytes()[link_range.clone()].iter().copied().enumerate().rev() {
495 let i = i + link_range.start;
496 if b == close {
497 close_brace = i;
498 break;
499 }
500 }
501
502 if close_brace < link_range.start || close_brace >= link_range.end {
503 return link_range;
504 }
505
506 let mut nesting = 1;
507
508 for (i, b) in md.as_bytes()[link_range.start..close_brace].iter().copied().enumerate().rev() {
509 let i = i + link_range.start;
510 if b == close {
511 nesting += 1;
512 }
513 if b == open {
514 nesting -= 1;
515 }
516 if nesting == 0 {
517 open_brace = i;
518 break;
519 }
520 }
521
522 assert!(open_brace != close_brace);
523
524 if open_brace < link_range.start || open_brace >= link_range.end {
525 return link_range;
526 }
527 (open_brace + 1)..close_brace
529}
530
531fn offset_reference_def_range(
532 md: &str,
533 dest: &CowStr<'_>,
534 link_range: Range<usize>,
535) -> Range<usize> {
536 match dest {
541 CowStr::Borrowed(s) => {
546 unsafe {
548 let s_start = dest.as_ptr();
549 let s_end = s_start.add(s.len());
550 let md_start = md.as_ptr();
551 let md_end = md_start.add(md.len());
552 if md_start <= s_start && s_end <= md_end {
553 let start = s_start.offset_from(md_start) as usize;
554 let end = s_end.offset_from(md_start) as usize;
555 start..end
556 } else {
557 link_range
558 }
559 }
560 }
561
562 CowStr::Boxed(_) | CowStr::Inlined(_) => link_range,
564 }
565}