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