Auto merge of #135272 - BoxyUwU:generic_arg_infer_reliability_2, r=compiler-errors
Forbid usage of `hir` `Infer` const/ty variants in ambiguous contexts The feature `generic_arg_infer` allows providing `_` as an argument to const generics in order to infer them. This introduces a syntactic ambiguity as to whether generic arguments are type or const arguments. In order to get around this we introduced a fourth `GenericArg` variant, `Infer` used to represent `_` as an argument to generic parameters when we don't know if its a type or a const argument. This made hir visitors that care about `TyKind::Infer` or `ConstArgKind::Infer` very error prone as checking for `TyKind::Infer`s in `visit_ty` would find *some* type infer arguments but not *all* of them as they would sometimes be lowered to `GenericArg::Infer` instead. Additionally the `visit_infer` method would previously only visit `GenericArg::Infer` not *all* infers (e.g. `TyKind::Infer`), this made it very easy to override `visit_infer` and expect it to visit all infers when in reality it would only visit *some* infers. --- This PR aims to fix those issues by making the `TyKind` and `ConstArgKind` types generic over whether the infer types/consts are represented by `Ty/ConstArgKind::Infer` or out of line (e.g. by a `GenericArg::Infer` or accessible by overiding `visit_infer`). We then make HIR Visitors convert all const args and types to the versions where infer vars are stored out of line and call `visit_infer` in cases where a `Ty`/`Const` would previously have had a `Ty/ConstArgKind::Infer` variant: API Summary ```rust enum AmbigArg {} enum Ty/ConstArgKind<Unambig = ()> { ... Infer(Unambig), } impl Ty/ConstArg { fn try_as_ambig_ty/ct(self) -> Option<Ty/ConstArg<AmbigArg>>; } impl Ty/ConstArg<AmbigArg> { fn as_unambig_ty/ct(self) -> Ty/ConstArg; } enum InferKind { Ty(Ty), Const(ConstArg), Ambig(InferArg), } trait Visitor { ... fn visit_ty/const_arg(&mut self, Ty/ConstArg<AmbigArg>) -> Self::Result; fn visit_infer(&mut self, id: HirId, sp: Span, kind: InferKind) -> Self::Result; } // blanket impl'd, not meant to be overriden trait VisitorExt { fn visit_ty/const_arg_unambig(&mut self, Ty/ConstArg) -> Self::Result; } fn walk_unambig_ty/const_arg(&mut V, Ty/ConstArg) -> Self::Result; fn walk_ty/const_arg(&mut V, Ty/ConstArg<AmbigArg>) -> Self::Result; ``` The end result is that `visit_infer` visits *all* infer args and is also the *only* way to visit an infer arg, `visit_ty` and `visit_const_arg` can now no longer encounter a `Ty/ConstArgKind::Infer`. Representing this in the type system means that it is now very difficult to mess things up, either accessing `TyKind::Infer` "just works" and you won't miss *some* type infers- or it doesn't work and you have to look at `visit_infer` or some `GenericArg::Infer` which forces you to think about the full complexity involved. Unfortunately there is no lint right now about explicitly matching on uninhabited variants, I can't find the context for why this is the case 🤷♀️ I'm not convinced the framing of un/ambig ty/consts is necessarily the right one but I'm not sure what would be better. I somewhat like calling them full/partial types based on the fact that `Ty<Partial>`/`Ty<Full>` directly specifies how many of the type kinds are actually represented compared to `Ty<Ambig>` which which leaves that to the reader to figure out based on the logical consequences of it the type being in an ambiguous position. --- tool changes have been modified in their own commits for easier reviewing by anyone getting cc'd from subtree changes. I also attempted to split out "bug fixes arising from the refactoring" into their own commit so they arent lumped in with a big general refactor commit Fixes #112110
This commit is contained in:
commit
8231e8599e
119 changed files with 1056 additions and 669 deletions
|
@ -1,8 +1,8 @@
|
|||
use core::ops::ControlFlow;
|
||||
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::def_id::{DefId, LocalDefId};
|
||||
use rustc_hir::intravisit::{self, Visitor};
|
||||
use rustc_hir::intravisit::{self, Visitor, VisitorExt};
|
||||
use rustc_hir::{self as hir, AmbigArg};
|
||||
use rustc_middle::hir::map::Map;
|
||||
use rustc_middle::hir::nested_filter;
|
||||
use rustc_middle::middle::resolve_bound_vars as rbv;
|
||||
|
@ -48,7 +48,7 @@ fn find_component_for_bound_region<'tcx>(
|
|||
region_def_id: DefId,
|
||||
) -> Option<&'tcx hir::Ty<'tcx>> {
|
||||
FindNestedTypeVisitor { tcx, region_def_id, current_index: ty::INNERMOST }
|
||||
.visit_ty(arg)
|
||||
.visit_ty_unambig(arg)
|
||||
.break_value()
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ impl<'tcx> Visitor<'tcx> for FindNestedTypeVisitor<'tcx> {
|
|||
self.tcx.hir()
|
||||
}
|
||||
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx>) -> Self::Result {
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx, AmbigArg>) -> Self::Result {
|
||||
match arg.kind {
|
||||
hir::TyKind::BareFn(_) => {
|
||||
self.current_index.shift_in(1);
|
||||
|
@ -101,7 +101,7 @@ impl<'tcx> Visitor<'tcx> for FindNestedTypeVisitor<'tcx> {
|
|||
Some(rbv::ResolvedArg::EarlyBound(id)) => {
|
||||
debug!("EarlyBound id={:?}", id);
|
||||
if id.to_def_id() == self.region_def_id {
|
||||
return ControlFlow::Break(arg);
|
||||
return ControlFlow::Break(arg.as_unambig_ty());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ impl<'tcx> Visitor<'tcx> for FindNestedTypeVisitor<'tcx> {
|
|||
if debruijn_index == self.current_index
|
||||
&& id.to_def_id() == self.region_def_id
|
||||
{
|
||||
return ControlFlow::Break(arg);
|
||||
return ControlFlow::Break(arg.as_unambig_ty());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ impl<'tcx> Visitor<'tcx> for FindNestedTypeVisitor<'tcx> {
|
|||
)
|
||||
.is_break()
|
||||
{
|
||||
ControlFlow::Break(arg)
|
||||
ControlFlow::Break(arg.as_unambig_ty())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
};
|
||||
|
@ -210,7 +210,7 @@ impl<'tcx> Visitor<'tcx> for TyPathVisitor<'tcx> {
|
|||
ControlFlow::Continue(())
|
||||
}
|
||||
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx>) -> Self::Result {
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx, AmbigArg>) -> Self::Result {
|
||||
// ignore nested types
|
||||
//
|
||||
// If you have a type like `Foo<'a, &Ty>` we
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use rustc_data_structures::fx::FxIndexSet;
|
||||
use rustc_errors::{ErrorGuaranteed, MultiSpan};
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::intravisit::Visitor;
|
||||
use rustc_hir::intravisit::VisitorExt;
|
||||
use rustc_middle::bug;
|
||||
use rustc_middle::ty::TypeVisitor;
|
||||
use tracing::debug;
|
||||
|
@ -87,7 +87,7 @@ impl<'a, 'tcx> NiceRegionError<'a, 'tcx> {
|
|||
for matching_def_id in v.0 {
|
||||
let mut hir_v =
|
||||
super::static_impl_trait::HirTraitObjectVisitor(&mut traits, matching_def_id);
|
||||
hir_v.visit_ty(impl_self_ty);
|
||||
hir_v.visit_ty_unambig(impl_self_ty);
|
||||
}
|
||||
|
||||
if traits.is_empty() {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
use rustc_data_structures::fx::FxIndexSet;
|
||||
use rustc_errors::{Applicability, Diag, ErrorGuaranteed, MultiSpan, Subdiagnostic};
|
||||
use rustc_hir::def_id::DefId;
|
||||
use rustc_hir::intravisit::{Visitor, walk_ty};
|
||||
use rustc_hir::intravisit::{Visitor, VisitorExt, walk_ty};
|
||||
use rustc_hir::{
|
||||
self as hir, GenericBound, GenericParam, GenericParamKind, Item, ItemKind, Lifetime,
|
||||
self as hir, AmbigArg, GenericBound, GenericParam, GenericParamKind, Item, ItemKind, Lifetime,
|
||||
LifetimeName, LifetimeParamKind, MissingLifetimeKind, Node, TyKind,
|
||||
};
|
||||
use rustc_middle::ty::{
|
||||
|
@ -153,7 +153,7 @@ impl<'a, 'tcx> NiceRegionError<'a, 'tcx> {
|
|||
let mut add_label = true;
|
||||
if let hir::FnRetTy::Return(ty) = fn_decl.output {
|
||||
let mut v = StaticLifetimeVisitor(vec![], tcx.hir());
|
||||
v.visit_ty(ty);
|
||||
v.visit_ty_unambig(ty);
|
||||
if !v.0.is_empty() {
|
||||
span = v.0.clone().into();
|
||||
spans = v.0;
|
||||
|
@ -374,7 +374,7 @@ pub fn suggest_new_region_bound(
|
|||
}
|
||||
}
|
||||
}
|
||||
TyKind::TraitObject(_, lt, _) => {
|
||||
TyKind::TraitObject(_, lt) => {
|
||||
if let LifetimeName::ImplicitObjectLifetimeDefault = lt.res {
|
||||
err.span_suggestion_verbose(
|
||||
fn_return.span.shrink_to_hi(),
|
||||
|
@ -500,7 +500,7 @@ impl<'a, 'tcx> NiceRegionError<'a, 'tcx> {
|
|||
// In that case, only the first one will get suggestions.
|
||||
let mut traits = vec![];
|
||||
let mut hir_v = HirTraitObjectVisitor(&mut traits, *did);
|
||||
hir_v.visit_ty(self_ty);
|
||||
hir_v.visit_ty_unambig(self_ty);
|
||||
!traits.is_empty()
|
||||
})
|
||||
{
|
||||
|
@ -560,7 +560,7 @@ impl<'a, 'tcx> NiceRegionError<'a, 'tcx> {
|
|||
for found_did in found_dids {
|
||||
let mut traits = vec![];
|
||||
let mut hir_v = HirTraitObjectVisitor(&mut traits, *found_did);
|
||||
hir_v.visit_ty(self_ty);
|
||||
hir_v.visit_ty_unambig(self_ty);
|
||||
for &span in &traits {
|
||||
let subdiag = DynTraitConstraintSuggestion { span, ident };
|
||||
subdiag.add_to_diag(err);
|
||||
|
@ -591,12 +591,10 @@ impl<'tcx> TypeVisitor<TyCtxt<'tcx>> for TraitObjectVisitor {
|
|||
pub struct HirTraitObjectVisitor<'a>(pub &'a mut Vec<Span>, pub DefId);
|
||||
|
||||
impl<'a, 'tcx> Visitor<'tcx> for HirTraitObjectVisitor<'a> {
|
||||
fn visit_ty(&mut self, t: &'tcx hir::Ty<'tcx>) {
|
||||
if let TyKind::TraitObject(
|
||||
poly_trait_refs,
|
||||
Lifetime { res: LifetimeName::ImplicitObjectLifetimeDefault, .. },
|
||||
_,
|
||||
) = t.kind
|
||||
fn visit_ty(&mut self, t: &'tcx hir::Ty<'tcx, AmbigArg>) {
|
||||
if let TyKind::TraitObject(poly_trait_refs, lifetime_ptr) = t.kind
|
||||
&& let Lifetime { res: LifetimeName::ImplicitObjectLifetimeDefault, .. } =
|
||||
lifetime_ptr.pointer()
|
||||
{
|
||||
for ptr in poly_trait_refs {
|
||||
if Some(self.1) == ptr.trait_ref.trait_def_id() {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//! Error Reporting for `impl` items that do not match the obligations from their `trait`.
|
||||
|
||||
use rustc_errors::ErrorGuaranteed;
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::def::{Namespace, Res};
|
||||
use rustc_hir::def_id::DefId;
|
||||
use rustc_hir::intravisit::Visitor;
|
||||
use rustc_hir::intravisit::{Visitor, walk_ty};
|
||||
use rustc_hir::{self as hir, AmbigArg};
|
||||
use rustc_middle::hir::nested_filter;
|
||||
use rustc_middle::traits::ObligationCauseCode;
|
||||
use rustc_middle::ty::error::ExpectedFound;
|
||||
|
@ -137,11 +137,13 @@ impl<'tcx> Visitor<'tcx> for TypeParamSpanVisitor<'tcx> {
|
|||
self.tcx.hir()
|
||||
}
|
||||
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx>) {
|
||||
fn visit_ty(&mut self, arg: &'tcx hir::Ty<'tcx, AmbigArg>) {
|
||||
match arg.kind {
|
||||
hir::TyKind::Ref(_, ref mut_ty) => {
|
||||
// We don't want to suggest looking into borrowing `&T` or `&Self`.
|
||||
hir::intravisit::walk_ty(self, mut_ty.ty);
|
||||
if let Some(ambig_ty) = mut_ty.ty.try_as_ambig_ty() {
|
||||
walk_ty(self, ambig_ty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
hir::TyKind::Path(hir::QPath::Resolved(None, path)) => match &path.segments {
|
||||
|
|
|
@ -655,7 +655,7 @@ impl<'tcx> TypeErrCtxt<'_, 'tcx> {
|
|||
&& let ty::Ref(found_region, _, _) = found.kind()
|
||||
&& expected_region.is_bound()
|
||||
&& !found_region.is_bound()
|
||||
&& let hir::TyKind::Infer = arg_hir.kind
|
||||
&& let hir::TyKind::Infer(()) = arg_hir.kind
|
||||
{
|
||||
// If the expected region is late bound, the found region is not, and users are asking compiler
|
||||
// to infer the type, we can suggest adding `: &_`.
|
||||
|
|
|
@ -580,8 +580,8 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
|
|||
self.tcx.hir_node_by_def_id(obligation.cause.body_id)
|
||||
&& let hir::ItemKind::Impl(impl_) = item.kind
|
||||
&& let None = impl_.of_trait
|
||||
&& let hir::TyKind::TraitObject(_, _, syntax) = impl_.self_ty.kind
|
||||
&& let TraitObjectSyntax::None = syntax
|
||||
&& let hir::TyKind::TraitObject(_, tagged_ptr) = impl_.self_ty.kind
|
||||
&& let TraitObjectSyntax::None = tagged_ptr.tag()
|
||||
&& impl_.self_ty.span.edition().at_least_rust_2021()
|
||||
{
|
||||
// Silence the dyn-compatibility error in favor of the missing dyn on
|
||||
|
|
|
@ -11,7 +11,7 @@ use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
|
|||
use rustc_errors::{Applicability, Diag, E0038, E0276, MultiSpan, struct_span_code_err};
|
||||
use rustc_hir::def_id::{DefId, LocalDefId};
|
||||
use rustc_hir::intravisit::Visitor;
|
||||
use rustc_hir::{self as hir, LangItem};
|
||||
use rustc_hir::{self as hir, AmbigArg, LangItem};
|
||||
use rustc_infer::traits::{
|
||||
DynCompatibilityViolation, Obligation, ObligationCause, ObligationCauseCode,
|
||||
PredicateObligation, SelectionError,
|
||||
|
@ -87,9 +87,9 @@ impl<'v> Visitor<'v> for FindExprBySpan<'v> {
|
|||
}
|
||||
}
|
||||
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) {
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
|
||||
if self.span == ty.span {
|
||||
self.ty_result = Some(ty);
|
||||
self.ty_result = Some(ty.as_unambig_ty());
|
||||
} else {
|
||||
hir::intravisit::walk_ty(self, ty);
|
||||
}
|
||||
|
|
|
@ -14,14 +14,13 @@ use rustc_errors::{
|
|||
Applicability, Diag, EmissionGuarantee, MultiSpan, Style, SuggestionStyle, pluralize,
|
||||
struct_span_code_err,
|
||||
};
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::def::{CtorOf, DefKind, Res};
|
||||
use rustc_hir::def_id::DefId;
|
||||
use rustc_hir::intravisit::Visitor;
|
||||
use rustc_hir::intravisit::{Visitor, VisitorExt};
|
||||
use rustc_hir::lang_items::LangItem;
|
||||
use rustc_hir::{
|
||||
CoroutineDesugaring, CoroutineKind, CoroutineSource, Expr, HirId, Node, expr_needs_parens,
|
||||
is_range_literal,
|
||||
self as hir, AmbigArg, CoroutineDesugaring, CoroutineKind, CoroutineSource, Expr, HirId, Node,
|
||||
expr_needs_parens, is_range_literal,
|
||||
};
|
||||
use rustc_infer::infer::{BoundRegionConversionTime, DefineOpaqueTypes, InferCtxt, InferOk};
|
||||
use rustc_middle::hir::map;
|
||||
|
@ -179,7 +178,7 @@ pub fn suggest_restriction<'tcx, G: EmissionGuarantee>(
|
|||
let mut ty_spans = vec![];
|
||||
for input in fn_sig.decl.inputs {
|
||||
ReplaceImplTraitVisitor { ty_spans: &mut ty_spans, param_did: param.def_id }
|
||||
.visit_ty(input);
|
||||
.visit_ty_unambig(input);
|
||||
}
|
||||
// The type param `T: Trait` we will suggest to introduce.
|
||||
let type_param = format!("{type_param_name}: {bound_str}");
|
||||
|
@ -3074,7 +3073,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
|
|||
}
|
||||
if let Some(ty) = ty {
|
||||
match ty.kind {
|
||||
hir::TyKind::TraitObject(traits, _, _) => {
|
||||
hir::TyKind::TraitObject(traits, _) => {
|
||||
let (span, kw) = match traits {
|
||||
[first, ..] if first.span.lo() == ty.span.lo() => {
|
||||
// Missing `dyn` in front of trait object.
|
||||
|
@ -5065,7 +5064,7 @@ pub struct SelfVisitor<'v> {
|
|||
}
|
||||
|
||||
impl<'v> Visitor<'v> for SelfVisitor<'v> {
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) {
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
|
||||
if let hir::TyKind::Path(path) = ty.kind
|
||||
&& let hir::QPath::TypeRelative(inner_ty, segment) = path
|
||||
&& (Some(segment.ident.name) == self.name || self.name.is_none())
|
||||
|
@ -5073,7 +5072,7 @@ impl<'v> Visitor<'v> for SelfVisitor<'v> {
|
|||
&& let hir::QPath::Resolved(None, inner_path) = inner_path
|
||||
&& let Res::SelfTyAlias { .. } = inner_path.res
|
||||
{
|
||||
self.paths.push(ty);
|
||||
self.paths.push(ty.as_unambig_ty());
|
||||
}
|
||||
hir::intravisit::walk_ty(self, ty);
|
||||
}
|
||||
|
@ -5187,7 +5186,7 @@ struct ReplaceImplTraitVisitor<'a> {
|
|||
}
|
||||
|
||||
impl<'a, 'hir> hir::intravisit::Visitor<'hir> for ReplaceImplTraitVisitor<'a> {
|
||||
fn visit_ty(&mut self, t: &'hir hir::Ty<'hir>) {
|
||||
fn visit_ty(&mut self, t: &'hir hir::Ty<'hir, AmbigArg>) {
|
||||
if let hir::TyKind::Path(hir::QPath::Resolved(
|
||||
None,
|
||||
hir::Path { res: Res::Def(_, segment_did), .. },
|
||||
|
@ -5480,7 +5479,7 @@ impl<'v> Visitor<'v> for FindTypeParam {
|
|||
// Skip where-clauses, to avoid suggesting indirection for type parameters found there.
|
||||
}
|
||||
|
||||
fn visit_ty(&mut self, ty: &hir::Ty<'_>) {
|
||||
fn visit_ty(&mut self, ty: &hir::Ty<'_, AmbigArg>) {
|
||||
// We collect the spans of all uses of the "bare" type param, like in `field: T` or
|
||||
// `field: (T, T)` where we could make `T: ?Sized` while skipping cases that are known to be
|
||||
// valid like `field: &'a T` or `field: *mut T` and cases that *might* have further `Sized`
|
||||
|
|
|
@ -6,11 +6,10 @@ use rustc_errors::{
|
|||
Applicability, Diag, DiagCtxtHandle, DiagMessage, DiagStyledString, Diagnostic,
|
||||
EmissionGuarantee, IntoDiagArg, Level, MultiSpan, SubdiagMessageOp, Subdiagnostic,
|
||||
};
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::def::DefKind;
|
||||
use rustc_hir::def_id::{DefId, LocalDefId};
|
||||
use rustc_hir::intravisit::{Visitor, walk_ty};
|
||||
use rustc_hir::{FnRetTy, GenericParamKind, Node};
|
||||
use rustc_hir::intravisit::{Visitor, VisitorExt, walk_ty};
|
||||
use rustc_hir::{self as hir, AmbigArg, FnRetTy, GenericParamKind, Node};
|
||||
use rustc_macros::{Diagnostic, Subdiagnostic};
|
||||
use rustc_middle::ty::print::{PrintTraitRefExt as _, TraitRefPrintOnlyTraitPath};
|
||||
use rustc_middle::ty::{self, Binder, ClosureKind, FnSig, PolyTraitRef, Region, Ty, TyCtxt};
|
||||
|
@ -579,7 +578,7 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
|
|||
}
|
||||
|
||||
impl<'v> Visitor<'v> for ImplicitLifetimeFinder {
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) {
|
||||
fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
|
||||
let make_suggestion = |ident: Ident| {
|
||||
if ident.name == kw::Empty && ident.span.is_empty() {
|
||||
format!("{}, ", self.suggestion_param_name)
|
||||
|
@ -642,16 +641,16 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
|
|||
if let Some(fn_decl) = node.fn_decl()
|
||||
&& let hir::FnRetTy::Return(ty) = fn_decl.output
|
||||
{
|
||||
visitor.visit_ty(ty);
|
||||
visitor.visit_ty_unambig(ty);
|
||||
}
|
||||
if visitor.suggestions.is_empty() {
|
||||
// Do not suggest constraining the `&self` param, but rather the return type.
|
||||
// If that is wrong (because it is not sufficient), a follow up error will tell the
|
||||
// user to fix it. This way we lower the chances of *over* constraining, but still
|
||||
// get the cake of "correctly" contrained in two steps.
|
||||
visitor.visit_ty(self.ty_sup);
|
||||
visitor.visit_ty_unambig(self.ty_sup);
|
||||
}
|
||||
visitor.visit_ty(self.ty_sub);
|
||||
visitor.visit_ty_unambig(self.ty_sub);
|
||||
if visitor.suggestions.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue