Fine tune dianostics for when a borrow conflicts with a destructor that needs exclusive access.
In particular: 1. Extend `WriteKind::StorageDeadOrDrop` with state to track whether we are running a destructor or just freeing backing storage. (As part of this, when we drop a Box<..<Box<T>..> where `T` does not need drop, we now signal that the drop of `T` is a kind of storage dead rather than a drop.) 2. When reporting that a value does not live long enough, check if we're doing an "interesting" drop, i.e. we aren't just trivally freeing the borrowed state, but rather a user-defined dtor will run and potentially require exclusive aces to the borrowed state. 3. Added a new diagnosic to describe the scenario here.
This commit is contained in:
parent
2224a42c35
commit
1f0fbddfff
6 changed files with 187 additions and 21 deletions
|
@ -8,7 +8,8 @@
|
||||||
// option. This file may not be copied, modified, or distributed
|
// option. This file may not be copied, modified, or distributed
|
||||||
// except according to those terms.
|
// except according to those terms.
|
||||||
|
|
||||||
use borrow_check::WriteKind;
|
use borrow_check::{WriteKind, StorageDeadOrDrop};
|
||||||
|
use borrow_check::prefixes::IsPrefixOf;
|
||||||
use rustc::middle::region::ScopeTree;
|
use rustc::middle::region::ScopeTree;
|
||||||
use rustc::mir::VarBindingForm;
|
use rustc::mir::VarBindingForm;
|
||||||
use rustc::mir::{BindingForm, BorrowKind, ClearCrossCrate, Field, Local};
|
use rustc::mir::{BindingForm, BorrowKind, ClearCrossCrate, Field, Local};
|
||||||
|
@ -374,6 +375,14 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
err.buffer(&mut self.errors_buffer);
|
err.buffer(&mut self.errors_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reports StorageDeadOrDrop of `place` conflicts with `borrow`.
|
||||||
|
///
|
||||||
|
/// This means that some data referenced by `borrow` needs to live
|
||||||
|
/// past the point where the StorageDeadOrDrop of `place` occurs.
|
||||||
|
/// This is usually interpreted as meaning that `place` has too
|
||||||
|
/// short a lifetime. (But sometimes it is more useful to report
|
||||||
|
/// it as a more direct conflict between the execution of a
|
||||||
|
/// `Drop::drop` with an aliasing borrow.)
|
||||||
pub(super) fn report_borrowed_value_does_not_live_long_enough(
|
pub(super) fn report_borrowed_value_does_not_live_long_enough(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -381,6 +390,12 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
place_span: (&Place<'tcx>, Span),
|
place_span: (&Place<'tcx>, Span),
|
||||||
kind: Option<WriteKind>,
|
kind: Option<WriteKind>,
|
||||||
) {
|
) {
|
||||||
|
debug!("report_borrowed_value_does_not_live_long_enough(\
|
||||||
|
{:?}, {:?}, {:?}, {:?}\
|
||||||
|
)",
|
||||||
|
context, borrow, place_span, kind
|
||||||
|
);
|
||||||
|
|
||||||
let drop_span = place_span.1;
|
let drop_span = place_span.1;
|
||||||
let scope_tree = self.tcx.region_scope_tree(self.mir_def_id);
|
let scope_tree = self.tcx.region_scope_tree(self.mir_def_id);
|
||||||
let root_place = self
|
let root_place = self
|
||||||
|
@ -412,6 +427,19 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
|
|
||||||
let borrow_reason = self.find_why_borrow_contains_point(context, borrow);
|
let borrow_reason = self.find_why_borrow_contains_point(context, borrow);
|
||||||
|
|
||||||
|
if let Some(WriteKind::StorageDeadOrDrop(StorageDeadOrDrop::Destructor)) = kind
|
||||||
|
{
|
||||||
|
// If a borrow of path `B` conflicts with drop of `D` (and
|
||||||
|
// we're not in the uninteresting case where `B` is a
|
||||||
|
// prefix of `D`), then report this as a more interesting
|
||||||
|
// destructor conflict.
|
||||||
|
if !borrow.borrowed_place.is_prefix_of(place_span.0) {
|
||||||
|
self.report_borrow_conflicts_with_destructor(
|
||||||
|
context, borrow, borrow_reason, place_span, kind);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut err = match &self.describe_place(&borrow.borrowed_place) {
|
let mut err = match &self.describe_place(&borrow.borrowed_place) {
|
||||||
Some(_) if self.is_place_thread_local(root_place) => {
|
Some(_) if self.is_place_thread_local(root_place) => {
|
||||||
self.report_thread_local_value_does_not_live_long_enough(drop_span, borrow_span)
|
self.report_thread_local_value_does_not_live_long_enough(drop_span, borrow_span)
|
||||||
|
@ -475,6 +503,69 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
err
|
err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn report_borrow_conflicts_with_destructor(
|
||||||
|
&mut self,
|
||||||
|
context: Context,
|
||||||
|
borrow: &BorrowData<'tcx>,
|
||||||
|
borrow_reason: BorrowContainsPointReason<'tcx>,
|
||||||
|
place_span: (&Place<'tcx>, Span),
|
||||||
|
kind: Option<WriteKind>,
|
||||||
|
) {
|
||||||
|
debug!(
|
||||||
|
"report_borrow_conflicts_with_destructor(\
|
||||||
|
{:?}, {:?}, {:?}, {:?} {:?}\
|
||||||
|
)",
|
||||||
|
context, borrow, borrow_reason, place_span, kind,
|
||||||
|
);
|
||||||
|
|
||||||
|
let borrow_spans = self.retrieve_borrow_spans(borrow);
|
||||||
|
let borrow_span = borrow_spans.var_or_use();
|
||||||
|
|
||||||
|
let mut err = self.tcx.cannot_borrow_across_destructor(borrow_span, Origin::Mir);
|
||||||
|
|
||||||
|
let drop_span = place_span.1;
|
||||||
|
|
||||||
|
let (what_was_dropped, dropped_ty) = {
|
||||||
|
let place = place_span.0;
|
||||||
|
let desc = match self.describe_place(place) {
|
||||||
|
Some(name) => format!("`{}`", name.as_str()),
|
||||||
|
None => format!("temporary value"),
|
||||||
|
};
|
||||||
|
let ty = place.ty(self.mir, self.tcx).to_ty(self.tcx);
|
||||||
|
(desc, ty)
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = match dropped_ty.sty {
|
||||||
|
ty::Adt(adt, _) if adt.has_dtor(self.tcx) && !adt.is_box() => {
|
||||||
|
match self.describe_place(&borrow.borrowed_place) {
|
||||||
|
Some(borrowed) =>
|
||||||
|
format!("here, drop of {D} needs exclusive access to `{B}`, \
|
||||||
|
because the type `{T}` implements the `Drop` trait",
|
||||||
|
D=what_was_dropped, T=dropped_ty, B=borrowed),
|
||||||
|
None =>
|
||||||
|
format!("here is drop of {D}; whose type `{T}` implements the `Drop` trait",
|
||||||
|
D=what_was_dropped, T=dropped_ty),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => format!("drop of {D} occurs here", D=what_was_dropped),
|
||||||
|
};
|
||||||
|
err.span_label(drop_span, label);
|
||||||
|
|
||||||
|
// Only give this note and suggestion if they could be relevant
|
||||||
|
match borrow_reason {
|
||||||
|
BorrowContainsPointReason::Liveness {..}
|
||||||
|
| BorrowContainsPointReason::DropLiveness {..} => {
|
||||||
|
err.note("consider using a `let` binding to create a longer lived value");
|
||||||
|
}
|
||||||
|
BorrowContainsPointReason::OutlivesFreeRegion {..} => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.report_why_borrow_contains_point(
|
||||||
|
&mut err, borrow_reason, kind.map(|k| (k, place_span.0)));
|
||||||
|
|
||||||
|
err.buffer(&mut self.errors_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
fn report_thread_local_value_does_not_live_long_enough(
|
fn report_thread_local_value_does_not_live_long_enough(
|
||||||
&mut self,
|
&mut self,
|
||||||
drop_span: Span,
|
drop_span: Span,
|
||||||
|
|
|
@ -551,7 +551,8 @@ impl<'cx, 'gcx, 'tcx> DataflowResultsConsumer<'cx, 'tcx> for MirBorrowckCtxt<'cx
|
||||||
self.access_place(
|
self.access_place(
|
||||||
ContextKind::StorageDead.new(location),
|
ContextKind::StorageDead.new(location),
|
||||||
(&Place::Local(local), span),
|
(&Place::Local(local), span),
|
||||||
(Shallow(None), Write(WriteKind::StorageDeadOrDrop)),
|
(Shallow(None), Write(WriteKind::StorageDeadOrDrop(
|
||||||
|
StorageDeadOrDrop::LocalStorageDead))),
|
||||||
LocalMutationIsAllowed::Yes,
|
LocalMutationIsAllowed::Yes,
|
||||||
flow_state,
|
flow_state,
|
||||||
);
|
);
|
||||||
|
@ -778,12 +779,21 @@ enum ReadKind {
|
||||||
/// (For informational purposes only)
|
/// (For informational purposes only)
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
enum WriteKind {
|
enum WriteKind {
|
||||||
StorageDeadOrDrop,
|
StorageDeadOrDrop(StorageDeadOrDrop),
|
||||||
MutableBorrow(BorrowKind),
|
MutableBorrow(BorrowKind),
|
||||||
Mutate,
|
Mutate,
|
||||||
Move,
|
Move,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specify whether which case a StorageDeadOrDrop is in.
|
||||||
|
/// (For informational purposes only)
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
enum StorageDeadOrDrop {
|
||||||
|
LocalStorageDead,
|
||||||
|
BoxedStorageDead,
|
||||||
|
Destructor,
|
||||||
|
}
|
||||||
|
|
||||||
/// When checking permissions for a place access, this flag is used to indicate that an immutable
|
/// When checking permissions for a place access, this flag is used to indicate that an immutable
|
||||||
/// local place can be mutated.
|
/// local place can be mutated.
|
||||||
///
|
///
|
||||||
|
@ -1012,7 +1022,8 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
self.access_place(
|
self.access_place(
|
||||||
ContextKind::Drop.new(loc),
|
ContextKind::Drop.new(loc),
|
||||||
(drop_place, span),
|
(drop_place, span),
|
||||||
(Deep, Write(WriteKind::StorageDeadOrDrop)),
|
(Deep, Write(WriteKind::StorageDeadOrDrop(
|
||||||
|
StorageDeadOrDrop::Destructor))),
|
||||||
LocalMutationIsAllowed::Yes,
|
LocalMutationIsAllowed::Yes,
|
||||||
flow_state,
|
flow_state,
|
||||||
);
|
);
|
||||||
|
@ -1039,15 +1050,17 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
// Why? Because we do not schedule/emit
|
// Why? Because we do not schedule/emit
|
||||||
// `Drop(x)` in the MIR unless `x` needs drop in
|
// `Drop(x)` in the MIR unless `x` needs drop in
|
||||||
// the first place.
|
// the first place.
|
||||||
//
|
|
||||||
// FIXME: Its possible this logic actually should
|
|
||||||
// be attached to the `StorageDead` statement
|
|
||||||
// rather than the `Drop`. See discussion on PR
|
|
||||||
// #52782.
|
|
||||||
self.access_place(
|
self.access_place(
|
||||||
ContextKind::Drop.new(loc),
|
ContextKind::Drop.new(loc),
|
||||||
(drop_place, span),
|
(drop_place, span),
|
||||||
(Shallow(None), Write(WriteKind::StorageDeadOrDrop)),
|
(Shallow(None), Write(WriteKind::StorageDeadOrDrop(
|
||||||
|
// rust-lang/rust#52059: distinguish
|
||||||
|
// between invaliding the backing storage
|
||||||
|
// vs running a destructor.
|
||||||
|
//
|
||||||
|
// See also: rust-lang/rust#52782,
|
||||||
|
// specifically #discussion_r206658948
|
||||||
|
StorageDeadOrDrop::BoxedStorageDead))),
|
||||||
LocalMutationIsAllowed::Yes,
|
LocalMutationIsAllowed::Yes,
|
||||||
flow_state,
|
flow_state,
|
||||||
);
|
);
|
||||||
|
@ -1215,14 +1228,13 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
error_reported = true;
|
error_reported = true;
|
||||||
this.report_conflicting_borrow(context, place_span, bk, &borrow)
|
this.report_conflicting_borrow(context, place_span, bk, &borrow)
|
||||||
}
|
}
|
||||||
WriteKind::StorageDeadOrDrop => {
|
WriteKind::StorageDeadOrDrop(_) => {
|
||||||
error_reported = true;
|
error_reported = true;
|
||||||
this.report_borrowed_value_does_not_live_long_enough(
|
this.report_borrowed_value_does_not_live_long_enough(
|
||||||
context,
|
context,
|
||||||
borrow,
|
borrow,
|
||||||
place_span,
|
place_span,
|
||||||
Some(kind),
|
Some(kind))
|
||||||
);
|
|
||||||
}
|
}
|
||||||
WriteKind::Mutate => {
|
WriteKind::Mutate => {
|
||||||
error_reported = true;
|
error_reported = true;
|
||||||
|
@ -1464,7 +1476,7 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether a borrow of this place is invalidated when the function
|
/// Checks whether a borrow of this place is invalidated when the function
|
||||||
/// exits
|
/// exits
|
||||||
fn check_for_invalidation_at_exit(
|
fn check_for_invalidation_at_exit(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -1889,9 +1901,9 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
|
|
||||||
Reservation(wk @ WriteKind::Move)
|
Reservation(wk @ WriteKind::Move)
|
||||||
| Write(wk @ WriteKind::Move)
|
| Write(wk @ WriteKind::Move)
|
||||||
| Reservation(wk @ WriteKind::StorageDeadOrDrop)
|
| Reservation(wk @ WriteKind::StorageDeadOrDrop(_))
|
||||||
| Reservation(wk @ WriteKind::MutableBorrow(BorrowKind::Shared))
|
| Reservation(wk @ WriteKind::MutableBorrow(BorrowKind::Shared))
|
||||||
| Write(wk @ WriteKind::StorageDeadOrDrop)
|
| Write(wk @ WriteKind::StorageDeadOrDrop(_))
|
||||||
| Write(wk @ WriteKind::MutableBorrow(BorrowKind::Shared)) => {
|
| Write(wk @ WriteKind::MutableBorrow(BorrowKind::Shared)) => {
|
||||||
if let Err(_place_err) = self.is_mutable(place, is_local_mutation_allowed) {
|
if let Err(_place_err) = self.is_mutable(place, is_local_mutation_allowed) {
|
||||||
if self.tcx.migrate_borrowck() {
|
if self.tcx.migrate_borrowck() {
|
||||||
|
@ -1906,7 +1918,7 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
error_access = match wk {
|
error_access = match wk {
|
||||||
WriteKind::MutableBorrow(_) => AccessKind::MutableBorrow,
|
WriteKind::MutableBorrow(_) => AccessKind::MutableBorrow,
|
||||||
WriteKind::Move => AccessKind::Move,
|
WriteKind::Move => AccessKind::Move,
|
||||||
WriteKind::StorageDeadOrDrop |
|
WriteKind::StorageDeadOrDrop(_) |
|
||||||
WriteKind::Mutate => AccessKind::Mutate,
|
WriteKind::Mutate => AccessKind::Mutate,
|
||||||
};
|
};
|
||||||
self.report_mutability_error(
|
self.report_mutability_error(
|
||||||
|
|
|
@ -154,7 +154,7 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
|
||||||
format!("borrow later used here, when `{}` is dropped", local_name),
|
format!("borrow later used here, when `{}` is dropped", local_name),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some((WriteKind::StorageDeadOrDrop, place)) = kind_place {
|
if let Some((WriteKind::StorageDeadOrDrop(_), place)) = kind_place {
|
||||||
if let Place::Local(borrowed_local) = place {
|
if let Place::Local(borrowed_local) = place {
|
||||||
let dropped_local_scope = mir.local_decls[local].visibility_scope;
|
let dropped_local_scope = mir.local_decls[local].visibility_scope;
|
||||||
let borrowed_local_scope =
|
let borrowed_local_scope =
|
||||||
|
|
|
@ -16,7 +16,7 @@ use borrow_check::{ReadOrWrite, Activation, Read, Reservation, Write};
|
||||||
use borrow_check::{Context, ContextKind};
|
use borrow_check::{Context, ContextKind};
|
||||||
use borrow_check::{LocalMutationIsAllowed, MutateMode};
|
use borrow_check::{LocalMutationIsAllowed, MutateMode};
|
||||||
use borrow_check::ArtificialField;
|
use borrow_check::ArtificialField;
|
||||||
use borrow_check::{ReadKind, WriteKind};
|
use borrow_check::{ReadKind, WriteKind, StorageDeadOrDrop};
|
||||||
use borrow_check::nll::facts::AllFacts;
|
use borrow_check::nll::facts::AllFacts;
|
||||||
use borrow_check::path_utils::*;
|
use borrow_check::path_utils::*;
|
||||||
use dataflow::move_paths::indexes::BorrowIndex;
|
use dataflow::move_paths::indexes::BorrowIndex;
|
||||||
|
@ -154,7 +154,8 @@ impl<'cg, 'cx, 'tcx, 'gcx> Visitor<'tcx> for InvalidationGenerator<'cg, 'cx, 'tc
|
||||||
self.access_place(
|
self.access_place(
|
||||||
ContextKind::StorageDead.new(location),
|
ContextKind::StorageDead.new(location),
|
||||||
&Place::Local(local),
|
&Place::Local(local),
|
||||||
(Shallow(None), Write(WriteKind::StorageDeadOrDrop)),
|
(Shallow(None), Write(WriteKind::StorageDeadOrDrop(
|
||||||
|
StorageDeadOrDrop::LocalStorageDead))),
|
||||||
LocalMutationIsAllowed::Yes,
|
LocalMutationIsAllowed::Yes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -347,7 +348,8 @@ impl<'cg, 'cx, 'tcx, 'gcx> InvalidationGenerator<'cg, 'cx, 'tcx, 'gcx> {
|
||||||
self.access_place(
|
self.access_place(
|
||||||
ContextKind::Drop.new(loc),
|
ContextKind::Drop.new(loc),
|
||||||
drop_place,
|
drop_place,
|
||||||
(Deep, Write(WriteKind::StorageDeadOrDrop)),
|
(Deep, Write(WriteKind::StorageDeadOrDrop(
|
||||||
|
StorageDeadOrDrop::Destructor))),
|
||||||
LocalMutationIsAllowed::Yes,
|
LocalMutationIsAllowed::Yes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2187,6 +2187,51 @@ fn main() {
|
||||||
```
|
```
|
||||||
"##,
|
"##,
|
||||||
|
|
||||||
|
E0713: r##"
|
||||||
|
This error occurs when an attempt is made to borrow state past the end of the
|
||||||
|
lifetime of a type that implements the `Drop` trait.
|
||||||
|
|
||||||
|
Example of erroneous code:
|
||||||
|
|
||||||
|
```compile_fail,E0713
|
||||||
|
pub struct S<'a> { data: &'a mut String }
|
||||||
|
|
||||||
|
impl<'a> Drop for S<'a> {
|
||||||
|
fn drop(&mut self) { self.data.push_str("being dropped"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo(s: S) -> &mut String { let p = &mut *s.data; p }
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `demo` tries to borrow the string data held within its
|
||||||
|
argument `s` and then return that borrow. However, `S` is
|
||||||
|
declared as implementing `Drop`.
|
||||||
|
|
||||||
|
Structs implementing the `Drop` trait have an implicit destructor that
|
||||||
|
gets called when they go out of scope. This destructor gets exclusive
|
||||||
|
access to the fields of the struct when it runs.
|
||||||
|
|
||||||
|
This means that when `s` reaches the end of `demo`, its destructor
|
||||||
|
gets exclusive access to its `&mut`-borrowed string data. allowing
|
||||||
|
another borrow of that string data (`p`), to exist across the drop of
|
||||||
|
`s` would be a violation of the principle that `&mut`-borrows have
|
||||||
|
exclusive, unaliased access to their referenced data.
|
||||||
|
|
||||||
|
This error can be fixed by changing `demo` so that the destructor does
|
||||||
|
not run while the string-data is borrowed; for example by taking `S`
|
||||||
|
by reference:
|
||||||
|
|
||||||
|
```
|
||||||
|
pub struct S<'a> { data: &'a mut String }
|
||||||
|
|
||||||
|
impl<'a> Drop for S<'a> {
|
||||||
|
fn drop(&mut self) { self.data.push_str("being dropped"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo(s: &mut S) -> &mut String { let p = &mut *(*s).data; p }
|
||||||
|
```
|
||||||
|
"##,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
register_diagnostics! {
|
register_diagnostics! {
|
||||||
|
|
|
@ -573,6 +573,22 @@ pub trait BorrowckErrors<'cx>: Sized + Copy {
|
||||||
self.cancel_if_wrong_origin(err, o)
|
self.cancel_if_wrong_origin(err, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cannot_borrow_across_destructor(
|
||||||
|
self,
|
||||||
|
borrow_span: Span,
|
||||||
|
o: Origin,
|
||||||
|
) -> DiagnosticBuilder<'cx> {
|
||||||
|
let err = struct_span_err!(
|
||||||
|
self,
|
||||||
|
borrow_span,
|
||||||
|
E0713,
|
||||||
|
"borrow may still be in use when destructor runs{OGN}",
|
||||||
|
OGN = o
|
||||||
|
);
|
||||||
|
|
||||||
|
self.cancel_if_wrong_origin(err, o)
|
||||||
|
}
|
||||||
|
|
||||||
fn path_does_not_live_long_enough(
|
fn path_does_not_live_long_enough(
|
||||||
self,
|
self,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue