-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add lint for reads and writes that depend on evaluation order #1158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
use rustc::hir::def_id::DefId; | ||
use rustc::hir::intravisit::{Visitor, walk_expr}; | ||
use rustc::hir::*; | ||
use rustc::lint::*; | ||
use utils::{get_parent_expr, span_note_and_lint}; | ||
|
||
/// **What it does:** Checks for a read and a write to the same variable where | ||
/// whether the read occurs before or after the write depends on the evaluation | ||
/// order of sub-expressions. | ||
/// | ||
/// **Why is this bad?** It is often confusing to read. In addition, the | ||
/// sub-expression evaluation order for Rust is not well documented. | ||
/// | ||
/// **Known problems:** Code which intentionally depends on the evaluation | ||
/// order, or which is correct for any evaluation order. | ||
/// | ||
/// **Example:** | ||
/// ```rust | ||
/// let mut x = 0; | ||
/// let a = {x = 1; 1} + x; | ||
/// // Unclear whether a is 1 or 2. | ||
/// ``` | ||
declare_lint! { | ||
pub EVAL_ORDER_DEPENDENCE, | ||
Warn, | ||
"whether a variable read occurs before a write depends on sub-expression evaluation order" | ||
} | ||
|
||
#[derive(Copy,Clone)] | ||
pub struct EvalOrderDependence; | ||
|
||
impl LintPass for EvalOrderDependence { | ||
fn get_lints(&self) -> LintArray { | ||
lint_array!(EVAL_ORDER_DEPENDENCE) | ||
} | ||
} | ||
|
||
impl LateLintPass for EvalOrderDependence { | ||
fn check_expr(&mut self, cx: &LateContext, expr: &Expr) { | ||
// Find a write to a local variable. | ||
match expr.node { | ||
ExprAssign(ref lhs, _) | ExprAssignOp(_, ref lhs, _) => { | ||
if let ExprPath(None, ref path) = lhs.node { | ||
if path.segments.len() == 1 { | ||
let var = cx.tcx.expect_def(lhs.id).def_id(); | ||
let mut visitor = ReadVisitor { | ||
cx: cx, | ||
var: var, | ||
write_expr: expr, | ||
last_expr: expr, | ||
}; | ||
check_for_unsequenced_reads(&mut visitor); | ||
} | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
/// Walks up the AST from the the given write expression (`vis.write_expr`) | ||
/// looking for reads to the same variable that are unsequenced relative to the | ||
/// write. | ||
/// | ||
/// This means reads for which there is a common ancestor between the read and | ||
/// the write such that | ||
/// | ||
/// * evaluating the ancestor necessarily evaluates both the read and the write | ||
/// (for example, `&x` and `|| x = 1` don't necessarily evaluate `x`), and | ||
/// | ||
/// * which one is evaluated first depends on the order of sub-expression | ||
/// evaluation. Blocks, `if`s, loops, `match`es, and the short-circuiting | ||
/// logical operators are considered to have a defined evaluation order. | ||
/// | ||
/// When such a read is found, the lint is triggered. | ||
fn check_for_unsequenced_reads(vis: &mut ReadVisitor) { | ||
let map = &vis.cx.tcx.map; | ||
let mut cur_id = vis.write_expr.id; | ||
loop { | ||
let parent_id = map.get_parent_node(cur_id); | ||
if parent_id == cur_id { | ||
break; | ||
} | ||
let parent_node = match map.find(parent_id) { | ||
Some(parent) => parent, | ||
None => break, | ||
}; | ||
|
||
let stop_early = match parent_node { | ||
map::Node::NodeExpr(expr) => check_expr(vis, expr), | ||
map::Node::NodeStmt(stmt) => check_stmt(vis, stmt), | ||
map::Node::NodeItem(_) => { | ||
// We reached the top of the function, stop. | ||
break; | ||
}, | ||
_ => { StopEarly::KeepGoing } | ||
}; | ||
match stop_early { | ||
StopEarly::Stop => break, | ||
StopEarly::KeepGoing => {}, | ||
} | ||
|
||
cur_id = parent_id; | ||
} | ||
} | ||
|
||
/// Whether to stop early for the loop in `check_for_unsequenced_reads`. (If | ||
/// `check_expr` weren't an independent function, this would be unnecessary and | ||
/// we could just use `break`). | ||
enum StopEarly { | ||
KeepGoing, | ||
Stop, | ||
} | ||
|
||
fn check_expr<'v, 't>(vis: & mut ReadVisitor<'v, 't>, expr: &'v Expr) -> StopEarly { | ||
if expr.id == vis.last_expr.id { | ||
return StopEarly::KeepGoing; | ||
} | ||
|
||
match expr.node { | ||
ExprVec(_) | | ||
ExprTup(_) | | ||
ExprMethodCall(_, _, _) | | ||
ExprCall(_, _) | | ||
ExprAssign(_, _) | | ||
ExprIndex(_, _) | | ||
ExprRepeat(_, _) | | ||
ExprStruct(_, _, _) => { | ||
walk_expr(vis, expr); | ||
} | ||
ExprBinary(op, _, _) | | ||
ExprAssignOp(op, _, _) => { | ||
if op.node == BiAnd || op.node == BiOr { | ||
// x && y and x || y always evaluate x first, so these are | ||
// strictly sequenced. | ||
} else { | ||
walk_expr(vis, expr); | ||
} | ||
} | ||
ExprClosure(_, _, _, _) => { | ||
// Either | ||
// | ||
// * `var` is defined in the closure body, in which case we've | ||
// reached the top of the enclosing function and can stop, or | ||
// | ||
// * `var` is captured by the closure, in which case, because | ||
// evaluating a closure does not evaluate its body, we don't | ||
// necessarily have a write, so we need to stop to avoid | ||
// generating false positives. | ||
// | ||
// This is also the only place we need to stop early (grrr). | ||
return StopEarly::Stop; | ||
} | ||
// All other expressions either have only one child or strictly | ||
// sequence the evaluation order of their sub-expressions. | ||
_ => {} | ||
} | ||
|
||
vis.last_expr = expr; | ||
|
||
StopEarly::KeepGoing | ||
} | ||
|
||
fn check_stmt<'v, 't>(vis: &mut ReadVisitor<'v, 't>, stmt: &'v Stmt) -> StopEarly { | ||
match stmt.node { | ||
StmtExpr(ref expr, _) | | ||
StmtSemi(ref expr, _) => check_expr(vis, expr), | ||
StmtDecl(ref decl, _) => { | ||
// If the declaration is of a local variable, check its initializer | ||
// expression if it has one. Otherwise, keep going. | ||
let local = match decl.node { | ||
DeclLocal(ref local) => Some(local), | ||
_ => None, | ||
}; | ||
local.and_then(|local| local.init.as_ref()) | ||
.map_or(StopEarly::KeepGoing, |expr| check_expr(vis, expr)) | ||
} | ||
} | ||
} | ||
|
||
/// A visitor that looks for reads from a variable. | ||
struct ReadVisitor<'v, 't: 'v> { | ||
cx: &'v LateContext<'v, 't>, | ||
/// The id of the variable we're looking for. | ||
var: DefId, | ||
/// The expressions where the write to the variable occurred (for reporting | ||
/// in the lint). | ||
write_expr: &'v Expr, | ||
/// The last (highest in the AST) expression we've checked, so we know not | ||
/// to recheck it. | ||
last_expr: &'v Expr, | ||
} | ||
|
||
impl<'v, 't> Visitor<'v> for ReadVisitor<'v, 't> { | ||
fn visit_expr(&mut self, expr: &'v Expr) { | ||
if expr.id == self.last_expr.id { | ||
return; | ||
} | ||
|
||
match expr.node { | ||
ExprPath(None, ref path) => { | ||
if path.segments.len() == 1 && self.cx.tcx.expect_def(expr.id).def_id() == self.var { | ||
if is_in_assignment_position(self.cx, expr) { | ||
// This is a write, not a read. | ||
} else { | ||
span_note_and_lint( | ||
self.cx, | ||
EVAL_ORDER_DEPENDENCE, | ||
expr.span, | ||
"unsequenced read of a variable", | ||
self.write_expr.span, | ||
"whether read occurs before this write depends on evaluation order" | ||
); | ||
} | ||
} | ||
} | ||
// We're about to descend a closure. Since we don't know when (or | ||
// if) the closure will be evaluated, any reads in it might not | ||
// occur here (or ever). Like above, bail to avoid false positives. | ||
ExprClosure(_, _, _, _) | | ||
|
||
// We want to avoid a false positive when a variable name occurs | ||
// only to have its address taken, so we stop here. Technically, | ||
// this misses some weird cases, eg. | ||
// | ||
// ```rust | ||
// let mut x = 0; | ||
// let a = foo(&{x = 1; x}, x); | ||
// ``` | ||
// | ||
// TODO: fix this | ||
ExprAddrOf(_, _) => { | ||
return; | ||
} | ||
_ => {} | ||
} | ||
|
||
walk_expr(self, expr); | ||
} | ||
} | ||
|
||
/// Returns true if `expr` is the LHS of an assignment, like `expr = ...`. | ||
fn is_in_assignment_position(cx: &LateContext, expr: &Expr) -> bool { | ||
if let Some(parent) = get_parent_expr(cx, expr) { | ||
if let ExprAssign(ref lhs, _) = parent.node { | ||
return lhs.id == expr.id; | ||
} | ||
} | ||
false | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#![feature(plugin)] | ||
#![plugin(clippy)] | ||
|
||
#[deny(eval_order_dependence)] | ||
#[allow(unused_assignments, unused_variables, many_single_char_names, no_effect, dead_code, blacklisted_name)] | ||
fn main() { | ||
let mut x = 0; | ||
let a = { x = 1; 1 } + x; | ||
//~^ ERROR unsequenced read | ||
|
||
// Example from iss#277 | ||
x += { x = 20; 2 }; //~ERROR unsequenced read | ||
|
||
// Does it work in weird places? | ||
// ...in the base for a struct expression? | ||
struct Foo { a: i32, b: i32 }; | ||
let base = Foo { a: 4, b: 5 }; | ||
let foo = Foo { a: x, .. { x = 6; base } }; | ||
//~^ ERROR unsequenced read | ||
// ...inside a closure? | ||
let closure = || { | ||
let mut x = 0; | ||
x += { x = 20; 2 }; //~ERROR unsequenced read | ||
}; | ||
// ...not across a closure? | ||
let mut y = 0; | ||
let b = (y, || { y = 1 }); | ||
|
||
// && and || evaluate left-to-right. | ||
let a = { x = 1; true } && (x == 3); | ||
let a = { x = 1; true } || (x == 3); | ||
|
||
// Make sure we don't get confused by alpha conversion. | ||
let a = { let mut x = 1; x = 2; 1 } + x; | ||
|
||
// No warning if we don't read the variable... | ||
x = { x = 20; 2 }; | ||
// ...if the assignment is in a closure... | ||
let b = { || { x = 1; }; 1 } + x; | ||
// ... or the access is under an address. | ||
let b = ({ let p = &x; 1 }, { x = 1; x }); | ||
|
||
// Limitation: l-values other than simple variables don't trigger | ||
// the warning. | ||
let mut tup = (0, 0); | ||
let c = { tup.0 = 1; 1 } + tup.0; | ||
// Limitation: you can get away with a read under address-of. | ||
let mut z = 0; | ||
let b = (&{ z = x; x }, { x = 3; x }); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should either fix rustc's "unused_assignments" lint or produce our own lint for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easy if you just want to detect an assignments to
x
whose RHS also assigns tox
; the lint would just need to put here. I can add that if you want, but I don't know what to call it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to do anything... rustc warns: https://is.gd/Gxtp1U
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But not for
x = { x = 5; x };
:)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's because it's not an unused assignment ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
x = (x = 5, x + 1).1;
is the more interesting case, since blocks have a controlled order of execution.