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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
mod utils;
use self::utils::*;
use ::{ROUTE_ATTR, ROUTE_INFO_ATTR};
use std::mem::transmute;
use std::collections::HashMap;
use rustc::lint::{Level, LateContext, LintPass, LateLintPass, LintArray};
use rustc::hir::{Item, Expr, Crate, Decl, FnDecl, Body, QPath, PatKind};
use rustc::hir::def_id::DefId;
use rustc::ty::Ty;
use rustc::hir::intravisit::{FnKind};
use rustc::hir::Decl_::*;
use rustc::hir::Expr_::*;
use syntax_pos::Span;
use syntax::symbol::Symbol as Name;
use syntax::ast::NodeId;
const STATE_TYPE: &'static [&'static str] = &["rocket", "request", "state", "State"];
// Information about a specific Rocket instance.
#[derive(Debug, Default)]
struct InstanceInfo {
// Mapping from mounted struct info to the span of the mounted call.
mounted: HashMap<DefId, Span>,
// Mapping from managed types to the span of the manage call.
managed: HashMap<Ty<'static>, Span>,
}
/// A `Receiver` captures the "receiver" of a Rocket instance method call. A
/// Receiver can be an existing instance of Rocket or a call to an Rocket
/// initialization function.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum Receiver {
Instance(DefId, Span),
Call(NodeId, Span),
}
impl Receiver {
/// Returns the span associated with the receiver.
pub fn span(&self) -> Span {
match *self {
Receiver::Instance(_, sp) | Receiver::Call(_, sp) => sp
}
}
}
#[derive(Debug, Default)]
pub struct RocketLint {
// All of the types that were requested as managed state.
// (fn_name, fn_span, info_struct_def_id, req_type, req_param_span)
requested: Vec<(Name, Span, DefId, Ty<'static>, Span)>,
// Mapping from a `Rocket` instance initialization call span (an ignite or
// custom call) to the collected info about that instance.
instances: HashMap<Option<Receiver>, InstanceInfo>,
// Map of all route info structure names found in the program to its defid.
// This is used to map a declared route to its info structure defid.
info_structs: HashMap<Name, DefId>,
// The name, span, and info DefId for all route functions found. The DefId
// is obtained by indexing into info_structs with the name found in the
// attribute that Rocket generates.
declared: Vec<(Name, Span, DefId)>,
// Mapping from known named Rocket instances to initial receiver. This is
// used to do a sort-of flow-based analysis. We track variable declarations
// and link calls to Rocket methods to the (as best as we can tell) initial
// call to generate that Rocket instance. We use this to group calls to
// `manage` and `mount` to give more accurate warnings.
instance_vars: HashMap<DefId, Receiver>,
}
declare_lint!(UNMOUNTED_ROUTE, Warn, "Warn on routes that are unmounted.");
declare_lint!(UNMANAGED_STATE, Warn, "Warn on declared use of unmanaged state.");
impl<'tcx> LintPass for RocketLint {
fn get_lints(&self) -> LintArray {
lint_array!(UNMANAGED_STATE, UNMOUNTED_ROUTE)
}
}
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for RocketLint {
// This fills out the `instance_vars` table by tracking calls to
// function/methods that create Rocket instances. If the call is a method
// call with a receiver that we know is a Rocket instance, then we know it's
// been moved, and we track that move by linking all definition to the same
// receiver.
fn check_decl(&mut self, cx: &LateContext<'a, 'tcx>, decl: &'tcx Decl) {
// We only care about local declarations...everything else seems very
// unlikely. This is imperfect, after all.
if let DeclLocal(ref local) = decl.node {
// Retrieve the def_id for the new binding.
let new_def_id = match local.pat.node {
PatKind::Binding(_, def_id, ..) => def_id ,
_ => return
};
// `init` is the RHS of the declaration.
if let Some(ref init) = local.init {
// We only care about declarations that result in Rocket insts.
if !returns_rocket_instance(cx, init) {
return;
}
let (expr, span) = match find_initial_receiver(cx, init) {
Some(expr) => (expr, expr.span),
None => return
};
// If the receiver is a path, check if this path was declared
// before by another binding and use that binding's receiver as
// this binding's receiver, essentially taking us back in time.
// If we don't know about it, just insert a new receiver.
if let ExprPath(QPath::Resolved(_, ref path)) = expr.node {
if let Some(old_def_id) = path.def.def_id_opt() {
if let Some(&prev) = self.instance_vars.get(&old_def_id) {
self.instance_vars.insert(new_def_id, prev);
} else {
let recvr = Receiver::Instance(old_def_id, span);
self.instance_vars.insert(new_def_id, recvr);
}
}
}
// We use a call as a base case. Maybe it's a brand new Rocket
// instance, maybe it's a function returning a Rocket instance.
// Who knows. This is where imperfection comes in. We're just
// going to assume that calls to `mount` and `manage` are
// grouped with their originating call.
if let ExprCall(ref expr, ..) = expr.node {
let recvr = Receiver::Call(expr.id, span);
self.instance_vars.insert(new_def_id, recvr);
}
}
}
}
// Here, we collect all of the calls to `manage` and `mount` by instance,
// where the instance is determined by the receiver of the call. We look up
// the receiver in the type table we've constructed. If it's there, we use
// it, if not, we use the call as the receiver.
fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
/// Fetches the top-level `Receiver` instance given that a method call
/// was made to the receiver `rexpr`. Top-level here means "the
/// original". We search the `instance_vars` table to retrieve it.
let instance_for = |lint: &mut RocketLint, rexpr: &Expr| -> Option<Receiver> {
match rexpr.node {
ExprPath(QPath::Resolved(_, ref p)) => {
p.def.def_id_opt()
.and_then(|id| lint.instance_vars.get(&id))
.map(|recvr| recvr.clone())
}
ExprCall(ref c, ..) => Some(Receiver::Call(c.id, rexpr.span)),
_ => unreachable!()
}
};
if let Some((recvr, args)) = rocket_method_call("manage", cx, expr) {
let managed_val = &args[0];
let instance = recvr.and_then(|r| instance_for(self, r));
if let Some(managed_ty) = cx.tables.expr_ty_opt(managed_val) {
self.instances.entry(instance)
.or_insert_with(|| InstanceInfo::default())
.managed
.insert(unsafe { transmute(managed_ty) }, managed_val.span);
}
}
if let Some((recvr, args)) = rocket_method_call("mount", cx, expr) {
let instance = recvr.and_then(|r| instance_for(self, r));
for def_id in extract_mount_fn_def_ids(cx, &args[1]) {
self.instances.entry(instance)
.or_insert_with(|| InstanceInfo::default())
.mounted
.insert(def_id, expr.span);
}
}
// This captures a corner case where neither `manage` nor `mount` is
// called on an instance.
if let Some((Some(recvr_expr), _)) = rocket_method_call("launch", cx, expr) {
if let Some(instance) = instance_for(self, recvr_expr) {
self.instances.entry(Some(instance))
.or_insert_with(|| InstanceInfo::default());
}
}
}
// We collect all of the names and defids for the info structures that
// Rocket has generated. We do this by simply looking at the attribute,
// which Rocket's codegen was kind enough to generate.
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item) {
// Return early if this is not a route info structure.
if !item.attrs.iter().any(|attr| attr.check_name(ROUTE_INFO_ATTR)) {
return;
}
if let Some(def_id) = cx.tcx.hir.opt_local_def_id(item.id) {
self.info_structs.insert(item.name, def_id);
}
}
/// We do two things here: 1) we find all of the `State` request guards a
/// user wants, and 2) we find all of the routes declared by the user. We
/// determine that a function is a route by looking for the attribute that
/// Rocket declared. We tie the route to the info structure, obtained from
/// the `check_item` call, so that we can determine if the route was mounted
/// or not. The tie is done by looking at the name of the info structure in
/// the attribute that Rocket generated and then looking up the structure in
/// the `info_structs` map. The structure _must_ be there since Rocket
/// always generates the structure before the route.
fn check_fn(&mut self,
cx: &LateContext<'a, 'tcx>,
kind: FnKind<'tcx>,
decl: &'tcx FnDecl,
_: &'tcx Body,
fn_sp: Span,
fn_id: NodeId) {
// Get the name of the function, if any.
let fn_name = match kind {
FnKind::ItemFn(name, ..) => name,
_ => return
};
// Figure out if this is a route function by trying to find the
// `ROUTE_ATTR` attribute and extracing the info struct's name from it.
let attr_value = kind.attrs().iter().filter_map(|attr| {
match attr.check_name(ROUTE_ATTR) {
false => None,
true => attr.meta_item_list().and_then(|list| list[0].name())
}
}).next();
// Try to get the DEF_ID using the info struct's name from the
// `info_structs` map. Return early if anything goes awry.
let def_id = match attr_value {
Some(val) if self.info_structs.contains_key(&val) => {
self.info_structs.get(&val).unwrap()
}
_ => return
};
// Add this to the list of declared routes to check for mounting later
// unless unmounted routes were explicitly allowed for this function.
if cx.tcx.lint_level_at_node(UNMOUNTED_ROUTE, fn_id).0 != Level::Allow {
self.declared.push((fn_name, fn_sp, def_id.clone()));
}
// If unmanaged state was explicitly allowed for this function, don't
// record any additional information. Just return now.
if cx.tcx.lint_level_at_node(UNMANAGED_STATE, fn_id).0 == Level::Allow {
return;
}
// Collect all of the `State` types and spans into `tys` and `spans`.
let mut ty_and_spans: Vec<(Ty<'static>, Span)> = vec![];
if let Some(sig) = cx.tables.liberated_fn_sigs.get(&fn_id) {
for (i, input_ty) in sig.inputs().iter().enumerate() {
let def_id = match input_ty.ty_to_def_id() {
Some(id) => id,
None => continue
};
if match_def_path(cx.tcx, def_id, STATE_TYPE) {
if let Some(inner_type) = input_ty.walk_shallow().next() {
if decl.inputs.len() <= i {
println!("internal lint error: \
signature and declaration length mismatch: \
{:?}, {:?}", sig.inputs(), decl.inputs);
println!("this is likely a bug. please report this.");
continue;
}
let ty = unsafe { transmute(inner_type) };
let span = decl.inputs[i].span;
ty_and_spans.push((ty, span));
}
}
}
}
// Insert the information we've collected.
for (ty, span) in ty_and_spans {
self.requested.push((fn_name, fn_sp, def_id.clone(), ty, span));
}
}
fn check_crate_post(&mut self, cx: &LateContext<'a, 'tcx>, _: &'tcx Crate) {
// Iterate through all the instances, emitting warnings.
for (instance, info) in self.instances.iter() {
self.unmounted_warnings(cx, *instance, info);
self.unmanaged_warnings(cx, *instance, info);
}
}
}
impl RocketLint {
fn unmounted_warnings(&self, cx: &LateContext,
rcvr: Option<Receiver>,
info: &InstanceInfo) {
// Emit a warning for all unmounted, declared routes.
for &(route_name, fn_sp, info_def_id) in self.declared.iter() {
if !info.mounted.contains_key(&info_def_id) {
let help_span = rcvr.map(|r| r.span());
msg_and_help(cx, UNMOUNTED_ROUTE, fn_sp,
&format!("the '{}' route is not mounted", route_name),
"Rocket will not dispatch requests to unmounted routes.",
help_span, "maybe add a call to `mount` here?");
}
}
}
fn unmanaged_warnings(&self,
cx: &LateContext,
rcvr: Option<Receiver>,
info: &InstanceInfo) {
for &(_, _, info_def_id, ty, sp) in self.requested.iter() {
// Don't warn on unmounted routes.
if !info.mounted.contains_key(&info_def_id) { continue }
if !info.managed.contains_key(&ty) {
let help_span = rcvr.map(|r| r.span());
msg_and_help(cx, UNMANAGED_STATE, sp,
&format!("'{}' is not currently being managed by Rocket", ty),
"this 'State' request guard will always fail",
help_span, "maybe add a call to `manage` here?");
}
}
}
}