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;
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) => 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) -> 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, 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 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 => {
133 check_reference_redundancy(
134 cx,
135 item,
136 hir_id,
137 doc,
138 resolutions,
139 link_range,
140 &dest_url,
141 link_data,
142 );
143 }
144 _ => {}
145 }
146 }
147 }
148 }
149
150 None
151}
152
153fn check_inline_or_reference_unknown_redundancy(
155 cx: &DocContext<'_>,
156 item: &Item,
157 hir_id: HirId,
158 doc: &str,
159 resolutions: &DocLinkResMap,
160 link_range: Range<usize>,
161 dest: String,
162 link_data: LinkData,
163 (open, close): (u8, u8),
164) -> Option<()> {
165 struct RedundantExplicitLinks {
166 explicit_span: Span,
167 display_span: Span,
168 link_span: Span,
169 display_link: String,
170 }
171
172 impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinks {
173 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
174 let Self { explicit_span, display_span, link_span, display_link } = self;
175
176 Diag::new(dcx, level, "redundant explicit link target")
177 .with_span_label(
178 explicit_span,
179 "explicit target is redundant",
180 )
181 .with_span_label(
182 display_span,
183 "because label contains path that resolves to same destination",
184 )
185 .with_note(
186 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
187 )
188 .with_span_suggestion_with_style(
190 link_span,
191 "remove explicit link target",
192 format!("[{}]", display_link),
193 Applicability::MaybeIncorrect,
194 SuggestionStyle::ShowAlways,
195 )
196 }
197 }
198
199 let (resolvable_link, resolvable_link_range) =
200 (&link_data.resolvable_link?, &link_data.resolvable_link_range?);
201 let (dest_res, display_res) =
202 (find_resolution(resolutions, &dest)?, find_resolution(resolutions, resolvable_link)?);
203
204 if dest_res == display_res {
205 let link_span =
206 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
207 {
208 Some((sp, from_expansion)) => {
209 if from_expansion {
210 return None;
211 }
212 sp
213 }
214 None => item.attr_span(cx.tcx),
215 };
216 let (explicit_span, false) = source_span_for_markdown_range(
217 cx.tcx,
218 doc,
219 &offset_explicit_range(doc, link_range, open, close),
220 &item.attrs.doc_strings,
221 )?
222 else {
223 return None;
225 };
226 let (display_span, false) = source_span_for_markdown_range(
227 cx.tcx,
228 doc,
229 resolvable_link_range,
230 &item.attrs.doc_strings,
231 )?
232 else {
233 return None;
235 };
236
237 cx.tcx.emit_node_span_lint(
238 crate::lint::REDUNDANT_EXPLICIT_LINKS,
239 hir_id,
240 explicit_span,
241 RedundantExplicitLinks {
242 explicit_span,
243 display_span,
244 link_span,
245 display_link: link_data.display_link,
246 },
247 );
248 }
249
250 None
251}
252
253fn check_reference_redundancy(
255 cx: &DocContext<'_>,
256 item: &Item,
257 hir_id: HirId,
258 doc: &str,
259 resolutions: &DocLinkResMap,
260 link_range: Range<usize>,
261 dest: &CowStr<'_>,
262 link_data: LinkData,
263) -> Option<()> {
264 struct RedundantExplicitLinkTarget {
265 explicit_span: Span,
266 display_span: Span,
267 def_span: Span,
268 link_span: Span,
269 display_link: String,
270 }
271
272 impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinkTarget {
273 fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
274 let Self { explicit_span, display_span, def_span, link_span, display_link } = self;
275
276 Diag::new(dcx, level, "redundant explicit link target")
277 .with_span_label(explicit_span, "explicit target is redundant")
278 .with_span_label(
279 display_span,
280 "because label contains path that resolves to same destination",
281 )
282 .with_span_note(def_span, "referenced explicit link target defined here")
283 .with_note(
284 "when a link's destination is not specified,\nthe label is used to resolve intra-doc links"
285 )
286 .with_span_suggestion_with_style(
288 link_span,
289 "remove explicit link target",
290 format!("[{}]", display_link),
291 Applicability::MaybeIncorrect,
292 SuggestionStyle::ShowAlways,
293 )
294 }
295 }
296
297 let (resolvable_link, resolvable_link_range) =
298 (&link_data.resolvable_link?, &link_data.resolvable_link_range?);
299 let (dest_res, display_res) =
300 (find_resolution(resolutions, dest)?, find_resolution(resolutions, resolvable_link)?);
301
302 if dest_res == display_res {
303 let link_span =
304 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
305 {
306 Some((sp, from_expansion)) => {
307 if from_expansion {
308 return None;
309 }
310 sp
311 }
312 None => item.attr_span(cx.tcx),
313 };
314 let (explicit_span, false) = source_span_for_markdown_range(
315 cx.tcx,
316 doc,
317 &offset_explicit_range(doc, link_range.clone(), b'[', b']'),
318 &item.attrs.doc_strings,
319 )?
320 else {
321 return None;
323 };
324 let (display_span, false) = source_span_for_markdown_range(
325 cx.tcx,
326 doc,
327 resolvable_link_range,
328 &item.attrs.doc_strings,
329 )?
330 else {
331 return None;
333 };
334 let (def_span, _) = source_span_for_markdown_range(
335 cx.tcx,
336 doc,
337 &offset_reference_def_range(doc, dest, link_range),
338 &item.attrs.doc_strings,
339 )?;
340
341 cx.tcx.emit_node_span_lint(
342 crate::lint::REDUNDANT_EXPLICIT_LINKS,
343 hir_id,
344 explicit_span,
345 RedundantExplicitLinkTarget {
346 explicit_span,
347 display_span,
348 def_span,
349 link_span,
350 display_link: link_data.display_link,
351 },
352 );
353 }
354
355 None
356}
357
358fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId>> {
359 [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS]
360 .into_iter()
361 .find_map(|ns| resolutions.get(&(Symbol::intern(path), ns)).copied().flatten())
362}
363
364fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
366 offset_iter: &mut OffsetIter<'input, F>,
367) -> LinkData {
368 let mut resolvable_link = None;
369 let mut resolvable_link_range = None;
370 let mut display_link = String::new();
371 let mut is_resolvable = true;
372
373 for (event, range) in offset_iter.by_ref() {
374 match event {
375 Event::Text(code) => {
376 let code = code.to_string();
377 display_link.push_str(&code);
378 resolvable_link = Some(code);
379 resolvable_link_range = Some(range);
380 }
381 Event::Code(code) => {
382 let code = code.to_string();
383 display_link.push('`');
384 display_link.push_str(&code);
385 display_link.push('`');
386 resolvable_link = Some(code);
387 resolvable_link_range = Some(range);
388 }
389 Event::Start(_) => {
390 is_resolvable = false;
393 }
394 Event::End(_) => {
395 break;
396 }
397 _ => {}
398 }
399 }
400
401 if !is_resolvable {
402 resolvable_link_range = None;
403 resolvable_link = None;
404 }
405
406 LinkData { resolvable_link, resolvable_link_range, display_link }
407}
408
409fn offset_explicit_range(md: &str, link_range: Range<usize>, open: u8, close: u8) -> Range<usize> {
410 let mut open_brace = !0;
411 let mut close_brace = !0;
412 for (i, b) in md.as_bytes()[link_range.clone()].iter().copied().enumerate().rev() {
413 let i = i + link_range.start;
414 if b == close {
415 close_brace = i;
416 break;
417 }
418 }
419
420 if close_brace < link_range.start || close_brace >= link_range.end {
421 return link_range;
422 }
423
424 let mut nesting = 1;
425
426 for (i, b) in md.as_bytes()[link_range.start..close_brace].iter().copied().enumerate().rev() {
427 let i = i + link_range.start;
428 if b == close {
429 nesting += 1;
430 }
431 if b == open {
432 nesting -= 1;
433 }
434 if nesting == 0 {
435 open_brace = i;
436 break;
437 }
438 }
439
440 assert!(open_brace != close_brace);
441
442 if open_brace < link_range.start || open_brace >= link_range.end {
443 return link_range;
444 }
445 (open_brace + 1)..close_brace
447}
448
449fn offset_reference_def_range(
450 md: &str,
451 dest: &CowStr<'_>,
452 link_range: Range<usize>,
453) -> Range<usize> {
454 match dest {
459 CowStr::Borrowed(s) => {
464 unsafe {
466 let s_start = dest.as_ptr();
467 let s_end = s_start.add(s.len());
468 let md_start = md.as_ptr();
469 let md_end = md_start.add(md.len());
470 if md_start <= s_start && s_end <= md_end {
471 let start = s_start.offset_from(md_start) as usize;
472 let end = s_end.offset_from(md_start) as usize;
473 start..end
474 } else {
475 link_range
476 }
477 }
478 }
479
480 CowStr::Boxed(_) | CowStr::Inlined(_) => link_range,
482 }
483}