1use rustc_errors::{MultiSpan, msg};
2use rustc_hir::def::{DefKind, Res};
3use rustc_hir::intravisit::{self, Visitor, VisitorExt};
4use rustc_hir::{Body, HirId, Item, ItemKind, Node, Path, TyKind, find_attr};
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, Span, kw};
910use crate::lints::{NonLocalDefinitionsCargoUpdateNote, NonLocalDefinitionsDiag};
11use crate::{LateContext, LateLintPass, LintContext};
1213#[doc =
r" The `non_local_definitions` lint checks for `impl` blocks and `#[macro_export]`"]
#[doc = r" macro inside bodies (functions, enum discriminant, ...)."]
#[doc = r""]
#[doc = r" ### Example"]
#[doc = r""]
#[doc = r" ```rust"]
#[doc = r" #![warn(non_local_definitions)]"]
#[doc = r" trait MyTrait {}"]
#[doc = r" struct MyStruct;"]
#[doc = r""]
#[doc = r" fn foo() {"]
#[doc = r" impl MyTrait for MyStruct {}"]
#[doc = r" }"]
#[doc = r" ```"]
#[doc = r""]
#[doc = r" {{produces}}"]
#[doc = r""]
#[doc = r" ### Explanation"]
#[doc = r""]
#[doc =
r" Creating non-local definitions go against expectation and can create discrepancies"]
#[doc =
r" in tooling. It should be avoided. It may become deny-by-default in edition 2024"]
#[doc =
r" and higher, see the tracking issue <https://github.com/rust-lang/rust/issues/120363>."]
#[doc = r""]
#[doc =
r" An `impl` definition is non-local if it is nested inside an item and neither"]
#[doc =
r" the type nor the trait are at the same nesting level as the `impl` block."]
#[doc = r""]
#[doc =
r" All nested bodies (functions, enum discriminant, array length, consts) (expect for"]
#[doc =
r" `const _: Ty = { ... }` in top-level module, which is still undecided) are checked."]
pub static NON_LOCAL_DEFINITIONS: &::rustc_lint_defs::Lint =
&::rustc_lint_defs::Lint {
name: "NON_LOCAL_DEFINITIONS",
default_level: ::rustc_lint_defs::Warn,
desc: "checks for non-local definitions",
is_externally_loaded: false,
report_in_external_macro: true,
..::rustc_lint_defs::Lint::default_fields_for_macro()
};declare_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.
42pub NON_LOCAL_DEFINITIONS,
43 Warn,
44"checks for non-local definitions",
45 report_in_external_macro
46}4748#[derive(#[automatically_derived]
impl ::core::default::Default for NonLocalDefinitions {
#[inline]
fn default() -> NonLocalDefinitions {
NonLocalDefinitions {
body_depth: ::core::default::Default::default(),
}
}
}Default)]
49pub(crate) struct NonLocalDefinitions {
50 body_depth: u32,
51}
5253impl ::rustc_lint_defs::LintPass for NonLocalDefinitions {
fn name(&self) -> &'static str { "NonLocalDefinitions" }
fn get_lints(&self) -> ::rustc_lint_defs::LintVec {
::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[NON_LOCAL_DEFINITIONS]))
}
}
impl NonLocalDefinitions {
#[allow(unused)]
pub fn lint_vec() -> ::rustc_lint_defs::LintVec {
::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[NON_LOCAL_DEFINITIONS]))
}
}impl_lint_pass!(NonLocalDefinitions => [NON_LOCAL_DEFINITIONS]);
5455// 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.
5960impl<'tcx> LateLintPass<'tcx> for NonLocalDefinitions {
61fn check_body(&mut self, _cx: &LateContext<'tcx>, _body: &Body<'tcx>) {
62self.body_depth += 1;
63 }
6465fn check_body_post(&mut self, _cx: &LateContext<'tcx>, _body: &Body<'tcx>) {
66self.body_depth -= 1;
67 }
6869fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
70if self.body_depth == 0 {
71return;
72 }
7374let def_id = item.owner_id.def_id.into();
75let parent = cx.tcx.parent(def_id);
76let parent_def_kind = cx.tcx.def_kind(parent);
77let parent_opt_item_name = cx.tcx.opt_item_name(parent);
7879// Per RFC we (currently) ignore anon-const (`const _: Ty = ...`) in top-level module.
80if self.body_depth == 1
81&& parent_def_kind == DefKind::Const82 && parent_opt_item_name == Some(kw::Underscore)
83 {
84return;
85 }
8687let cargo_update = || {
88let oexpn = item.span.ctxt().outer_expn_data();
89if let Some(def_id) = oexpn.macro_def_id
90 && let ExpnKind::Macro(macro_kind, macro_name) = oexpn.kind
91 && def_id.krate != LOCAL_CRATE92 && rustc_session::utils::was_invoked_from_cargo()
93 {
94Some(NonLocalDefinitionsCargoUpdateNote {
95 macro_kind: macro_kind.descr(),
96macro_name,
97 crate_name: cx.tcx.crate_name(def_id.krate),
98 })
99 } else {
100None101 }
102 };
103104// 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
107let is_at_toplevel_doctest = || {
108self.body_depth == 2
109&& cx.tcx.env_var_os("UNSTABLE_RUSTDOC_TEST_PATH".as_ref()).is_some()
110 };
111112match item.kind {
113 ItemKind::Impl(impl_) => {
114// The RFC states:
115 //
116 // > An item nested inside an expression-containing item (through any
117 // > level of nesting) may not define an impl Trait for Type unless
118 // > either the **Trait** or the **Type** is also nested inside the
119 // > same expression-containing item.
120 //
121 // To achieve this we get try to get the paths of the _Trait_ and
122 // _Type_, and we look inside those paths to try a find in one
123 // of them a type whose parent is the same as the impl definition.
124 //
125 // If that's the case this means that this impl block declaration
126 // is using local items and so we don't lint on it.
127128 // 1. We collect all the `hir::Path` from the `Self` type and `Trait` ref
129 // of the `impl` definition
130let mut collector = PathCollector { paths: Vec::new() };
131collector.visit_ty_unambig(&impl_.self_ty);
132if let Some(of_trait) = impl_.of_trait {
133collector.visit_trait_ref(&of_trait.trait_ref);
134 }
135136// 1.5. Remove any path that doesn't resolve to a `DefId` or if it resolve to a
137 // type-param (e.g. `T`).
138collector.paths.retain(
139 |p| #[allow(non_exhaustive_omitted_patterns)] match p.res {
Res::Def(def_kind, _) if def_kind != DefKind::TyParam => true,
_ => false,
}matches!(p.res, Res::Def(def_kind, _) if def_kind != DefKind::TyParam),
140 );
141142// 1.9. We retrieve the parent def id of the impl item, ...
143 //
144 // ... modulo const-anons items, for enhanced compatibility with the ecosystem
145 // as that pattern is common with `serde`, `bevy`, ...
146 //
147 // For this example we want the `DefId` parent of the outermost const-anon items.
148 // ```
149 // const _: () = { // the parent of this const-anon
150 // const _: () = {
151 // impl Foo {}
152 // };
153 // };
154 // ```
155 //
156 // It isn't possible to mix a impl in a module with const-anon, but an item can
157 // be put inside a module and referenced by a impl so we also have to treat the
158 // item parent as transparent to module and for consistency we have to do the same
159 // for impl, otherwise the item-def and impl-def won't have the same parent.
160let outermost_impl_parent = peel_parent_while(cx.tcx, parent, |tcx, did| {
161tcx.def_kind(did) == DefKind::Mod162 || (tcx.def_kind(did) == DefKind::Const163 && tcx.opt_item_name(did) == Some(kw::Underscore))
164 });
165166// 2. We check if any of the paths reference a the `impl`-parent.
167 //
168 // If that the case we bail out, as was asked by T-lang, even though this isn't
169 // correct from a type-system point of view, as inference exists and one-impl-rule
170 // make its so that we could still leak the impl.
171if collector172 .paths
173 .iter()
174 .any(|path| path_has_local_parent(path, cx, parent, outermost_impl_parent))
175 {
176return;
177 }
178179// Get the span of the parent const item ident (if it's a not a const anon).
180 //
181 // Used to suggest changing the const item to a const anon.
182let span_for_const_anon_suggestion = if parent_def_kind == DefKind::Const183 && parent_opt_item_name != Some(kw::Underscore)
184 && let Some(parent) = parent.as_local()
185 && let Node::Item(item) = cx.tcx.hir_node_by_def_id(parent)
186 && let ItemKind::Const(ident, _, ty, _) = item.kind
187 && let TyKind::Tup(&[]) = ty.kind
188 {
189Some(ident.span)
190 } else {
191None192 };
193194let const_anon = #[allow(non_exhaustive_omitted_patterns)] match parent_def_kind {
DefKind::Const | DefKind::Static { .. } => true,
_ => false,
}matches!(parent_def_kind, DefKind::Const | DefKind::Static { .. })195 .then_some(span_for_const_anon_suggestion);
196197let impl_span = item.span.shrink_to_lo().to(impl_.self_ty.span);
198let mut ms = MultiSpan::from_span(impl_span);
199200for path in &collector.paths {
201 ms.push_span_label(
202 path_span_without_args(path),
203::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}` is not local",
path_name_to_string(path)))
})format!("`{}` is not local", path_name_to_string(path)),
204 );
205 }
206207let doctest = is_at_toplevel_doctest();
208209if !doctest {
210ms.push_span_label(
211cx.tcx.def_span(parent),
212rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed("move the `impl` block outside of this {$body_kind_descr} {$depth ->\n [one] `{$body_name}`\n *[other] `{$body_name}` and up {$depth} bodies\n }"))msg!(
213"move the `impl` block outside of this {$body_kind_descr} {$depth ->
214 [one] `{$body_name}`
215 *[other] `{$body_name}` and up {$depth} bodies
216 }"
217),
218 );
219 }
220221let macro_to_change =
222if let ExpnKind::Macro(kind, name) = item.span.ctxt().outer_expn_data().kind {
223Some((name.to_string(), kind.descr()))
224 } else {
225None226 };
227228cx.emit_span_lint(
229NON_LOCAL_DEFINITIONS,
230ms,
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_name235 .map(|s| s.to_ident_string())
236 .unwrap_or_else(|| "<unnameable>".to_string()),
237 cargo_update: cargo_update(),
238const_anon,
239doctest,
240macro_to_change,
241 },
242 )
243 }
244 ItemKind::Macro(_, _macro, _kinds)
245if {
#[allow(deprecated)]
{
{
'done:
{
for i in cx.tcx.get_all_attrs(item.owner_id.def_id) {
#[allow(unused_imports)]
use rustc_hir::attrs::AttributeKind::*;
let i: &rustc_hir::Attribute = i;
match i {
rustc_hir::Attribute::Parsed(MacroExport { .. }) => {
break 'done Some(());
}
rustc_hir::Attribute::Unparsed(..) =>
{}
#[deny(unreachable_patterns)]
_ => {}
}
}
None
}
}
}
}.is_some()find_attr!(cx.tcx, item.owner_id.def_id, MacroExport { .. }) =>
246 {
247cx.emit_span_lint(
248NON_LOCAL_DEFINITIONS,
249item.span,
250 NonLocalDefinitionsDiag::MacroRules {
251 depth: self.body_depth,
252 body_kind_descr: cx.tcx.def_kind_descr(parent_def_kind, parent),
253 body_name: parent_opt_item_name254 .map(|s| s.to_ident_string())
255 .unwrap_or_else(|| "<unnameable>".to_string()),
256 cargo_update: cargo_update(),
257 doctest: is_at_toplevel_doctest(),
258 },
259 )
260 }
261_ => {}
262 }
263 }
264}
265266/// Simple hir::Path collector
267struct PathCollector<'tcx> {
268 paths: Vec<Path<'tcx>>,
269}
270271impl<'tcx> Visitor<'tcx> for PathCollector<'tcx> {
272fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) {
273self.paths.push(path.clone()); // need to clone, bc of the restricted lifetime
274intravisit::walk_path(self, path)
275 }
276}
277278/// Given a path, this checks if the if the parent resolution def id corresponds to
279/// the def id of the parent impl definition (the direct one and the outermost one).
280///
281/// Given this path, we will look at the path (and ignore any generic args):
282///
283/// ```text
284/// std::convert::PartialEq<Foo<Bar>>
285/// ^^^^^^^^^^^^^^^^^^^^^^^
286/// ```
287#[inline]
288fn path_has_local_parent(
289 path: &Path<'_>,
290 cx: &LateContext<'_>,
291 impl_parent: DefId,
292 outermost_impl_parent: Option<DefId>,
293) -> bool {
294path.res
295 .opt_def_id()
296 .is_some_and(|did| did_has_local_parent(did, cx.tcx, impl_parent, outermost_impl_parent))
297}
298299/// Given a def id this checks if the parent def id (modulo modules) correspond to
300/// the def id of the parent impl definition (the direct one and the outermost one).
301#[inline]
302fn did_has_local_parent(
303 did: DefId,
304 tcx: TyCtxt<'_>,
305 impl_parent: DefId,
306 outermost_impl_parent: Option<DefId>,
307) -> bool {
308if !did.is_local() {
309return false;
310 }
311312let Some(parent_did) = tcx.opt_parent(did) else {
313return false;
314 };
315316peel_parent_while(tcx, parent_did, |tcx, did| {
317tcx.def_kind(did) == DefKind::Mod318 || (tcx.def_kind(did) == DefKind::Const319 && tcx.opt_item_name(did) == Some(kw::Underscore))
320 })
321 .map(|parent_did| parent_did == impl_parent || Some(parent_did) == outermost_impl_parent)
322 .unwrap_or(false)
323}
324325/// Given a `DefId` checks if it satisfies `f` if it does check with it's parent and continue
326/// until it doesn't satisfies `f` and return the last `DefId` checked.
327///
328/// In other word this method return the first `DefId` that doesn't satisfies `f`.
329#[inline]
330fn peel_parent_while(
331 tcx: TyCtxt<'_>,
332mut did: DefId,
333mut f: impl FnMut(TyCtxt<'_>, DefId) -> bool,
334) -> Option<DefId> {
335while !did.is_crate_root() && f(tcx, did) {
336 did = tcx.opt_parent(did).filter(|parent_did| parent_did.is_local())?;
337 }
338339Some(did)
340}
341342/// Return for a given `Path` the span until the last args
343fn path_span_without_args(path: &Path<'_>) -> Span {
344if let Some(args) = &path.segments.last().unwrap().args {
345path.span.until(args.span_ext)
346 } else {
347path.span
348 }
349}
350351/// Return a "error message-able" ident for the last segment of the `Path`
352fn path_name_to_string(path: &Path<'_>) -> String {
353path.segments.last().unwrap().ident.to_string()
354}