rustc_lint/non_local_def.rs
1use rustc_errors::MultiSpan;
2use rustc_hir::def::{DefKind, Res};
3use rustc_hir::intravisit::{self, Visitor, VisitorExt};
4use rustc_hir::{Body, HirId, Item, ItemKind, Node, Path, TyKind};
5use rustc_middle::ty::TyCtxt;
6use rustc_session::{declare_lint, impl_lint_pass};
7use rustc_span::def_id::{DefId, LOCAL_CRATE};
8use rustc_span::{ExpnKind, MacroKind, Span, kw, sym};
9
10use crate::lints::{NonLocalDefinitionsCargoUpdateNote, NonLocalDefinitionsDiag};
11use crate::{LateContext, LateLintPass, LintContext, fluent_generated as fluent};
12
13declare_lint! {
14 /// The `non_local_definitions` lint checks for `impl` blocks and `#[macro_export]`
15 /// macro inside bodies (functions, enum discriminant, ...).
16 ///
17 /// ### Example
18 ///
19 /// ```rust
20 /// #![warn(non_local_definitions)]
21 /// trait MyTrait {}
22 /// struct MyStruct;
23 ///
24 /// fn foo() {
25 /// impl MyTrait for MyStruct {}
26 /// }
27 /// ```
28 ///
29 /// {{produces}}
30 ///
31 /// ### Explanation
32 ///
33 /// Creating non-local definitions go against expectation and can create discrepancies
34 /// in tooling. It should be avoided. It may become deny-by-default in edition 2024
35 /// and higher, see the tracking issue <https://github.com/rust-lang/rust/issues/120363>.
36 ///
37 /// An `impl` definition is non-local if it is nested inside an item and neither
38 /// the type nor the trait are at the same nesting level as the `impl` block.
39 ///
40 /// All nested bodies (functions, enum discriminant, array length, consts) (expect for
41 /// `const _: Ty = { ... }` in top-level module, which is still undecided) are checked.
42 pub NON_LOCAL_DEFINITIONS,
43 Warn,
44 "checks for non-local definitions",
45 report_in_external_macro
46}
47
48#[derive(Default)]
49pub(crate) struct NonLocalDefinitions {
50 body_depth: u32,
51}
52
53impl_lint_pass!(NonLocalDefinitions => [NON_LOCAL_DEFINITIONS]);
54
55// FIXME(Urgau): Figure out how to handle modules nested in bodies.
56// It's currently not handled by the current logic because modules are not bodies.
57// They don't even follow the correct order (check_body -> check_mod -> check_body_post)
58// instead check_mod is called after every body has been handled.
59
60impl<'tcx> LateLintPass<'tcx> for NonLocalDefinitions {
61 fn check_body(&mut self, _cx: &LateContext<'tcx>, _body: &Body<'tcx>) {
62 self.body_depth += 1;
63 }
64
65 fn check_body_post(&mut self, _cx: &LateContext<'tcx>, _body: &Body<'tcx>) {
66 self.body_depth -= 1;
67 }
68
69 fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
70 if self.body_depth == 0 {
71 return;
72 }
73
74 let def_id = item.owner_id.def_id.into();
75 let parent = cx.tcx.parent(def_id);
76 let parent_def_kind = cx.tcx.def_kind(parent);
77 let parent_opt_item_name = cx.tcx.opt_item_name(parent);
78
79 // Per RFC we (currently) ignore anon-const (`const _: Ty = ...`) in top-level module.
80 if self.body_depth == 1
81 && parent_def_kind == DefKind::Const
82 && parent_opt_item_name == Some(kw::Underscore)
83 {
84 return;
85 }
86
87 let cargo_update = || {
88 let oexpn = item.span.ctxt().outer_expn_data();
89 if let Some(def_id) = oexpn.macro_def_id
90 && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind
91 && def_id.krate != LOCAL_CRATE
92 && rustc_session::utils::was_invoked_from_cargo()
93 {
94 Some(NonLocalDefinitionsCargoUpdateNote {
95 macro_kind: macro_kind.descr(),
96 macro_name,
97 crate_name: cx.tcx.crate_name(def_id.krate),
98 })
99 } else {
100 None
101 }
102 };
103
104 // determining if we are in a doctest context can't currently be determined
105 // by the code itself (there are no specific attributes), but fortunately rustdoc
106 // sets a perma-unstable env var for libtest so we just reuse that for now
107 let is_at_toplevel_doctest =
108 || self.body_depth == 2 && std::env::var("UNSTABLE_RUSTDOC_TEST_PATH").is_ok();
109
110 match item.kind {
111 ItemKind::Impl(impl_) => {
112 // The RFC states:
113 //
114 // > An item nested inside an expression-containing item (through any
115 // > level of nesting) may not define an impl Trait for Type unless
116 // > either the **Trait** or the **Type** is also nested inside the
117 // > same expression-containing item.
118 //
119 // To achieve this we get try to get the paths of the _Trait_ and
120 // _Type_, and we look inside those paths to try a find in one
121 // of them a type whose parent is the same as the impl definition.
122 //
123 // If that's the case this means that this impl block declaration
124 // is using local items and so we don't lint on it.
125
126 // 1. We collect all the `hir::Path` from the `Self` type and `Trait` ref
127 // of the `impl` definition
128 let mut collector = PathCollector { paths: Vec::new() };
129 collector.visit_ty_unambig(&impl_.self_ty);
130 if let Some(of_trait) = &impl_.of_trait {
131 collector.visit_trait_ref(of_trait);
132 }
133
134 // 1.5. Remove any path that doesn't resolve to a `DefId` or if it resolve to a
135 // type-param (e.g. `T`).
136 collector.paths.retain(
137 |p| matches!(p.res, Res::Def(def_kind, _) if def_kind != DefKind::TyParam),
138 );
139
140 // 1.9. We retrieve the parent def id of the impl item, ...
141 //
142 // ... modulo const-anons items, for enhanced compatibility with the ecosystem
143 // as that pattern is common with `serde`, `bevy`, ...
144 //
145 // For this example we want the `DefId` parent of the outermost const-anon items.
146 // ```
147 // const _: () = { // the parent of this const-anon
148 // const _: () = {
149 // impl Foo {}
150 // };
151 // };
152 // ```
153 //
154 // It isn't possible to mix a impl in a module with const-anon, but an item can
155 // be put inside a module and referenced by a impl so we also have to treat the
156 // item parent as transparent to module and for consistency we have to do the same
157 // for impl, otherwise the item-def and impl-def won't have the same parent.
158 let outermost_impl_parent = peel_parent_while(cx.tcx, parent, |tcx, did| {
159 tcx.def_kind(did) == DefKind::Mod
160 || (tcx.def_kind(did) == DefKind::Const
161 && tcx.opt_item_name(did) == Some(kw::Underscore))
162 });
163
164 // 2. We check if any of the paths reference a the `impl`-parent.
165 //
166 // If that the case we bail out, as was asked by T-lang, even though this isn't
167 // correct from a type-system point of view, as inference exists and one-impl-rule
168 // make its so that we could still leak the impl.
169 if collector
170 .paths
171 .iter()
172 .any(|path| path_has_local_parent(path, cx, parent, outermost_impl_parent))
173 {
174 return;
175 }
176
177 // Get the span of the parent const item ident (if it's a not a const anon).
178 //
179 // Used to suggest changing the const item to a const anon.
180 let span_for_const_anon_suggestion = if parent_def_kind == DefKind::Const
181 && parent_opt_item_name != Some(kw::Underscore)
182 && let Some(parent) = parent.as_local()
183 && let Node::Item(item) = cx.tcx.hir_node_by_def_id(parent)
184 && let ItemKind::Const(ty, _, _) = item.kind
185 && let TyKind::Tup(&[]) = ty.kind
186 {
187 Some(item.ident.span)
188 } else {
189 None
190 };
191
192 let const_anon = matches!(parent_def_kind, DefKind::Const | DefKind::Static { .. })
193 .then_some(span_for_const_anon_suggestion);
194
195 let impl_span = item.span.shrink_to_lo().to(impl_.self_ty.span);
196 let mut ms = MultiSpan::from_span(impl_span);
197
198 for path in &collector.paths {
199 // FIXME: While a translatable diagnostic message can have an argument
200 // we (currently) have no way to set different args per diag msg with
201 // `MultiSpan::push_span_label`.
202 #[allow(rustc::untranslatable_diagnostic)]
203 ms.push_span_label(
204 path_span_without_args(path),
205 format!("`{}` is not local", path_name_to_string(path)),
206 );
207 }
208
209 let doctest = is_at_toplevel_doctest();
210
211 if !doctest {
212 ms.push_span_label(
213 cx.tcx.def_span(parent),
214 fluent::lint_non_local_definitions_impl_move_help,
215 );
216 }
217
218 let macro_to_change =
219 if let ExpnKind::Macro(kind, name) = item.span.ctxt().outer_expn_data().kind {
220 Some((name.to_string(), kind.descr()))
221 } else {
222 None
223 };
224
225 cx.emit_span_lint(
226 NON_LOCAL_DEFINITIONS,
227 ms,
228 NonLocalDefinitionsDiag::Impl {
229 depth: self.body_depth,
230 body_kind_descr: cx.tcx.def_kind_descr(parent_def_kind, parent),
231 body_name: parent_opt_item_name
232 .map(|s| s.to_ident_string())
233 .unwrap_or_else(|| "<unnameable>".to_string()),
234 cargo_update: cargo_update(),
235 const_anon,
236 doctest,
237 macro_to_change,
238 },
239 )
240 }
241 ItemKind::Macro(_macro, MacroKind::Bang)
242 if cx.tcx.has_attr(item.owner_id.def_id, sym::macro_export) =>
243 {
244 cx.emit_span_lint(
245 NON_LOCAL_DEFINITIONS,
246 item.span,
247 NonLocalDefinitionsDiag::MacroRules {
248 depth: self.body_depth,
249 body_kind_descr: cx.tcx.def_kind_descr(parent_def_kind, parent),
250 body_name: parent_opt_item_name
251 .map(|s| s.to_ident_string())
252 .unwrap_or_else(|| "<unnameable>".to_string()),
253 cargo_update: cargo_update(),
254 doctest: is_at_toplevel_doctest(),
255 },
256 )
257 }
258 _ => {}
259 }
260 }
261}
262
263/// Simple hir::Path collector
264struct PathCollector<'tcx> {
265 paths: Vec<Path<'tcx>>,
266}
267
268impl<'tcx> Visitor<'tcx> for PathCollector<'tcx> {
269 fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) {
270 self.paths.push(path.clone()); // need to clone, bc of the restricted lifetime
271 intravisit::walk_path(self, path)
272 }
273}
274
275/// Given a path, this checks if the if the parent resolution def id corresponds to
276/// the def id of the parent impl definition (the direct one and the outermost one).
277///
278/// Given this path, we will look at the path (and ignore any generic args):
279///
280/// ```text
281/// std::convert::PartialEq<Foo<Bar>>
282/// ^^^^^^^^^^^^^^^^^^^^^^^
283/// ```
284#[inline]
285fn path_has_local_parent(
286 path: &Path<'_>,
287 cx: &LateContext<'_>,
288 impl_parent: DefId,
289 outermost_impl_parent: Option<DefId>,
290) -> bool {
291 path.res
292 .opt_def_id()
293 .is_some_and(|did| did_has_local_parent(did, cx.tcx, impl_parent, outermost_impl_parent))
294}
295
296/// Given a def id this checks if the parent def id (modulo modules) correspond to
297/// the def id of the parent impl definition (the direct one and the outermost one).
298#[inline]
299fn did_has_local_parent(
300 did: DefId,
301 tcx: TyCtxt<'_>,
302 impl_parent: DefId,
303 outermost_impl_parent: Option<DefId>,
304) -> bool {
305 if !did.is_local() {
306 return false;
307 }
308
309 let Some(parent_did) = tcx.opt_parent(did) else {
310 return false;
311 };
312
313 peel_parent_while(tcx, parent_did, |tcx, did| {
314 tcx.def_kind(did) == DefKind::Mod
315 || (tcx.def_kind(did) == DefKind::Const
316 && tcx.opt_item_name(did) == Some(kw::Underscore))
317 })
318 .map(|parent_did| parent_did == impl_parent || Some(parent_did) == outermost_impl_parent)
319 .unwrap_or(false)
320}
321
322/// Given a `DefId` checks if it satisfies `f` if it does check with it's parent and continue
323/// until it doesn't satisfies `f` and return the last `DefId` checked.
324///
325/// In other word this method return the first `DefId` that doesn't satisfies `f`.
326#[inline]
327fn peel_parent_while(
328 tcx: TyCtxt<'_>,
329 mut did: DefId,
330 mut f: impl FnMut(TyCtxt<'_>, DefId) -> bool,
331) -> Option<DefId> {
332 while !did.is_crate_root() && f(tcx, did) {
333 did = tcx.opt_parent(did).filter(|parent_did| parent_did.is_local())?;
334 }
335
336 Some(did)
337}
338
339/// Return for a given `Path` the span until the last args
340fn path_span_without_args(path: &Path<'_>) -> Span {
341 if let Some(args) = &path.segments.last().unwrap().args {
342 path.span.until(args.span_ext)
343 } else {
344 path.span
345 }
346}
347
348/// Return a "error message-able" ident for the last segment of the `Path`
349fn path_name_to_string(path: &Path<'_>) -> String {
350 path.segments.last().unwrap().ident.to_string()
351}