Rollup merge of #145041 - lcnr:borrowck-limitations-error, r=BoxyUwU
rework GAT borrowck limitation error The old one depends on the `ConstraintCategory` of the constraint which meant we did not emit this note if we had to prove the higher ranked trait bound due to e.g. normalization. This made it annoying brittle and caused MIR borrowck errors to be order dependent, fixes the issue in https://github.com/rust-lang/rust/pull/140737#discussion_r2259592651. r? types cc ```@amandasystems```
This commit is contained in:
commit
f3f1847e40
22 changed files with 133 additions and 58 deletions
|
|
@ -90,7 +90,7 @@ borrowck_lifetime_constraints_error =
|
|||
lifetime may not live long enough
|
||||
|
||||
borrowck_limitations_implies_static =
|
||||
due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
|
||||
borrowck_move_closure_suggestion =
|
||||
consider adding 'move' keyword before the nested closure
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use rustc_abi::{FieldIdx, VariantIdx};
|
|||
use rustc_data_structures::fx::FxIndexMap;
|
||||
use rustc_errors::{Applicability, Diag, EmissionGuarantee, MultiSpan, listify};
|
||||
use rustc_hir::def::{CtorKind, Namespace};
|
||||
use rustc_hir::{self as hir, CoroutineKind, LangItem};
|
||||
use rustc_hir::{
|
||||
self as hir, CoroutineKind, GenericBound, LangItem, WhereBoundPredicate, WherePredicateKind,
|
||||
};
|
||||
use rustc_index::{IndexSlice, IndexVec};
|
||||
use rustc_infer::infer::{BoundRegionConversionTime, NllRegionVariableOrigin};
|
||||
use rustc_infer::traits::SelectionError;
|
||||
|
|
@ -658,25 +660,66 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
|
|||
|
||||
/// Add a note to region errors and borrow explanations when higher-ranked regions in predicates
|
||||
/// implicitly introduce an "outlives `'static`" constraint.
|
||||
///
|
||||
/// This is very similar to `fn suggest_static_lifetime_for_gat_from_hrtb` which handles this
|
||||
/// note for failed type tests instead of outlives errors.
|
||||
fn add_placeholder_from_predicate_note<G: EmissionGuarantee>(
|
||||
&self,
|
||||
err: &mut Diag<'_, G>,
|
||||
diag: &mut Diag<'_, G>,
|
||||
path: &[OutlivesConstraint<'tcx>],
|
||||
) {
|
||||
let predicate_span = path.iter().find_map(|constraint| {
|
||||
let tcx = self.infcx.tcx;
|
||||
let Some((gat_hir_id, generics)) = path.iter().find_map(|constraint| {
|
||||
let outlived = constraint.sub;
|
||||
if let Some(origin) = self.regioncx.definitions.get(outlived)
|
||||
&& let NllRegionVariableOrigin::Placeholder(_) = origin.origin
|
||||
&& let ConstraintCategory::Predicate(span) = constraint.category
|
||||
&& let NllRegionVariableOrigin::Placeholder(placeholder) = origin.origin
|
||||
&& let Some(id) = placeholder.bound.kind.get_id()
|
||||
&& let Some(placeholder_id) = id.as_local()
|
||||
&& let gat_hir_id = tcx.local_def_id_to_hir_id(placeholder_id)
|
||||
&& let Some(generics_impl) =
|
||||
tcx.parent_hir_node(tcx.parent_hir_id(gat_hir_id)).generics()
|
||||
{
|
||||
Some(span)
|
||||
Some((gat_hir_id, generics_impl))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(span) = predicate_span {
|
||||
err.span_note(span, "due to current limitations in the borrow checker, this implies a `'static` lifetime");
|
||||
// Look for the where-bound which introduces the placeholder.
|
||||
// As we're using the HIR, we need to handle both `for<'a> T: Trait<'a>`
|
||||
// and `T: for<'a> Trait`<'a>.
|
||||
for pred in generics.predicates {
|
||||
let WherePredicateKind::BoundPredicate(WhereBoundPredicate {
|
||||
bound_generic_params,
|
||||
bounds,
|
||||
..
|
||||
}) = pred.kind
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if bound_generic_params
|
||||
.iter()
|
||||
.rfind(|bgp| tcx.local_def_id_to_hir_id(bgp.def_id) == gat_hir_id)
|
||||
.is_some()
|
||||
{
|
||||
diag.span_note(pred.span, fluent::borrowck_limitations_implies_static);
|
||||
return;
|
||||
}
|
||||
for bound in bounds.iter() {
|
||||
if let GenericBound::Trait(bound) = bound {
|
||||
if bound
|
||||
.bound_generic_params
|
||||
.iter()
|
||||
.rfind(|bgp| tcx.local_def_id_to_hir_id(bgp.def_id) == gat_hir_id)
|
||||
.is_some()
|
||||
{
|
||||
diag.span_note(bound.span, fluent::borrowck_limitations_implies_static);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,7 +215,6 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
|
|||
diag: &mut Diag<'_>,
|
||||
lower_bound: RegionVid,
|
||||
) {
|
||||
let mut suggestions = vec![];
|
||||
let tcx = self.infcx.tcx;
|
||||
|
||||
// find generic associated types in the given region 'lower_bound'
|
||||
|
|
@ -237,9 +236,11 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
|
|||
.collect::<Vec<_>>();
|
||||
debug!(?gat_id_and_generics);
|
||||
|
||||
// find higher-ranked trait bounds bounded to the generic associated types
|
||||
// Look for the where-bound which introduces the placeholder.
|
||||
// As we're using the HIR, we need to handle both `for<'a> T: Trait<'a>`
|
||||
// and `T: for<'a> Trait`<'a>.
|
||||
let mut hrtb_bounds = vec![];
|
||||
gat_id_and_generics.iter().flatten().for_each(|(gat_hir_id, generics)| {
|
||||
gat_id_and_generics.iter().flatten().for_each(|&(gat_hir_id, generics)| {
|
||||
for pred in generics.predicates {
|
||||
let BoundPredicate(WhereBoundPredicate { bound_generic_params, bounds, .. }) =
|
||||
pred.kind
|
||||
|
|
@ -248,17 +249,32 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
|
|||
};
|
||||
if bound_generic_params
|
||||
.iter()
|
||||
.rfind(|bgp| tcx.local_def_id_to_hir_id(bgp.def_id) == *gat_hir_id)
|
||||
.rfind(|bgp| tcx.local_def_id_to_hir_id(bgp.def_id) == gat_hir_id)
|
||||
.is_some()
|
||||
{
|
||||
for bound in *bounds {
|
||||
hrtb_bounds.push(bound);
|
||||
}
|
||||
} else {
|
||||
for bound in *bounds {
|
||||
if let Trait(trait_bound) = bound {
|
||||
if trait_bound
|
||||
.bound_generic_params
|
||||
.iter()
|
||||
.rfind(|bgp| tcx.local_def_id_to_hir_id(bgp.def_id) == gat_hir_id)
|
||||
.is_some()
|
||||
{
|
||||
hrtb_bounds.push(bound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
debug!(?hrtb_bounds);
|
||||
|
||||
let mut suggestions = vec![];
|
||||
hrtb_bounds.iter().for_each(|bound| {
|
||||
let Trait(PolyTraitRef { trait_ref, span: trait_span, .. }) = bound else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -22,12 +22,6 @@ LL | force_send(async_load(¬_static));
|
|||
...
|
||||
LL | }
|
||||
| - `not_static` dropped here while still borrowed
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/implementation-not-general-enough-ice-133252.rs:16:18
|
||||
|
|
||||
LL | fn force_send<T: Send>(_: T) {}
|
||||
| ^^^^
|
||||
|
||||
error: aborting due to 2 previous errors
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ LL | print_items::<WindowsMut<'_>>(windows);
|
|||
LL | }
|
||||
| - temporary value is freed at the end of this statement
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-implied-1.rs:26:26
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-implied-1.rs:26:5
|
||||
|
|
||||
LL | for<'a> I::Item<'a>: Debug,
|
||||
| ^^^^^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 1 previous error
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ LL | let _next = iter2.next();
|
|||
= note: requirement occurs because of a mutable reference to `Eat<&mut I, F>`
|
||||
= note: mutable references are invariant over their type parameter
|
||||
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
|
||||
= note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-implied-2.rs:31:8
|
||||
|
|
||||
LL | F: FnMut(I::Item<'_>),
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 1 previous error
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ LL | trivial_bound(iter);
|
|||
| `iter` escapes the function body here
|
||||
| argument requires that `'1` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-implied-3.rs:14:26
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-implied-3.rs:14:5
|
||||
|
|
||||
LL | for<'a> I::Item<'a>: Sized,
|
||||
| ^^^^^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 1 previous error
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ LL | | let _x = x;
|
|||
LL | | };
|
||||
| |_____^
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/collectivity-regression.rs:11:16
|
||||
|
|
||||
LL | for<'a> T: Get<Value<'a> = ()>,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ error: `Self` does not live long enough
|
|||
|
|
||||
LL | <B as FromLendingIterator<A>>::from_iter(self)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/lending_iterator.rs:4:21
|
||||
|
|
||||
LL | fn from_iter<T: for<'x> LendingIterator<Item<'x> = A>>(iter: T) -> Self;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 2 previous errors
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ LL | fn give_some<'a>() {
|
|||
LL | want_hrtb::<&'a u32>()
|
||||
| ^^^^^^^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-just-for-static.rs:9:15
|
||||
|
|
||||
LL | where T : for<'a> Foo<&'a isize>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ LL | fn foo_hrtb_bar_not<'b, T>(mut t: T)
|
|||
LL | foo_hrtb_bar_not(&mut t);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ requires that `'b` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/hrtb-perfect-forwarding.rs:37:8
|
||||
|
|
||||
LL | T: for<'a> Foo<&'a isize> + Bar<&'b isize>,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ LL | fn test_lifetime<'lt, T: Trait>(_: Foo<&'lt u8>) {}
|
|||
| | |
|
||||
| | lifetime `'lt` defined here
|
||||
| requires that `'lt` must outlive `'static`
|
||||
|
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/normalization-placeholder-leak.rs:19:5
|
||||
|
|
||||
LL | for<'x> T::Ty<'x>: Sized;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: lifetime may not live long enough
|
||||
--> $DIR/normalization-placeholder-leak.rs:38:5
|
||||
|
|
@ -39,6 +45,12 @@ LL | fn test_alias<'lt, T: AnotherTrait>(_: Foo<T::Ty2::<'lt>>) {}
|
|||
| | |
|
||||
| | lifetime `'lt` defined here
|
||||
| requires that `'lt` must outlive `'static`
|
||||
|
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/normalization-placeholder-leak.rs:19:5
|
||||
|
|
||||
LL | for<'x> T::Ty<'x>: Sized;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 6 previous errors
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ LL | fn bar<'a>() {
|
|||
LL | foo::<&'a i32>();
|
||||
| ^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/issue-26217.rs:1:30
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/issue-26217.rs:1:19
|
||||
|
|
||||
LL | fn foo<T>() where for<'a> T: 'a {}
|
||||
| ^^
|
||||
| ^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 1 previous error
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ impl<T> ProjectedMyTrait for T
|
|||
where
|
||||
T: Project,
|
||||
for<'a> T::Projected<'a>: MyTrait,
|
||||
//~^ NOTE due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
//~| NOTE due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
//~^ NOTE due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
//~| NOTE due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
{}
|
||||
|
||||
fn require_trait<T: MyTrait>(_: T) {}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ impl<T> ProjectedMyTrait for T
|
|||
where
|
||||
T: Project,
|
||||
for<'a> T::Projected<'a>: MyTrait,
|
||||
//~^ NOTE due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
//~| NOTE due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
//~^ NOTE due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
//~| NOTE due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
{}
|
||||
|
||||
fn require_trait<T: MyTrait>(_: T) {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ error: `T` does not live long enough
|
|||
LL | require_trait(wrap);
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/issue-105507.rs:27:35
|
||||
|
|
||||
LL | for<'a> T::Projected<'a>: MyTrait,
|
||||
|
|
@ -20,7 +20,7 @@ error: `U` does not live long enough
|
|||
LL | require_trait(wrap1);
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/issue-105507.rs:27:35
|
||||
|
|
||||
LL | for<'a> T::Projected<'a>: MyTrait,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ LL | baz(f);
|
|||
= note: requirement occurs because of a mutable pointer to `&u32`
|
||||
= note: mutable pointers are invariant over their type parameter
|
||||
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/closure-arg-type-mismatch.rs:8:11
|
||||
|
|
||||
LL | fn baz<F: Fn(*mut &u32)>(_: F) {}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ LL | assert_static_via_hrtb_with_assoc_type(&&local);
|
|||
LL | }
|
||||
| - `local` dropped here while still borrowed
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/local-outlives-static-via-hrtb.rs:15:53
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/local-outlives-static-via-hrtb.rs:15:42
|
||||
|
|
||||
LL | fn assert_static_via_hrtb<G>(_: G) where for<'a> G: Outlives<'a> {}
|
||||
| ^^^^^^^^^^^^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error[E0597]: `local` does not live long enough
|
||||
--> $DIR/local-outlives-static-via-hrtb.rs:25:45
|
||||
|
|
@ -32,11 +32,11 @@ LL | assert_static_via_hrtb_with_assoc_type(&&local);
|
|||
LL | }
|
||||
| - `local` dropped here while still borrowed
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/local-outlives-static-via-hrtb.rs:19:20
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/local-outlives-static-via-hrtb.rs:19:5
|
||||
|
|
||||
LL | for<'a> &'a T: Reference<AssociatedType = &'a ()>,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 2 previous errors
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ LL | let b = |_| &a;
|
|||
LL | }
|
||||
| - `a` dropped here while still borrowed
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:20:22
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:20:11
|
||||
|
|
||||
LL | fn bad<F: Fn(&()) -> &()>(_: F) {}
|
||||
| ^^^
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
||||
error: implementation of `Fn` is not general enough
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:13:5
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ LL | let b = |_| &a;
|
|||
LL | }
|
||||
| - `a` dropped here while still borrowed
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:20:22
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:20:11
|
||||
|
|
||||
LL | fn bad<F: Fn(&()) -> &()>(_: F) {}
|
||||
| ^^^
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
||||
error: implementation of `Fn` is not general enough
|
||||
--> $DIR/location-insensitive-scopes-issue-117146.rs:13:5
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ LL | fn test2<'a>() {
|
|||
LL | outlives_forall::<Value<'a>>();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/type-test-universe.rs:6:16
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/type-test-universe.rs:6:5
|
||||
|
|
||||
LL | for<'u> T: 'u,
|
||||
| ^^
|
||||
| ^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 2 previous errors
|
||||
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ LL | unsafe { extend_hrtb(src) }
|
|||
| `src` escapes the function body here
|
||||
| argument requires that `'a` must outlive `'static`
|
||||
|
|
||||
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
||||
--> $DIR/reject_lifetime_extension.rs:85:25
|
||||
note: due to a current limitation of the type system, this implies a `'static` lifetime
|
||||
--> $DIR/reject_lifetime_extension.rs:85:9
|
||||
|
|
||||
LL | for<'b> &'b u8: TransmuteFrom<&'a u8>,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to 8 previous errors
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue