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