Add need-mut and unused-mut diagnostics

This commit is contained in:
hkalbasi 2023-02-21 22:30:38 +03:30
parent c0a0664d12
commit a25710b0c0
10 changed files with 1089 additions and 277 deletions

View file

@ -422,6 +422,13 @@ impl Body {
}
}
pub fn walk_child_bindings(&self, pat: PatId, f: &mut impl FnMut(BindingId)) {
if let Pat::Bind { id, .. } = self[pat] {
f(id)
}
self[pat].walk_child_pats(|p| self.walk_child_bindings(p, f));
}
pub fn pretty_print(&self, db: &dyn DefDatabase, owner: DefWithBodyId) -> String {
pretty::print_body_hir(db, self, owner)
}

View file

@ -103,6 +103,22 @@ fn references() {
"#,
5,
);
check_number(
r#"
struct Foo(i32);
impl Foo {
fn method(&mut self, x: i32) {
self.0 = 2 * self.0 + x;
}
}
const GOAL: i32 = {
let mut x = Foo(3);
x.method(5);
x.0
};
"#,
11,
);
}
#[test]
@ -358,7 +374,7 @@ fn ifs() {
if a < b { b } else { a }
}
const GOAL: u8 = max(max(1, max(10, 3)), 0-122);
const GOAL: i32 = max(max(1, max(10, 3)), 0-122);
"#,
10,
);
@ -366,7 +382,7 @@ fn ifs() {
check_number(
r#"
const fn max(a: &i32, b: &i32) -> &i32 {
if a < b { b } else { a }
if *a < *b { b } else { a }
}
const GOAL: i32 = *max(max(&1, max(&10, &3)), &5);
@ -464,6 +480,16 @@ fn tuples() {
"#,
20,
);
check_number(
r#"
const GOAL: u8 = {
let mut a = (10, 20, 3, 15);
a.1 = 2;
a.0 + a.1 + a.2 + a.3
};
"#,
30,
);
check_number(
r#"
struct TupleLike(i32, u8, i64, u16);
@ -539,7 +565,7 @@ fn let_else() {
let Some(x) = x else { return 10 };
2 * x
}
const GOAL: u8 = f(Some(1000)) + f(None);
const GOAL: i32 = f(Some(1000)) + f(None);
"#,
2010,
);
@ -615,7 +641,7 @@ fn options() {
0
}
}
const GOAL: u8 = f(Some(Some(10))) + f(Some(None)) + f(None);
const GOAL: i32 = f(Some(Some(10))) + f(Some(None)) + f(None);
"#,
11,
);
@ -746,24 +772,24 @@ fn enums() {
r#"
enum E {
F1 = 1,
F2 = 2 * E::F1 as u8,
F3 = 3 * E::F2 as u8,
F2 = 2 * E::F1 as isize, // Rustc expects an isize here
F3 = 3 * E::F2 as isize,
}
const GOAL: i32 = E::F3 as u8;
const GOAL: u8 = E::F3 as u8;
"#,
6,
);
check_number(
r#"
enum E { F1 = 1, F2, }
const GOAL: i32 = E::F2 as u8;
const GOAL: u8 = E::F2 as u8;
"#,
2,
);
check_number(
r#"
enum E { F1, }
const GOAL: i32 = E::F1 as u8;
const GOAL: u8 = E::F1 as u8;
"#,
0,
);
@ -894,8 +920,22 @@ fn exec_limits() {
}
sum
}
const GOAL: usize = f(10000);
const GOAL: i32 = f(10000);
"#,
10000 * 10000,
);
}
#[test]
fn type_error() {
let e = eval_goal(
r#"
const GOAL: u8 = {
let x: u16 = 2;
let y: (u8, u8) = x;
y.0
};
"#,
);
assert!(matches!(e, Err(ConstEvalError::MirLowerError(MirLowerError::TypeMismatch(_)))));
}

View file

@ -7,17 +7,19 @@ use crate::{
};
use chalk_ir::Mutability;
use hir_def::{
expr::{Expr, Ordering},
expr::{BindingId, Expr, ExprId, Ordering, PatId},
DefWithBodyId, FieldId, UnionId, VariantId,
};
use la_arena::{Arena, Idx, RawIdx};
use la_arena::{Arena, ArenaMap, Idx, RawIdx};
mod eval;
mod lower;
pub mod borrowck;
pub use eval::{interpret_mir, pad16, Evaluator, MirEvalError};
pub use lower::{lower_to_mir, mir_body_query, mir_body_recover, MirLowerError};
use smallvec::{smallvec, SmallVec};
use stdx::impl_from;
use super::consteval::{intern_const_scalar, try_const_usize};
@ -181,6 +183,11 @@ impl SwitchTargets {
iter::zip(&self.values, &self.targets).map(|(x, y)| (*x, *y))
}
/// Returns a slice with all possible jump targets (including the fallback target).
pub fn all_targets(&self) -> &[BasicBlockId] {
&self.targets
}
/// Finds the `BasicBlock` to which this `SwitchInt` will branch given the
/// specific value. This cannot fail, as it'll return the `otherwise`
/// branch if there's not a specific match for the value.
@ -758,7 +765,7 @@ pub enum Rvalue {
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Statement {
pub enum StatementKind {
Assign(Place, Rvalue),
//FakeRead(Box<(FakeReadCause, Place)>),
//SetDiscriminant {
@ -773,6 +780,17 @@ pub enum Statement {
//Intrinsic(Box<NonDivergingIntrinsic>),
Nop,
}
impl StatementKind {
fn with_span(self, span: MirSpan) -> Statement {
Statement { kind: self, span }
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Statement {
pub kind: StatementKind,
pub span: MirSpan,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct BasicBlock {
@ -803,6 +821,7 @@ pub struct MirBody {
pub start_block: BasicBlockId,
pub owner: DefWithBodyId,
pub arg_count: usize,
pub binding_locals: ArenaMap<BindingId, LocalId>,
}
impl MirBody {}
@ -810,3 +829,12 @@ impl MirBody {}
fn const_as_usize(c: &Const) -> usize {
try_const_usize(c).unwrap() as usize
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MirSpan {
ExprId(ExprId),
PatId(PatId),
Unknown,
}
impl_from!(ExprId, PatId for MirSpan);

View file

@ -0,0 +1,201 @@
//! MIR borrow checker, which is used in diagnostics like `unused_mut`
// Currently it is an ad-hoc implementation, only useful for mutability analysis. Feel free to remove all of these
// and implement a proper borrow checker.
use la_arena::ArenaMap;
use stdx::never;
use super::{
BasicBlockId, BorrowKind, LocalId, MirBody, MirSpan, Place, ProjectionElem, Rvalue,
StatementKind, Terminator,
};
#[derive(Debug)]
pub enum Mutability {
Mut { span: MirSpan },
Not,
}
fn is_place_direct(lvalue: &Place) -> bool {
!lvalue.projection.iter().any(|x| *x == ProjectionElem::Deref)
}
enum ProjectionCase {
/// Projection is a local
Direct,
/// Projection is some field or slice of a local
DirectPart,
/// Projection is deref of something
Indirect,
}
fn place_case(lvalue: &Place) -> ProjectionCase {
let mut is_part_of = false;
for proj in lvalue.projection.iter().rev() {
match proj {
ProjectionElem::Deref => return ProjectionCase::Indirect, // It's indirect
ProjectionElem::ConstantIndex { .. }
| ProjectionElem::Subslice { .. }
| ProjectionElem::Field(_)
| ProjectionElem::TupleField(_)
| ProjectionElem::Index(_) => {
is_part_of = true;
}
ProjectionElem::OpaqueCast(_) => (),
}
}
if is_part_of {
ProjectionCase::DirectPart
} else {
ProjectionCase::Direct
}
}
/// Returns a map from basic blocks to the set of locals that might be ever initialized before
/// the start of the block. Only `StorageDead` can remove something from this map, and we ignore
/// `Uninit` and `drop` and similars after initialization.
fn ever_initialized_map(body: &MirBody) -> ArenaMap<BasicBlockId, ArenaMap<LocalId, bool>> {
let mut result: ArenaMap<BasicBlockId, ArenaMap<LocalId, bool>> =
body.basic_blocks.iter().map(|x| (x.0, ArenaMap::default())).collect();
fn dfs(
body: &MirBody,
b: BasicBlockId,
l: LocalId,
result: &mut ArenaMap<BasicBlockId, ArenaMap<LocalId, bool>>,
) {
let mut is_ever_initialized = result[b][l]; // It must be filled, as we use it as mark for dfs
let block = &body.basic_blocks[b];
for statement in &block.statements {
match &statement.kind {
StatementKind::Assign(p, _) => {
if p.projection.len() == 0 && p.local == l {
is_ever_initialized = true;
}
}
StatementKind::StorageDead(p) => {
if *p == l {
is_ever_initialized = false;
}
}
StatementKind::Deinit(_) | StatementKind::Nop | StatementKind::StorageLive(_) => (),
}
}
let Some(terminator) = &block.terminator else {
never!("Terminator should be none only in construction");
return;
};
let targets = match terminator {
Terminator::Goto { target } => vec![*target],
Terminator::SwitchInt { targets, .. } => targets.all_targets().to_vec(),
Terminator::Resume
| Terminator::Abort
| Terminator::Return
| Terminator::Unreachable => vec![],
Terminator::Call { target, cleanup, destination, .. } => {
if destination.projection.len() == 0 && destination.local == l {
is_ever_initialized = true;
}
target.into_iter().chain(cleanup.into_iter()).copied().collect()
}
Terminator::Drop { .. }
| Terminator::DropAndReplace { .. }
| Terminator::Assert { .. }
| Terminator::Yield { .. }
| Terminator::GeneratorDrop
| Terminator::FalseEdge { .. }
| Terminator::FalseUnwind { .. } => {
never!("We don't emit these MIR terminators yet");
vec![]
}
};
for target in targets {
if !result[target].contains_idx(l) || !result[target][l] && is_ever_initialized {
result[target].insert(l, is_ever_initialized);
dfs(body, target, l, result);
}
}
}
for (b, block) in body.basic_blocks.iter() {
for statement in &block.statements {
if let StatementKind::Assign(p, _) = &statement.kind {
if p.projection.len() == 0 {
let l = p.local;
if !result[b].contains_idx(l) {
result[b].insert(l, false);
dfs(body, b, l, &mut result);
}
}
}
}
}
result
}
pub fn mutability_of_locals(body: &MirBody) -> ArenaMap<LocalId, Mutability> {
let mut result: ArenaMap<LocalId, Mutability> =
body.locals.iter().map(|x| (x.0, Mutability::Not)).collect();
let ever_init_maps = ever_initialized_map(body);
for (block_id, ever_init_map) in ever_init_maps.iter() {
let mut ever_init_map = ever_init_map.clone();
let block = &body.basic_blocks[block_id];
for statement in &block.statements {
match &statement.kind {
StatementKind::Assign(place, value) => {
match place_case(place) {
ProjectionCase::Direct => {
if ever_init_map.get(place.local).copied().unwrap_or_default() {
result[place.local] = Mutability::Mut { span: statement.span };
} else {
ever_init_map.insert(place.local, true);
}
}
ProjectionCase::DirectPart => {
// Partial initialization is not supported, so it is definitely `mut`
result[place.local] = Mutability::Mut { span: statement.span };
}
ProjectionCase::Indirect => (),
}
if let Rvalue::Ref(BorrowKind::Mut { .. }, p) = value {
if is_place_direct(p) {
result[p.local] = Mutability::Mut { span: statement.span };
}
}
}
StatementKind::StorageDead(p) => {
ever_init_map.insert(*p, false);
}
StatementKind::Deinit(_) | StatementKind::StorageLive(_) | StatementKind::Nop => (),
}
}
let Some(terminator) = &block.terminator else {
never!("Terminator should be none only in construction");
continue;
};
match terminator {
Terminator::Goto { .. }
| Terminator::Resume
| Terminator::Abort
| Terminator::Return
| Terminator::Unreachable
| Terminator::FalseEdge { .. }
| Terminator::FalseUnwind { .. }
| Terminator::GeneratorDrop
| Terminator::SwitchInt { .. }
| Terminator::Drop { .. }
| Terminator::DropAndReplace { .. }
| Terminator::Assert { .. }
| Terminator::Yield { .. } => (),
Terminator::Call { destination, .. } => {
if destination.projection.len() == 0 {
if ever_init_map.get(destination.local).copied().unwrap_or_default() {
result[destination.local] = Mutability::Mut { span: MirSpan::Unknown };
} else {
ever_init_map.insert(destination.local, true);
}
}
}
}
}
result
}

View file

@ -29,7 +29,7 @@ use crate::{
use super::{
const_as_usize, return_slot, AggregateKind, BinOp, CastKind, LocalId, MirBody, MirLowerError,
Operand, Place, ProjectionElem, Rvalue, Statement, Terminator, UnOp,
Operand, Place, ProjectionElem, Rvalue, StatementKind, Terminator, UnOp,
};
pub struct Evaluator<'a> {
@ -395,7 +395,8 @@ impl Evaluator<'_> {
.locals
.iter()
.map(|(id, x)| {
let size = self.size_of_sized(&x.ty, &locals, "no unsized local")?;
let size =
self.size_of_sized(&x.ty, &locals, "no unsized local in extending stack")?;
let my_ptr = stack_ptr;
stack_ptr += size;
Ok((id, Stack(my_ptr)))
@ -425,16 +426,16 @@ impl Evaluator<'_> {
return Err(MirEvalError::ExecutionLimitExceeded);
}
for statement in &current_block.statements {
match statement {
Statement::Assign(l, r) => {
match &statement.kind {
StatementKind::Assign(l, r) => {
let addr = self.place_addr(l, &locals)?;
let result = self.eval_rvalue(r, &locals)?.to_vec(&self)?;
self.write_memory(addr, &result)?;
}
Statement::Deinit(_) => not_supported!("de-init statement"),
Statement::StorageLive(_) => not_supported!("storage-live statement"),
Statement::StorageDead(_) => not_supported!("storage-dead statement"),
Statement::Nop => (),
StatementKind::Deinit(_) => not_supported!("de-init statement"),
StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::Nop => (),
}
}
let Some(terminator) = current_block.terminator.as_ref() else {

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ use hir_def::path::ModPath;
use hir_expand::{name::Name, HirFileId, InFile};
use syntax::{ast, AstPtr, SyntaxNodePtr, TextRange};
use crate::{AssocItem, Field, MacroKind, Type};
use crate::{AssocItem, Field, Local, MacroKind, Type};
macro_rules! diagnostics {
($($diag:ident,)*) => {
@ -41,6 +41,7 @@ diagnostics![
MissingFields,
MissingMatchArms,
MissingUnsafe,
NeedMut,
NoSuchField,
PrivateAssocItem,
PrivateField,
@ -54,6 +55,7 @@ diagnostics![
UnresolvedMethodCall,
UnresolvedModule,
UnresolvedProcMacro,
UnusedMut,
];
#[derive(Debug)]
@ -209,4 +211,15 @@ pub struct TypeMismatch {
pub actual: Type,
}
#[derive(Debug)]
pub struct NeedMut {
pub local: Local,
pub span: InFile<SyntaxNodePtr>,
}
#[derive(Debug)]
pub struct UnusedMut {
pub local: Local,
}
pub use hir_ty::diagnostics::IncorrectCase;

View file

@ -63,7 +63,7 @@ use hir_ty::{
display::HexifiedConst,
layout::layout_of_ty,
method_resolution::{self, TyFingerprint},
mir::interpret_mir,
mir::{self, interpret_mir},
primitive::UintTy,
traits::FnTrait,
AliasTy, CallableDefId, CallableSig, Canonical, CanonicalVarKinds, Cast, ClosureId,
@ -85,12 +85,12 @@ use crate::db::{DefDatabase, HirDatabase};
pub use crate::{
attrs::{HasAttrs, Namespace},
diagnostics::{
AnyDiagnostic, BreakOutsideOfLoop, ExpectedFunction, InactiveCode, IncorrectCase,
InvalidDeriveTarget, MacroError, MalformedDerive, MismatchedArgCount, MissingFields,
MissingMatchArms, MissingUnsafe, NoSuchField, PrivateAssocItem, PrivateField,
AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, IncorrectCase, InvalidDeriveTarget,
MacroError, MalformedDerive, MismatchedArgCount, MissingFields, MissingMatchArms,
MissingUnsafe, NeedMut, NoSuchField, PrivateAssocItem, PrivateField,
ReplaceFilterMapNextWithFindMap, TypeMismatch, UnimplementedBuiltinMacro,
UnresolvedExternCrate, UnresolvedField, UnresolvedImport, UnresolvedMacroCall,
UnresolvedMethodCall, UnresolvedModule, UnresolvedProcMacro,
UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall, UnresolvedModule,
UnresolvedProcMacro, UnusedMut,
},
has_source::HasSource,
semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo, VisibleTraits},
@ -1500,6 +1500,38 @@ impl DefWithBody {
}
}
let hir_body = db.body(self.into());
if let Ok(mir_body) = db.mir_body(self.into()) {
let mol = mir::borrowck::mutability_of_locals(&mir_body);
for (binding_id, _) in hir_body.bindings.iter() {
let need_mut = &mol[mir_body.binding_locals[binding_id]];
let local = Local { parent: self.into(), binding_id };
match (need_mut, local.is_mut(db)) {
(mir::borrowck::Mutability::Mut { .. }, true)
| (mir::borrowck::Mutability::Not, false) => (),
(mir::borrowck::Mutability::Mut { span }, false) => {
let span: InFile<SyntaxNodePtr> = match span {
mir::MirSpan::ExprId(e) => match source_map.expr_syntax(*e) {
Ok(s) => s.map(|x| x.into()),
Err(_) => continue,
},
mir::MirSpan::PatId(p) => match source_map.pat_syntax(*p) {
Ok(s) => s.map(|x| match x {
Either::Left(e) => e.into(),
Either::Right(e) => e.into(),
}),
Err(_) => continue,
},
mir::MirSpan::Unknown => continue,
};
acc.push(NeedMut { local, span }.into());
}
(mir::borrowck::Mutability::Not, true) => acc.push(UnusedMut { local }.into()),
}
}
}
for diagnostic in BodyValidationDiagnostic::collect(db, self.into()) {
match diagnostic {
BodyValidationDiagnostic::RecordMissingFields {
@ -2490,6 +2522,10 @@ impl LocalSource {
pub fn syntax(&self) -> &SyntaxNode {
self.source.value.syntax()
}
pub fn syntax_ptr(self) -> InFile<SyntaxNodePtr> {
self.source.map(|x| SyntaxNodePtr::new(x.syntax()))
}
}
impl Local {

View file

@ -0,0 +1,302 @@
use crate::{Diagnostic, DiagnosticsContext, Severity};
// Diagnostic: need-mut
//
// This diagnostic is triggered on mutating an immutable variable.
pub(crate) fn need_mut(ctx: &DiagnosticsContext<'_>, d: &hir::NeedMut) -> Diagnostic {
Diagnostic::new(
"need-mut",
format!("cannot mutate immutable variable `{}`", d.local.name(ctx.sema.db)),
ctx.sema.diagnostics_display_range(d.span.clone()).range,
)
}
// Diagnostic: unused-mut
//
// This diagnostic is triggered when a mutable variable isn't actually mutated.
pub(crate) fn unused_mut(ctx: &DiagnosticsContext<'_>, d: &hir::UnusedMut) -> Diagnostic {
Diagnostic::new(
"unused-mut",
"remove this `mut`",
ctx.sema.diagnostics_display_range(d.local.primary_source(ctx.sema.db).syntax_ptr()).range,
)
.severity(Severity::WeakWarning)
}
#[cfg(test)]
mod tests {
use crate::tests::check_diagnostics;
#[test]
fn unused_mut_simple() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = 2;
//^^^^^ weak: remove this `mut`
f(x);
}
"#,
);
}
#[test]
fn no_false_positive_simple() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x = 2;
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = 2;
x = 5;
f(x);
}
"#,
);
}
#[test]
fn field_mutate() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = (2, 7);
//^^^^^ weak: remove this `mut`
f(x.1);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = (2, 7);
x.0 = 5;
f(x.1);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x = (2, 7);
x.0 = 5;
//^^^^^^^ error: cannot mutate immutable variable `x`
f(x.1);
}
"#,
);
}
#[test]
fn mutable_reference() {
check_diagnostics(
r#"
fn main() {
let mut x = &mut 2;
//^^^^^ weak: remove this `mut`
*x = 5;
}
"#,
);
check_diagnostics(
r#"
fn main() {
let x = 2;
&mut x;
//^^^^^^ error: cannot mutate immutable variable `x`
}
"#,
);
check_diagnostics(
r#"
fn main() {
let x_own = 2;
let ref mut x_ref = x_own;
//^^^^^^^^^^^^^ error: cannot mutate immutable variable `x_own`
}
"#,
);
check_diagnostics(
r#"
struct Foo;
impl Foo {
fn method(&mut self, x: i32) {}
}
fn main() {
let x = Foo;
x.method(2);
//^ error: cannot mutate immutable variable `x`
}
"#,
);
}
#[test]
fn match_bindings() {
check_diagnostics(
r#"
fn main() {
match (2, 3) {
(x, mut y) => {
//^^^^^ weak: remove this `mut`
x = 7;
//^^^^^ error: cannot mutate immutable variable `x`
}
}
}
"#,
);
}
#[test]
fn mutation_in_dead_code() {
// This one is interesting. Dead code is not represented at all in the MIR, so
// there would be no mutablility error for locals in dead code. Rustc tries to
// not emit `unused_mut` in this case, but since it works without `mut`, and
// special casing it is not trivial, we emit it.
check_diagnostics(
r#"
fn main() {
return;
let mut x = 2;
//^^^^^ weak: remove this `mut`
&mut x;
}
"#,
);
check_diagnostics(
r#"
fn main() {
loop {}
let mut x = 2;
//^^^^^ weak: remove this `mut`
&mut x;
}
"#,
);
check_diagnostics(
r#"
enum X {}
fn g() -> X {
loop {}
}
fn f() -> ! {
loop {}
}
fn main(b: bool) {
if b {
f();
} else {
g();
}
let mut x = 2;
//^^^^^ weak: remove this `mut`
&mut x;
}
"#,
);
check_diagnostics(
r#"
fn main(b: bool) {
if b {
loop {}
} else {
return;
}
let mut x = 2;
//^^^^^ weak: remove this `mut`
&mut x;
}
"#,
);
}
#[test]
fn initialization_is_not_mutation() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x;
//^^^^^ weak: remove this `mut`
x = 5;
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main(b: bool) {
let mut x;
//^^^^^ weak: remove this `mut`
if b {
x = 1;
} else {
x = 3;
}
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main(b: bool) {
let x;
if b {
x = 1;
}
x = 3;
//^^^^^ error: cannot mutate immutable variable `x`
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x;
loop {
x = 1;
//^^^^^ error: cannot mutate immutable variable `x`
f(x);
}
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
loop {
let mut x = 1;
//^^^^^ weak: remove this `mut`
f(x);
if let mut y = 2 {
//^^^^^ weak: remove this `mut`
f(y);
}
match 3 {
mut z => f(z),
//^^^^^ weak: remove this `mut`
}
}
}
"#,
);
}
}

View file

@ -37,6 +37,7 @@ mod handlers {
pub(crate) mod missing_fields;
pub(crate) mod missing_match_arms;
pub(crate) mod missing_unsafe;
pub(crate) mod mutability_errors;
pub(crate) mod no_such_field;
pub(crate) mod private_assoc_item;
pub(crate) mod private_field;
@ -273,7 +274,8 @@ pub fn diagnostics(
AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d),
AnyDiagnostic::UnresolvedField(d) => handlers::unresolved_field::unresolved_field(&ctx, &d),
AnyDiagnostic::UnresolvedMethodCall(d) => handlers::unresolved_method::unresolved_method(&ctx, &d),
AnyDiagnostic::NeedMut(d) => handlers::mutability_errors::need_mut(&ctx, &d),
AnyDiagnostic::UnusedMut(d) => handlers::mutability_errors::unused_mut(&ctx, &d),
AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
Some(it) => it,
None => continue,