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 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 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
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 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 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
165 {
166 Some((sp, from_expansion)) => {
167 if from_expansion {
168 return None;
169 }
170 sp
171 }
172 None => item.attr_span(cx.tcx),
173 };
174 let (explicit_span, false) = source_span_for_markdown_range(
175 cx.tcx,
176 doc,
177 &offset_explicit_range(doc, link_range, open, close),
178 &item.attrs.doc_strings,
179 )?
180 else {
181 return None;
183 };
184 let (display_span, false) = source_span_for_markdown_range(
185 cx.tcx,
186 doc,
187 resolvable_link_range,
188 &item.attrs.doc_strings,
189 )?
190 else {
191 return None;
193 };
194
195 cx.tcx.node_span_lint(crate::lint::REDUNDANT_EXPLICIT_LINKS, hir_id, explicit_span, |lint| {
196 lint.primary_message("redundant explicit link target")
197 .span_label(explicit_span, "explicit target is redundant")
198 .span_label(display_span, "because label contains path that resolves to same destination")
199 .note("when a link's destination is not specified,\nthe label is used to resolve intra-doc links")
200 .span_suggestion_with_style(link_span, "remove explicit link target", format!("[{}]", link_data.display_link), Applicability::MaybeIncorrect, SuggestionStyle::ShowAlways);
201 });
202 }
203
204 None
205}
206
207fn check_reference_redundancy(
209 cx: &DocContext<'_>,
210 item: &Item,
211 hir_id: HirId,
212 doc: &str,
213 resolutions: &DocLinkResMap,
214 link_range: Range<usize>,
215 dest: &CowStr<'_>,
216 link_data: LinkData,
217) -> Option<()> {
218 let (resolvable_link, resolvable_link_range) =
219 (&link_data.resolvable_link?, &link_data.resolvable_link_range?);
220 let (dest_res, display_res) =
221 (find_resolution(resolutions, dest)?, find_resolution(resolutions, resolvable_link)?);
222
223 if dest_res == display_res {
224 let link_span =
225 match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings)
226 {
227 Some((sp, from_expansion)) => {
228 if from_expansion {
229 return None;
230 }
231 sp
232 }
233 None => item.attr_span(cx.tcx),
234 };
235 let (explicit_span, false) = source_span_for_markdown_range(
236 cx.tcx,
237 doc,
238 &offset_explicit_range(doc, link_range.clone(), b'[', b']'),
239 &item.attrs.doc_strings,
240 )?
241 else {
242 return None;
244 };
245 let (display_span, false) = source_span_for_markdown_range(
246 cx.tcx,
247 doc,
248 resolvable_link_range,
249 &item.attrs.doc_strings,
250 )?
251 else {
252 return None;
254 };
255 let (def_span, _) = source_span_for_markdown_range(
256 cx.tcx,
257 doc,
258 &offset_reference_def_range(doc, dest, link_range),
259 &item.attrs.doc_strings,
260 )?;
261
262 cx.tcx.node_span_lint(crate::lint::REDUNDANT_EXPLICIT_LINKS, hir_id, explicit_span, |lint| {
263 lint.primary_message("redundant explicit link target")
264 .span_label(explicit_span, "explicit target is redundant")
265 .span_label(display_span, "because label contains path that resolves to same destination")
266 .span_note(def_span, "referenced explicit link target defined here")
267 .note("when a link's destination is not specified,\nthe label is used to resolve intra-doc links")
268 .span_suggestion_with_style(link_span, "remove explicit link target", format!("[{}]", link_data.display_link), Applicability::MaybeIncorrect, SuggestionStyle::ShowAlways);
269 });
270 }
271
272 None
273}
274
275fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option<Res<NodeId>> {
276 [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS]
277 .into_iter()
278 .find_map(|ns| resolutions.get(&(Symbol::intern(path), ns)).copied().flatten())
279}
280
281fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
283 offset_iter: &mut OffsetIter<'input, F>,
284) -> LinkData {
285 let mut resolvable_link = None;
286 let mut resolvable_link_range = None;
287 let mut display_link = String::new();
288 let mut is_resolvable = true;
289
290 for (event, range) in offset_iter.by_ref() {
291 match event {
292 Event::Text(code) => {
293 let code = code.to_string();
294 display_link.push_str(&code);
295 resolvable_link = Some(code);
296 resolvable_link_range = Some(range);
297 }
298 Event::Code(code) => {
299 let code = code.to_string();
300 display_link.push('`');
301 display_link.push_str(&code);
302 display_link.push('`');
303 resolvable_link = Some(code);
304 resolvable_link_range = Some(range);
305 }
306 Event::Start(_) => {
307 is_resolvable = false;
310 }
311 Event::End(_) => {
312 break;
313 }
314 _ => {}
315 }
316 }
317
318 if !is_resolvable {
319 resolvable_link_range = None;
320 resolvable_link = None;
321 }
322
323 LinkData { resolvable_link, resolvable_link_range, display_link }
324}
325
326fn offset_explicit_range(md: &str, link_range: Range<usize>, open: u8, close: u8) -> Range<usize> {
327 let mut open_brace = !0;
328 let mut close_brace = !0;
329 for (i, b) in md.as_bytes()[link_range.clone()].iter().copied().enumerate().rev() {
330 let i = i + link_range.start;
331 if b == close {
332 close_brace = i;
333 break;
334 }
335 }
336
337 if close_brace < link_range.start || close_brace >= link_range.end {
338 return link_range;
339 }
340
341 let mut nesting = 1;
342
343 for (i, b) in md.as_bytes()[link_range.start..close_brace].iter().copied().enumerate().rev() {
344 let i = i + link_range.start;
345 if b == close {
346 nesting += 1;
347 }
348 if b == open {
349 nesting -= 1;
350 }
351 if nesting == 0 {
352 open_brace = i;
353 break;
354 }
355 }
356
357 assert!(open_brace != close_brace);
358
359 if open_brace < link_range.start || open_brace >= link_range.end {
360 return link_range;
361 }
362 (open_brace + 1)..close_brace
364}
365
366fn offset_reference_def_range(
367 md: &str,
368 dest: &CowStr<'_>,
369 link_range: Range<usize>,
370) -> Range<usize> {
371 match dest {
376 CowStr::Borrowed(s) => {
381 unsafe {
383 let s_start = dest.as_ptr();
384 let s_end = s_start.add(s.len());
385 let md_start = md.as_ptr();
386 let md_end = md_start.add(md.len());
387 if md_start <= s_start && s_end <= md_end {
388 let start = s_start.offset_from(md_start) as usize;
389 let end = s_end.offset_from(md_start) as usize;
390 start..end
391 } else {
392 link_range
393 }
394 }
395 }
396
397 CowStr::Boxed(_) | CowStr::Inlined(_) => link_range,
399 }
400}