mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-27 18:26:19 +00:00
409 lines
15 KiB
Rust
409 lines
15 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::{
|
|
AdtId, DefWithBodyId, FieldId, FunctionId, VariantId,
|
|
expr_store::{Body, path::Path},
|
|
hir::{AsmOperand, Expr, ExprId, ExprOrPatId, InlineAsmKind, Pat, PatId, Statement, UnaryOp},
|
|
resolver::{HasResolver, ResolveValueResult, Resolver, ValueNs},
|
|
signatures::StaticFlags,
|
|
type_ref::Rawness,
|
|
};
|
|
use span::Edition;
|
|
|
|
use crate::{
|
|
InferenceResult, Interner, TargetFeatures, TyExt, TyKind, db::HirDatabase,
|
|
utils::is_fn_unsafe_to_call,
|
|
};
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct MissingUnsafeResult {
|
|
pub unsafe_exprs: Vec<(ExprOrPatId, UnsafetyReason)>,
|
|
/// If `fn_is_unsafe` is false, `unsafe_exprs` are hard errors. If true, they're `unsafe_op_in_unsafe_fn`.
|
|
pub fn_is_unsafe: bool,
|
|
pub deprecated_safe_calls: Vec<ExprId>,
|
|
}
|
|
|
|
pub fn missing_unsafe(db: &dyn HirDatabase, def: DefWithBodyId) -> MissingUnsafeResult {
|
|
let _p = tracing::info_span!("missing_unsafe").entered();
|
|
|
|
let is_unsafe = match def {
|
|
DefWithBodyId::FunctionId(it) => db.function_signature(it).is_unsafe(),
|
|
DefWithBodyId::StaticId(_) | DefWithBodyId::ConstId(_) | DefWithBodyId::VariantId(_) => {
|
|
false
|
|
}
|
|
};
|
|
|
|
let mut res = MissingUnsafeResult { fn_is_unsafe: is_unsafe, ..MissingUnsafeResult::default() };
|
|
let body = db.body(def);
|
|
let infer = db.infer(def);
|
|
let mut callback = |diag| match diag {
|
|
UnsafeDiagnostic::UnsafeOperation { node, inside_unsafe_block, reason } => {
|
|
if inside_unsafe_block == InsideUnsafeBlock::No {
|
|
res.unsafe_exprs.push((node, reason));
|
|
}
|
|
}
|
|
UnsafeDiagnostic::DeprecatedSafe2024 { node, inside_unsafe_block } => {
|
|
if inside_unsafe_block == InsideUnsafeBlock::No {
|
|
res.deprecated_safe_calls.push(node)
|
|
}
|
|
}
|
|
};
|
|
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
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum UnsafetyReason {
|
|
UnionField,
|
|
UnsafeFnCall,
|
|
InlineAsm,
|
|
RawPtrDeref,
|
|
MutableStatic,
|
|
ExternStatic,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum InsideUnsafeBlock {
|
|
No,
|
|
Yes,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum UnsafeDiagnostic {
|
|
UnsafeOperation {
|
|
node: ExprOrPatId,
|
|
inside_unsafe_block: InsideUnsafeBlock,
|
|
reason: UnsafetyReason,
|
|
},
|
|
/// A lint.
|
|
DeprecatedSafe2024 { node: ExprId, inside_unsafe_block: InsideUnsafeBlock },
|
|
}
|
|
|
|
pub fn unsafe_operations_for_body(
|
|
db: &dyn HirDatabase,
|
|
infer: &InferenceResult,
|
|
def: DefWithBodyId,
|
|
body: &Body,
|
|
callback: &mut dyn FnMut(ExprOrPatId),
|
|
) {
|
|
let mut visitor_callback = |diag| {
|
|
if let UnsafeDiagnostic::UnsafeOperation { node, .. } = diag {
|
|
callback(node);
|
|
}
|
|
};
|
|
let mut visitor = UnsafeVisitor::new(db, infer, body, def, &mut visitor_callback);
|
|
visitor.walk_expr(body.body_expr);
|
|
for ¶m in &body.params {
|
|
visitor.walk_pat(param);
|
|
}
|
|
}
|
|
|
|
pub fn unsafe_operations(
|
|
db: &dyn HirDatabase,
|
|
infer: &InferenceResult,
|
|
def: DefWithBodyId,
|
|
body: &Body,
|
|
current: ExprId,
|
|
callback: &mut dyn FnMut(InsideUnsafeBlock),
|
|
) {
|
|
let mut visitor_callback = |diag| {
|
|
if let UnsafeDiagnostic::UnsafeOperation { inside_unsafe_block, .. } = diag {
|
|
callback(inside_unsafe_block);
|
|
}
|
|
};
|
|
let mut visitor = UnsafeVisitor::new(db, infer, body, def, &mut visitor_callback);
|
|
_ = visitor.resolver.update_to_inner_scope(db, def, current);
|
|
visitor.walk_expr(current);
|
|
}
|
|
|
|
struct UnsafeVisitor<'db> {
|
|
db: &'db dyn HirDatabase,
|
|
infer: &'db InferenceResult,
|
|
body: &'db Body,
|
|
resolver: Resolver<'db>,
|
|
def: DefWithBodyId,
|
|
inside_unsafe_block: InsideUnsafeBlock,
|
|
inside_assignment: bool,
|
|
inside_union_destructure: bool,
|
|
callback: &'db mut dyn FnMut(UnsafeDiagnostic),
|
|
def_target_features: TargetFeatures,
|
|
// FIXME: This needs to be the edition of the span of each call.
|
|
edition: Edition,
|
|
}
|
|
|
|
impl<'db> UnsafeVisitor<'db> {
|
|
fn new(
|
|
db: &'db dyn HirDatabase,
|
|
infer: &'db InferenceResult,
|
|
body: &'db Body,
|
|
def: DefWithBodyId,
|
|
unsafe_expr_cb: &'db mut dyn FnMut(UnsafeDiagnostic),
|
|
) -> Self {
|
|
let resolver = def.resolver(db);
|
|
let def_target_features = match def {
|
|
DefWithBodyId::FunctionId(func) => TargetFeatures::from_attrs(&db.attrs(func.into())),
|
|
_ => TargetFeatures::default(),
|
|
};
|
|
let edition = resolver.module().krate().data(db).edition;
|
|
Self {
|
|
db,
|
|
infer,
|
|
body,
|
|
resolver,
|
|
def,
|
|
inside_unsafe_block: InsideUnsafeBlock::No,
|
|
inside_assignment: false,
|
|
inside_union_destructure: false,
|
|
callback: unsafe_expr_cb,
|
|
def_target_features,
|
|
edition,
|
|
}
|
|
}
|
|
|
|
fn on_unsafe_op(&mut self, node: ExprOrPatId, reason: UnsafetyReason) {
|
|
(self.callback)(UnsafeDiagnostic::UnsafeOperation {
|
|
node,
|
|
inside_unsafe_block: self.inside_unsafe_block,
|
|
reason,
|
|
});
|
|
}
|
|
|
|
fn check_call(&mut self, node: ExprId, func: FunctionId) {
|
|
let unsafety = is_fn_unsafe_to_call(self.db, func, &self.def_target_features, self.edition);
|
|
match unsafety {
|
|
crate::utils::Unsafety::Safe => {}
|
|
crate::utils::Unsafety::Unsafe => {
|
|
self.on_unsafe_op(node.into(), UnsafetyReason::UnsafeFnCall)
|
|
}
|
|
crate::utils::Unsafety::DeprecatedSafe2024 => {
|
|
(self.callback)(UnsafeDiagnostic::DeprecatedSafe2024 {
|
|
node,
|
|
inside_unsafe_block: self.inside_unsafe_block,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn with_inside_unsafe_block<R>(
|
|
&mut self,
|
|
inside_unsafe_block: InsideUnsafeBlock,
|
|
f: impl FnOnce(&mut Self) -> R,
|
|
) -> R {
|
|
let old = mem::replace(&mut self.inside_unsafe_block, inside_unsafe_block);
|
|
let result = f(self);
|
|
self.inside_unsafe_block = old;
|
|
result
|
|
}
|
|
|
|
fn walk_pats_top(&mut self, pats: impl Iterator<Item = PatId>, parent_expr: ExprId) {
|
|
let guard = self.resolver.update_to_inner_scope(self.db, 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.on_unsafe_op(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, .. } => {
|
|
let callee = &self.infer[callee];
|
|
if let Some(func) = callee.as_fn_def(self.db) {
|
|
self.check_call(current, func);
|
|
}
|
|
if let TyKind::Function(fn_ptr) = callee.kind(Interner) {
|
|
if fn_ptr.sig.safety == chalk_ir::Safety::Unsafe {
|
|
self.on_unsafe_op(current.into(), UnsafetyReason::UnsafeFnCall);
|
|
}
|
|
}
|
|
}
|
|
Expr::Path(path) => {
|
|
let guard = self.resolver.update_to_inner_scope(self.db, 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 let Some((func, _)) = self.infer.method_resolution(current) {
|
|
self.check_call(current, func);
|
|
}
|
|
}
|
|
Expr::UnaryOp { expr, op: UnaryOp::Deref } => {
|
|
if let TyKind::Raw(..) = &self.infer[*expr].kind(Interner) {
|
|
self.on_unsafe_op(current.into(), UnsafetyReason::RawPtrDeref);
|
|
}
|
|
}
|
|
&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(asm) => {
|
|
if asm.kind == InlineAsmKind::Asm {
|
|
// `naked_asm!()` requires `unsafe` on the attribute (`#[unsafe(naked)]`),
|
|
// and `global_asm!()` doesn't require it at all.
|
|
self.on_unsafe_op(current.into(), UnsafetyReason::InlineAsm);
|
|
}
|
|
|
|
asm.operands.iter().for_each(|(_, op)| match op {
|
|
AsmOperand::In { expr, .. }
|
|
| AsmOperand::Out { expr: Some(expr), .. }
|
|
| AsmOperand::InOut { expr, .. }
|
|
| AsmOperand::Const(expr) => self.walk_expr(*expr),
|
|
AsmOperand::SplitInOut { in_expr, out_expr, .. } => {
|
|
self.walk_expr(*in_expr);
|
|
if let Some(out_expr) = out_expr {
|
|
self.walk_expr(*out_expr);
|
|
}
|
|
}
|
|
AsmOperand::Out { expr: None, .. } | AsmOperand::Sym(_) => (),
|
|
AsmOperand::Label(expr) => {
|
|
// Inline asm labels are considered safe even when inside unsafe blocks.
|
|
self.with_inside_unsafe_block(InsideUnsafeBlock::No, |this| {
|
|
this.walk_expr(*expr)
|
|
});
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
// 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.on_unsafe_op(current.into(), UnsafetyReason::UnionField);
|
|
}
|
|
}
|
|
}
|
|
Expr::Unsafe { statements, .. } => {
|
|
self.with_inside_unsafe_block(InsideUnsafeBlock::Yes, |this| {
|
|
this.walk_pats_top(
|
|
statements.iter().filter_map(|statement| match statement {
|
|
&Statement::Let { pat, .. } => Some(pat),
|
|
_ => None,
|
|
}),
|
|
current,
|
|
);
|
|
this.body.walk_child_exprs_without_pats(current, |child| this.walk_expr(child));
|
|
});
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
Expr::Const(e) => self.walk_expr(*e),
|
|
_ => {}
|
|
}
|
|
|
|
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, path, hygiene);
|
|
if let Some(ResolveValueResult::ValueNs(ValueNs::StaticId(id), _)) = value_or_partial {
|
|
let static_data = self.db.static_signature(id);
|
|
if static_data.flags.contains(StaticFlags::MUTABLE) {
|
|
self.on_unsafe_op(node, UnsafetyReason::MutableStatic);
|
|
} else if static_data.flags.contains(StaticFlags::EXTERN)
|
|
&& !static_data.flags.contains(StaticFlags::EXPLICIT_SAFE)
|
|
{
|
|
self.on_unsafe_op(node, UnsafetyReason::ExternStatic);
|
|
}
|
|
}
|
|
}
|
|
}
|