Skip to main content

rustc_lint/
non_local_def.rs

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};
9
10use crate::lints::{NonLocalDefinitionsCargoUpdateNote, NonLocalDefinitionsDiag};
11use crate::{LateContext, LateLintPass, LintContext};
12
13#[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.
42    pub NON_LOCAL_DEFINITIONS,
43    Warn,
44    "checks for non-local definitions",
45    report_in_external_macro
46}
47
48#[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}
52
53impl ::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]);
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            && #[allow(non_exhaustive_omitted_patterns)] match parent_def_kind {
    DefKind::Const { .. } => true,
    _ => false,
}matches!(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
109                && cx.tcx.env_var_os("UNSTABLE_RUSTDOC_TEST_PATH".as_ref()).is_some()
110        };
111
112        match 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.
127
128                // 1. We collect all the `hir::Path` from the `Self` type and `Trait` ref
129                // of the `impl` definition
130                let mut collector = PathCollector { paths: Vec::new() };
131                collector.visit_ty_unambig(&impl_.self_ty);
132                if let Some(of_trait) = impl_.of_trait {
133                    collector.visit_trait_ref(&of_trait.trait_ref);
134                }
135
136                // 1.5. Remove any path that doesn't resolve to a `DefId` or if it resolve to a
137                // type-param (e.g. `T`).
138                collector.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                );
141
142                // 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.
160                let outermost_impl_parent = peel_parent_while(cx.tcx, parent, |tcx, did| {
161                    tcx.def_kind(did) == DefKind::Mod
162                        || (#[allow(non_exhaustive_omitted_patterns)] match tcx.def_kind(did) {
    DefKind::Const { .. } => true,
    _ => false,
}matches!(tcx.def_kind(did), DefKind::Const { .. })
163                            && tcx.opt_item_name(did) == Some(kw::Underscore))
164                });
165
166                // 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.
171                if collector
172                    .paths
173                    .iter()
174                    .any(|path| path_has_local_parent(path, cx, parent, outermost_impl_parent))
175                {
176                    return;
177                }
178
179                // 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.
182                let span_for_const_anon_suggestion =
183                    if #[allow(non_exhaustive_omitted_patterns)] match parent_def_kind {
    DefKind::Const { .. } => true,
    _ => false,
}matches!(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 =
196                    #[allow(non_exhaustive_omitted_patterns)] match parent_def_kind {
    DefKind::Const { .. } | DefKind::Static { .. } => true,
    _ => false,
}matches!(parent_def_kind, DefKind::Const { .. } | DefKind::Static { .. })
197                        .then_some(span_for_const_anon_suggestion);
198
199                let impl_span = item.span.shrink_to_lo().to(impl_.self_ty.span);
200                let mut ms = MultiSpan::from_span(impl_span);
201
202                for path in &collector.paths {
203                    ms.push_span_label(
204                        path_span_without_args(path),
205                        ::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)),
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                        rustc_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!(
215                            "move the `impl` block outside of this {$body_kind_descr} {$depth ->
216                                [one] `{$body_name}`
217                                *[other] `{$body_name}` and up {$depth} bodies
218                            }"
219                        ),
220                    );
221                }
222
223                let macro_to_change =
224                    if let ExpnKind::Macro(kind, name) = item.span.ctxt().outer_expn_data().kind {
225                        Some((name.to_string(), kind.descr()))
226                    } else {
227                        None
228                    };
229
230                cx.emit_span_lint(
231                    NON_LOCAL_DEFINITIONS,
232                    ms,
233                    NonLocalDefinitionsDiag::Impl {
234                        depth: self.body_depth,
235                        body_kind_descr: cx.tcx.def_kind_descr(parent_def_kind, parent),
236                        body_name: parent_opt_item_name
237                            .map(|s| s.to_ident_string())
238                            .unwrap_or_else(|| "<unnameable>".to_string()),
239                        cargo_update: cargo_update(),
240                        const_anon,
241                        doctest,
242                        macro_to_change,
243                    },
244                )
245            }
246            ItemKind::Macro(_, _macro, _kinds)
247                if {

        #[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 { .. }) =>
248            {
249                cx.emit_span_lint(
250                    NON_LOCAL_DEFINITIONS,
251                    item.span,
252                    NonLocalDefinitionsDiag::MacroRules {
253                        depth: self.body_depth,
254                        body_kind_descr: cx.tcx.def_kind_descr(parent_def_kind, parent),
255                        body_name: parent_opt_item_name
256                            .map(|s| s.to_ident_string())
257                            .unwrap_or_else(|| "<unnameable>".to_string()),
258                        cargo_update: cargo_update(),
259                        doctest: is_at_toplevel_doctest(),
260                    },
261                )
262            }
263            _ => {}
264        }
265    }
266}
267
268/// Simple hir::Path collector
269struct PathCollector<'tcx> {
270    paths: Vec<Path<'tcx>>,
271}
272
273impl<'tcx> Visitor<'tcx> for PathCollector<'tcx> {
274    fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) {
275        self.paths.push(path.clone()); // need to clone, bc of the restricted lifetime
276        intravisit::walk_path(self, path)
277    }
278}
279
280/// Given a path, this checks if the if the parent resolution def id corresponds to
281/// the def id of the parent impl definition (the direct one and the outermost one).
282///
283/// Given this path, we will look at the path (and ignore any generic args):
284///
285/// ```text
286///    std::convert::PartialEq<Foo<Bar>>
287///    ^^^^^^^^^^^^^^^^^^^^^^^
288/// ```
289#[inline]
290fn path_has_local_parent(
291    path: &Path<'_>,
292    cx: &LateContext<'_>,
293    impl_parent: DefId,
294    outermost_impl_parent: Option<DefId>,
295) -> bool {
296    path.res
297        .opt_def_id()
298        .is_some_and(|did| did_has_local_parent(did, cx.tcx, impl_parent, outermost_impl_parent))
299}
300
301/// Given a def id this checks if the parent def id (modulo modules) correspond to
302/// the def id of the parent impl definition (the direct one and the outermost one).
303#[inline]
304fn did_has_local_parent(
305    did: DefId,
306    tcx: TyCtxt<'_>,
307    impl_parent: DefId,
308    outermost_impl_parent: Option<DefId>,
309) -> bool {
310    if !did.is_local() {
311        return false;
312    }
313
314    let Some(parent_did) = tcx.opt_parent(did) else {
315        return false;
316    };
317
318    peel_parent_while(tcx, parent_did, |tcx, did| {
319        tcx.def_kind(did) == DefKind::Mod
320            || (#[allow(non_exhaustive_omitted_patterns)] match tcx.def_kind(did) {
    DefKind::Const { .. } => true,
    _ => false,
}matches!(tcx.def_kind(did), DefKind::Const { .. })
321                && tcx.opt_item_name(did) == Some(kw::Underscore))
322    })
323    .map(|parent_did| parent_did == impl_parent || Some(parent_did) == outermost_impl_parent)
324    .unwrap_or(false)
325}
326
327/// Given a `DefId` checks if it satisfies `f` if it does check with it's parent and continue
328/// until it doesn't satisfies `f` and return the last `DefId` checked.
329///
330/// In other word this method return the first `DefId` that doesn't satisfies `f`.
331#[inline]
332fn peel_parent_while(
333    tcx: TyCtxt<'_>,
334    mut did: DefId,
335    mut f: impl FnMut(TyCtxt<'_>, DefId) -> bool,
336) -> Option<DefId> {
337    while !did.is_crate_root() && f(tcx, did) {
338        did = tcx.opt_parent(did).filter(|parent_did| parent_did.is_local())?;
339    }
340
341    Some(did)
342}
343
344/// Return for a given `Path` the span until the last args
345fn path_span_without_args(path: &Path<'_>) -> Span {
346    if let Some(args) = &path.segments.last().unwrap().args {
347        path.span.until(args.span_ext)
348    } else {
349        path.span
350    }
351}
352
353/// Return a "error message-able" ident for the last segment of the `Path`
354fn path_name_to_string(path: &Path<'_>) -> String {
355    path.segments.last().unwrap().ident.to_string()
356}