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