mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-01 11:31:15 +00:00
289 lines
11 KiB
Rust
289 lines
11 KiB
Rust
//! Provides validations for unsafe code. Currently checks if unsafe functions are missing
|
|
//! unsafe blocks.
|
|
|
|
use std::mem;
|
|
|
|
use either::Either;
|
|
use hir_def::{
|
|
body::Body,
|
|
hir::{Expr, ExprId, ExprOrPatId, Pat, PatId, Statement, UnaryOp},
|
|
path::Path,
|
|
resolver::{HasResolver, ResolveValueResult, Resolver, ValueNs},
|
|
type_ref::Rawness,
|
|
AdtId, DefWithBodyId, FieldId, VariantId,
|
|
};
|
|
|
|
use crate::{
|
|
db::HirDatabase, utils::is_fn_unsafe_to_call, InferenceResult, Interner, TyExt, TyKind,
|
|
};
|
|
|
|
/// Returns `(unsafe_exprs, fn_is_unsafe)`.
|
|
///
|
|
/// If `fn_is_unsafe` is false, `unsafe_exprs` are hard errors. If true, they're `unsafe_op_in_unsafe_fn`.
|
|
pub fn missing_unsafe(
|
|
db: &dyn HirDatabase,
|
|
def: DefWithBodyId,
|
|
) -> (Vec<(ExprOrPatId, UnsafetyReason)>, bool) {
|
|
let _p = tracing::info_span!("missing_unsafe").entered();
|
|
|
|
let mut res = Vec::new();
|
|
let is_unsafe = match def {
|
|
DefWithBodyId::FunctionId(it) => db.function_data(it).is_unsafe(),
|
|
DefWithBodyId::StaticId(_)
|
|
| DefWithBodyId::ConstId(_)
|
|
| DefWithBodyId::VariantId(_)
|
|
| DefWithBodyId::InTypeConstId(_) => false,
|
|
};
|
|
|
|
let body = db.body(def);
|
|
let infer = db.infer(def);
|
|
let mut callback = |node, inside_unsafe_block, reason| {
|
|
if inside_unsafe_block == InsideUnsafeBlock::No {
|
|
res.push((node, reason));
|
|
}
|
|
};
|
|
let mut visitor = UnsafeVisitor::new(db, &infer, &body, def, &mut callback);
|
|
visitor.walk_expr(body.body_expr);
|
|
|
|
if !is_unsafe {
|
|
// Unsafety in function parameter patterns (that can only be union destructuring)
|
|
// cannot be inserted into an unsafe block, so even with `unsafe_op_in_unsafe_fn`
|
|
// it is turned off for unsafe functions.
|
|
for ¶m in &body.params {
|
|
visitor.walk_pat(param);
|
|
}
|
|
}
|
|
|
|
(res, is_unsafe)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum UnsafetyReason {
|
|
UnionField,
|
|
UnsafeFnCall,
|
|
InlineAsm,
|
|
RawPtrDeref,
|
|
MutableStatic,
|
|
ExternStatic,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum InsideUnsafeBlock {
|
|
No,
|
|
Yes,
|
|
}
|
|
|
|
pub fn unsafe_expressions(
|
|
db: &dyn HirDatabase,
|
|
infer: &InferenceResult,
|
|
def: DefWithBodyId,
|
|
body: &Body,
|
|
current: ExprId,
|
|
unsafe_expr_cb: &mut dyn FnMut(ExprOrPatId, InsideUnsafeBlock, UnsafetyReason),
|
|
) {
|
|
let mut visitor = UnsafeVisitor::new(db, infer, body, def, unsafe_expr_cb);
|
|
_ = visitor.resolver.update_to_inner_scope(db.upcast(), def, current);
|
|
visitor.walk_expr(current);
|
|
}
|
|
|
|
struct UnsafeVisitor<'a> {
|
|
db: &'a dyn HirDatabase,
|
|
infer: &'a InferenceResult,
|
|
body: &'a Body,
|
|
resolver: Resolver,
|
|
def: DefWithBodyId,
|
|
inside_unsafe_block: InsideUnsafeBlock,
|
|
inside_assignment: bool,
|
|
inside_union_destructure: bool,
|
|
unsafe_expr_cb: &'a mut dyn FnMut(ExprOrPatId, InsideUnsafeBlock, UnsafetyReason),
|
|
}
|
|
|
|
impl<'a> UnsafeVisitor<'a> {
|
|
fn new(
|
|
db: &'a dyn HirDatabase,
|
|
infer: &'a InferenceResult,
|
|
body: &'a Body,
|
|
def: DefWithBodyId,
|
|
unsafe_expr_cb: &'a mut dyn FnMut(ExprOrPatId, InsideUnsafeBlock, UnsafetyReason),
|
|
) -> Self {
|
|
let resolver = def.resolver(db.upcast());
|
|
Self {
|
|
db,
|
|
infer,
|
|
body,
|
|
resolver,
|
|
def,
|
|
inside_unsafe_block: InsideUnsafeBlock::No,
|
|
inside_assignment: false,
|
|
inside_union_destructure: false,
|
|
unsafe_expr_cb,
|
|
}
|
|
}
|
|
|
|
fn call_cb(&mut self, node: ExprOrPatId, reason: UnsafetyReason) {
|
|
(self.unsafe_expr_cb)(node, self.inside_unsafe_block, reason);
|
|
}
|
|
|
|
fn walk_pats_top(&mut self, pats: impl Iterator<Item = PatId>, parent_expr: ExprId) {
|
|
let guard = self.resolver.update_to_inner_scope(self.db.upcast(), self.def, parent_expr);
|
|
pats.for_each(|pat| self.walk_pat(pat));
|
|
self.resolver.reset_to_guard(guard);
|
|
}
|
|
|
|
fn walk_pat(&mut self, current: PatId) {
|
|
let pat = &self.body.pats[current];
|
|
|
|
if self.inside_union_destructure {
|
|
match pat {
|
|
Pat::Tuple { .. }
|
|
| Pat::Record { .. }
|
|
| Pat::Range { .. }
|
|
| Pat::Slice { .. }
|
|
| Pat::Path(..)
|
|
| Pat::Lit(..)
|
|
| Pat::Bind { .. }
|
|
| Pat::TupleStruct { .. }
|
|
| Pat::Ref { .. }
|
|
| Pat::Box { .. }
|
|
| Pat::Expr(..)
|
|
| Pat::ConstBlock(..) => self.call_cb(current.into(), UnsafetyReason::UnionField),
|
|
// `Or` only wraps other patterns, and `Missing`/`Wild` do not constitute a read.
|
|
Pat::Missing | Pat::Wild | Pat::Or(_) => {}
|
|
}
|
|
}
|
|
|
|
match pat {
|
|
Pat::Record { .. } => {
|
|
if let Some((AdtId::UnionId(_), _)) = self.infer[current].as_adt() {
|
|
let old_inside_union_destructure =
|
|
mem::replace(&mut self.inside_union_destructure, true);
|
|
self.body.walk_pats_shallow(current, |pat| self.walk_pat(pat));
|
|
self.inside_union_destructure = old_inside_union_destructure;
|
|
return;
|
|
}
|
|
}
|
|
Pat::Path(path) => self.mark_unsafe_path(current.into(), path),
|
|
&Pat::ConstBlock(expr) => {
|
|
let old_inside_assignment = mem::replace(&mut self.inside_assignment, false);
|
|
self.walk_expr(expr);
|
|
self.inside_assignment = old_inside_assignment;
|
|
}
|
|
&Pat::Expr(expr) => self.walk_expr(expr),
|
|
_ => {}
|
|
}
|
|
|
|
self.body.walk_pats_shallow(current, |pat| self.walk_pat(pat));
|
|
}
|
|
|
|
fn walk_expr(&mut self, current: ExprId) {
|
|
let expr = &self.body.exprs[current];
|
|
let inside_assignment = mem::replace(&mut self.inside_assignment, false);
|
|
match expr {
|
|
&Expr::Call { callee, .. } => {
|
|
if let Some(func) = self.infer[callee].as_fn_def(self.db) {
|
|
if is_fn_unsafe_to_call(self.db, func) {
|
|
self.call_cb(current.into(), UnsafetyReason::UnsafeFnCall);
|
|
}
|
|
}
|
|
}
|
|
Expr::Path(path) => {
|
|
let guard =
|
|
self.resolver.update_to_inner_scope(self.db.upcast(), self.def, current);
|
|
self.mark_unsafe_path(current.into(), path);
|
|
self.resolver.reset_to_guard(guard);
|
|
}
|
|
Expr::Ref { expr, rawness: Rawness::RawPtr, mutability: _ } => {
|
|
match self.body.exprs[*expr] {
|
|
// Do not report unsafe for `addr_of[_mut]!(EXTERN_OR_MUT_STATIC)`,
|
|
// see https://github.com/rust-lang/rust/pull/125834.
|
|
Expr::Path(_) => return,
|
|
// https://github.com/rust-lang/rust/pull/129248
|
|
// Taking a raw ref to a deref place expr is always safe.
|
|
Expr::UnaryOp { expr, op: UnaryOp::Deref } => {
|
|
self.body
|
|
.walk_child_exprs_without_pats(expr, |child| self.walk_expr(child));
|
|
|
|
return;
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
Expr::MethodCall { .. } => {
|
|
if self
|
|
.infer
|
|
.method_resolution(current)
|
|
.map(|(func, _)| is_fn_unsafe_to_call(self.db, func))
|
|
.unwrap_or(false)
|
|
{
|
|
self.call_cb(current.into(), UnsafetyReason::UnsafeFnCall);
|
|
}
|
|
}
|
|
Expr::UnaryOp { expr, op: UnaryOp::Deref } => {
|
|
if let TyKind::Raw(..) = &self.infer[*expr].kind(Interner) {
|
|
self.call_cb(current.into(), UnsafetyReason::RawPtrDeref);
|
|
}
|
|
}
|
|
Expr::Unsafe { .. } => {
|
|
let old_inside_unsafe_block =
|
|
mem::replace(&mut self.inside_unsafe_block, InsideUnsafeBlock::Yes);
|
|
self.body.walk_child_exprs_without_pats(current, |child| self.walk_expr(child));
|
|
self.inside_unsafe_block = old_inside_unsafe_block;
|
|
return;
|
|
}
|
|
&Expr::Assignment { target, value: _ } => {
|
|
let old_inside_assignment = mem::replace(&mut self.inside_assignment, true);
|
|
self.walk_pats_top(std::iter::once(target), current);
|
|
self.inside_assignment = old_inside_assignment;
|
|
}
|
|
Expr::InlineAsm(_) => self.call_cb(current.into(), UnsafetyReason::InlineAsm),
|
|
// rustc allows union assignment to propagate through field accesses and casts.
|
|
Expr::Cast { .. } => self.inside_assignment = inside_assignment,
|
|
Expr::Field { .. } => {
|
|
self.inside_assignment = inside_assignment;
|
|
if !inside_assignment {
|
|
if let Some(Either::Left(FieldId { parent: VariantId::UnionId(_), .. })) =
|
|
self.infer.field_resolution(current)
|
|
{
|
|
self.call_cb(current.into(), UnsafetyReason::UnionField);
|
|
}
|
|
}
|
|
}
|
|
Expr::Block { statements, .. } | Expr::Async { statements, .. } => {
|
|
self.walk_pats_top(
|
|
statements.iter().filter_map(|statement| match statement {
|
|
&Statement::Let { pat, .. } => Some(pat),
|
|
_ => None,
|
|
}),
|
|
current,
|
|
);
|
|
}
|
|
Expr::Match { arms, .. } => {
|
|
self.walk_pats_top(arms.iter().map(|arm| arm.pat), current);
|
|
}
|
|
&Expr::Let { pat, .. } => {
|
|
self.walk_pats_top(std::iter::once(pat), current);
|
|
}
|
|
Expr::Closure { args, .. } => {
|
|
self.walk_pats_top(args.iter().copied(), current);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
self.body.walk_child_exprs_without_pats(current, |child| self.walk_expr(child));
|
|
}
|
|
|
|
fn mark_unsafe_path(&mut self, node: ExprOrPatId, path: &Path) {
|
|
let hygiene = self.body.expr_or_pat_path_hygiene(node);
|
|
let value_or_partial =
|
|
self.resolver.resolve_path_in_value_ns(self.db.upcast(), path, hygiene);
|
|
if let Some(ResolveValueResult::ValueNs(ValueNs::StaticId(id), _)) = value_or_partial {
|
|
let static_data = self.db.static_data(id);
|
|
if static_data.mutable {
|
|
self.call_cb(node, UnsafetyReason::MutableStatic);
|
|
} else if static_data.is_extern && !static_data.has_safe_kw {
|
|
self.call_cb(node, UnsafetyReason::ExternStatic);
|
|
}
|
|
}
|
|
}
|
|
}
|