1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use rustc_hir::HirId;
use rustc_middle::mir::visit::Visitor;
use rustc_middle::mir::*;
use rustc_middle::ty::TyCtxt;
use rustc_session::lint::builtin::CONST_ITEM_MUTATION;
use rustc_span::def_id::DefId;
use rustc_span::Span;

use crate::{errors, MirLint};

pub struct CheckConstItemMutation;

impl<'tcx> MirLint<'tcx> for CheckConstItemMutation {
    fn run_lint(&self, tcx: TyCtxt<'tcx>, body: &Body<'tcx>) {
        let mut checker = ConstMutationChecker { body, tcx, target_local: None };
        checker.visit_body(&body);
    }
}

struct ConstMutationChecker<'a, 'tcx> {
    body: &'a Body<'tcx>,
    tcx: TyCtxt<'tcx>,
    target_local: Option<Local>,
}

impl<'tcx> ConstMutationChecker<'_, 'tcx> {
    fn is_const_item(&self, local: Local) -> Option<DefId> {
        if let LocalInfo::ConstRef { def_id } = *self.body.local_decls[local].local_info() {
            Some(def_id)
        } else {
            None
        }
    }

    fn is_const_item_without_destructor(&self, local: Local) -> Option<DefId> {
        let def_id = self.is_const_item(local)?;

        // We avoid linting mutation of a const item if the const's type has a
        // Drop impl. The Drop logic observes the mutation which was performed.
        //
        //     pub struct Log { msg: &'static str }
        //     pub const LOG: Log = Log { msg: "" };
        //     impl Drop for Log {
        //         fn drop(&mut self) { println!("{}", self.msg); }
        //     }
        //
        //     LOG.msg = "wow";  // prints "wow"
        //
        // FIXME(https://github.com/rust-lang/rust/issues/77425):
        // Drop this exception once there is a stable attribute to suppress the
        // const item mutation lint for a single specific const only. Something
        // equivalent to:
        //
        //     #[const_mutation_allowed]
        //     pub const LOG: Log = Log { msg: "" };
        match self.tcx.calculate_dtor(def_id, |_, _| Ok(())) {
            Some(_) => None,
            None => Some(def_id),
        }
    }

    /// If we should lint on this usage, return the [`HirId`], source [`Span`]
    /// and [`Span`] of the const item to use in the lint.
    fn should_lint_const_item_usage(
        &self,
        place: &Place<'tcx>,
        const_item: DefId,
        location: Location,
    ) -> Option<(HirId, Span, Span)> {
        // Don't lint on borrowing/assigning when a dereference is involved.
        // If we 'leave' the temporary via a dereference, we must
        // be modifying something else
        //
        // `unsafe { *FOO = 0; *BAR.field = 1; }`
        // `unsafe { &mut *FOO }`
        // `unsafe { (*ARRAY)[0] = val; }`
        if !place.projection.iter().any(|p| matches!(p, PlaceElem::Deref)) {
            let source_info = self.body.source_info(location);
            let lint_root = self.body.source_scopes[source_info.scope]
                .local_data
                .as_ref()
                .assert_crate_local()
                .lint_root;

            Some((lint_root, source_info.span, self.tcx.def_span(const_item)))
        } else {
            None
        }
    }
}

impl<'tcx> Visitor<'tcx> for ConstMutationChecker<'_, 'tcx> {
    fn visit_statement(&mut self, stmt: &Statement<'tcx>, loc: Location) {
        if let StatementKind::Assign(box (lhs, _)) = &stmt.kind {
            // Check for assignment to fields of a constant
            // Assigning directly to a constant (e.g. `FOO = true;`) is a hard error,
            // so emitting a lint would be redundant.
            if !lhs.projection.is_empty() {
                if let Some(def_id) = self.is_const_item_without_destructor(lhs.local)
                    && let Some((lint_root, span, item)) = self.should_lint_const_item_usage(&lhs, def_id, loc) {
                        self.tcx.emit_spanned_lint(
                            CONST_ITEM_MUTATION,
                            lint_root,
                            span,
                            errors::ConstMutate::Modify { konst: item }
                        );
                }
            }
            // We are looking for MIR of the form:
            //
            // ```
            // _1 = const FOO;
            // _2 = &mut _1;
            // method_call(_2, ..)
            // ```
            //
            // Record our current LHS, so that we can detect this
            // pattern in `visit_rvalue`
            self.target_local = lhs.as_local();
        }
        self.super_statement(stmt, loc);
        self.target_local = None;
    }
    fn visit_rvalue(&mut self, rvalue: &Rvalue<'tcx>, loc: Location) {
        if let Rvalue::Ref(_, BorrowKind::Mut { .. }, place) = rvalue {
            let local = place.local;
            if let Some(def_id) = self.is_const_item(local) {
                // If this Rvalue is being used as the right-hand side of a
                // `StatementKind::Assign`, see if it ends up getting used as
                // the `self` parameter of a method call (as the terminator of our current
                // BasicBlock). If so, we emit a more specific lint.
                let method_did = self.target_local.and_then(|target_local| {
                    rustc_middle::util::find_self_call(
                        self.tcx,
                        &self.body,
                        target_local,
                        loc.block,
                    )
                });
                let lint_loc =
                    if method_did.is_some() { self.body.terminator_loc(loc.block) } else { loc };

                let method_call = if let Some((method_did, _)) = method_did {
                    Some(self.tcx.def_span(method_did))
                } else {
                    None
                };
                if let Some((lint_root, span, item)) =
                    self.should_lint_const_item_usage(place, def_id, lint_loc)
                {
                    self.tcx.emit_spanned_lint(
                        CONST_ITEM_MUTATION,
                        lint_root,
                        span,
                        errors::ConstMutate::MutBorrow { method_call, konst: item },
                    );
                }
            }
        }
        self.super_rvalue(rvalue, loc);
    }
}