Merge commit 'f712eb5cdc' into clippy-subtree-update
This commit is contained in:
parent
4847c40c8b
commit
6ced8c33c0
248 changed files with 5023 additions and 900 deletions
|
|
@ -1,6 +1,10 @@
|
|||
use crate::ClippyConfiguration;
|
||||
use crate::msrvs::Msrv;
|
||||
use crate::types::{DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename};
|
||||
use crate::types::{
|
||||
DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering,
|
||||
SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind,
|
||||
SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
|
||||
};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_session::Session;
|
||||
use rustc_span::edit_distance::edit_distance;
|
||||
|
|
@ -17,8 +21,9 @@ use std::{cmp, env, fmt, fs, io};
|
|||
#[rustfmt::skip]
|
||||
const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[
|
||||
"KiB", "MiB", "GiB", "TiB", "PiB", "EiB",
|
||||
"MHz", "GHz", "THz",
|
||||
"AccessKit",
|
||||
"CoreFoundation", "CoreGraphics", "CoreText",
|
||||
"CoAP", "CoreFoundation", "CoreGraphics", "CoreText",
|
||||
"DevOps",
|
||||
"Direct2D", "Direct3D", "DirectWrite", "DirectX",
|
||||
"ECMAScript",
|
||||
|
|
@ -46,6 +51,29 @@ const DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS: &[&str] = &["i", "j", "x", "y", "z
|
|||
const DEFAULT_ALLOWED_PREFIXES: &[&str] = &["to", "as", "into", "from", "try_into", "try_from"];
|
||||
const DEFAULT_ALLOWED_TRAITS_WITH_RENAMED_PARAMS: &[&str] =
|
||||
&["core::convert::From", "core::convert::TryFrom", "core::str::FromStr"];
|
||||
const DEFAULT_MODULE_ITEM_ORDERING_GROUPS: &[(&str, &[SourceItemOrderingModuleItemKind])] = {
|
||||
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
||||
use SourceItemOrderingModuleItemKind::*;
|
||||
&[
|
||||
("modules", &[ExternCrate, Mod, ForeignMod]),
|
||||
("use", &[Use]),
|
||||
("macros", &[Macro]),
|
||||
("global_asm", &[GlobalAsm]),
|
||||
("UPPER_SNAKE_CASE", &[Static, Const]),
|
||||
("PascalCase", &[TyAlias, Enum, Struct, Union, Trait, TraitAlias, Impl]),
|
||||
("lower_snake_case", &[Fn]),
|
||||
]
|
||||
};
|
||||
const DEFAULT_TRAIT_ASSOC_ITEM_KINDS_ORDER: &[SourceItemOrderingTraitAssocItemKind] = {
|
||||
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
||||
use SourceItemOrderingTraitAssocItemKind::*;
|
||||
&[Const, Type, Fn]
|
||||
};
|
||||
const DEFAULT_SOURCE_ITEM_ORDERING: &[SourceItemOrderingCategory] = {
|
||||
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
||||
use SourceItemOrderingCategory::*;
|
||||
&[Enum, Impl, Module, Struct, Trait]
|
||||
};
|
||||
|
||||
/// Conf with parse errors
|
||||
#[derive(Default)]
|
||||
|
|
@ -102,7 +130,9 @@ pub fn sanitize_explanation(raw_docs: &str) -> String {
|
|||
// Remove tags and hidden code:
|
||||
let mut explanation = String::with_capacity(128);
|
||||
let mut in_code = false;
|
||||
for line in raw_docs.lines().map(str::trim) {
|
||||
for line in raw_docs.lines() {
|
||||
let line = line.strip_prefix(' ').unwrap_or(line);
|
||||
|
||||
if let Some(lang) = line.strip_prefix("```") {
|
||||
let tag = lang.split_once(',').map_or(lang, |(left, _)| left);
|
||||
if !in_code && matches!(tag, "" | "rust" | "ignore" | "should_panic" | "no_run" | "compile_fail") {
|
||||
|
|
@ -530,6 +560,9 @@ define_Conf! {
|
|||
/// crate. For example, `pub(crate)` items.
|
||||
#[lints(missing_docs_in_private_items)]
|
||||
missing_docs_in_crate_items: bool = false,
|
||||
/// The named groupings of different source item kinds within modules.
|
||||
#[lints(arbitrary_source_item_ordering)]
|
||||
module_item_order_groupings: SourceItemOrderingModuleItemGroupings = DEFAULT_MODULE_ITEM_ORDERING_GROUPS.into(),
|
||||
/// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
|
||||
#[default_text = "current version"]
|
||||
#[lints(
|
||||
|
|
@ -570,6 +603,7 @@ define_Conf! {
|
|||
manual_try_fold,
|
||||
map_clone,
|
||||
map_unwrap_or,
|
||||
map_with_unused_argument_over_ranges,
|
||||
match_like_matches_macro,
|
||||
mem_replace_with_default,
|
||||
missing_const_for_fn,
|
||||
|
|
@ -608,6 +642,9 @@ define_Conf! {
|
|||
/// The maximum number of single char bindings a scope may have
|
||||
#[lints(many_single_char_names)]
|
||||
single_char_binding_names_threshold: u64 = 4,
|
||||
/// Which kind of elements should be ordered internally, possible values being `enum`, `impl`, `module`, `struct`, `trait`.
|
||||
#[lints(arbitrary_source_item_ordering)]
|
||||
source_item_ordering: SourceItemOrdering = DEFAULT_SOURCE_ITEM_ORDERING.into(),
|
||||
/// The maximum allowed stack size for functions in bytes
|
||||
#[lints(large_stack_frames)]
|
||||
stack_size_threshold: u64 = 512_000,
|
||||
|
|
@ -637,6 +674,9 @@ define_Conf! {
|
|||
/// The maximum number of lines a function or method can have
|
||||
#[lints(too_many_lines)]
|
||||
too_many_lines_threshold: u64 = 100,
|
||||
/// The order of associated items in traits.
|
||||
#[lints(arbitrary_source_item_ordering)]
|
||||
trait_assoc_item_kinds_order: SourceItemOrderingTraitAssocItemKinds = DEFAULT_TRAIT_ASSOC_ITEM_KINDS_ORDER.into(),
|
||||
/// The maximum size (in bytes) to consider a `Copy` type for passing by value instead of by
|
||||
/// reference. By default there is no limit
|
||||
#[default_text = "target_pointer_width * 2"]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ extern crate rustc_driver;
|
|||
extern crate rustc_errors;
|
||||
extern crate rustc_session;
|
||||
extern crate rustc_span;
|
||||
extern crate smallvec;
|
||||
|
||||
mod conf;
|
||||
mod metadata;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use rustc_attr::parse_version;
|
|||
use rustc_session::{RustcVersion, Session};
|
||||
use rustc_span::{Symbol, sym};
|
||||
use serde::Deserialize;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! msrv_aliases {
|
||||
|
|
@ -18,7 +19,7 @@ macro_rules! msrv_aliases {
|
|||
// names may refer to stabilized feature flags or library items
|
||||
msrv_aliases! {
|
||||
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY }
|
||||
1,82,0 { IS_NONE_OR }
|
||||
1,82,0 { IS_NONE_OR, REPEAT_N }
|
||||
1,81,0 { LINT_REASONS_STABILIZATION }
|
||||
1,80,0 { BOX_INTO_ITER}
|
||||
1,77,0 { C_STR_LITERALS }
|
||||
|
|
@ -54,7 +55,7 @@ msrv_aliases! {
|
|||
1,33,0 { UNDERSCORE_IMPORTS }
|
||||
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
|
||||
1,29,0 { ITER_FLATTEN }
|
||||
1,28,0 { FROM_BOOL }
|
||||
1,28,0 { FROM_BOOL, REPEAT_WITH }
|
||||
1,27,0 { ITERATOR_TRY_FOLD }
|
||||
1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN }
|
||||
1,24,0 { IS_ASCII_DIGIT }
|
||||
|
|
@ -67,7 +68,7 @@ msrv_aliases! {
|
|||
/// Tracks the current MSRV from `clippy.toml`, `Cargo.toml` or set via `#[clippy::msrv]`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Msrv {
|
||||
stack: Vec<RustcVersion>,
|
||||
stack: SmallVec<[RustcVersion; 2]>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Msrv {
|
||||
|
|
@ -87,14 +88,14 @@ impl<'de> Deserialize<'de> for Msrv {
|
|||
{
|
||||
let v = String::deserialize(deserializer)?;
|
||||
parse_version(Symbol::intern(&v))
|
||||
.map(|v| Msrv { stack: vec![v] })
|
||||
.map(|v| Msrv { stack: smallvec![v] })
|
||||
.ok_or_else(|| serde::de::Error::custom("not a valid Rust version"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Msrv {
|
||||
pub fn empty() -> Msrv {
|
||||
Msrv { stack: Vec::new() }
|
||||
Msrv { stack: SmallVec::new() }
|
||||
}
|
||||
|
||||
pub fn read_cargo(&mut self, sess: &Session) {
|
||||
|
|
@ -103,7 +104,7 @@ impl Msrv {
|
|||
.and_then(|v| parse_version(Symbol::intern(&v)));
|
||||
|
||||
match (self.current(), cargo_msrv) {
|
||||
(None, Some(cargo_msrv)) => self.stack = vec![cargo_msrv],
|
||||
(None, Some(cargo_msrv)) => self.stack = smallvec![cargo_msrv],
|
||||
(Some(clippy_msrv), Some(cargo_msrv)) => {
|
||||
if clippy_msrv != cargo_msrv {
|
||||
sess.dcx().warn(format!(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use serde::de::{self, Deserializer, Visitor};
|
||||
use serde::{Deserialize, Serialize, ser};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -102,6 +103,306 @@ impl<'de> Deserialize<'de> for MacroMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the item categories that can be ordered by the source ordering lint.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SourceItemOrderingCategory {
|
||||
Enum,
|
||||
Impl,
|
||||
Module,
|
||||
Struct,
|
||||
Trait,
|
||||
}
|
||||
|
||||
/// Represents which item categories are enabled for ordering.
|
||||
///
|
||||
/// The [`Deserialize`] implementation checks that there are no duplicates in
|
||||
/// the user configuration.
|
||||
pub struct SourceItemOrdering(Vec<SourceItemOrderingCategory>);
|
||||
|
||||
impl SourceItemOrdering {
|
||||
pub fn contains(&self, category: &SourceItemOrderingCategory) -> bool {
|
||||
self.0.contains(category)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for SourceItemOrdering
|
||||
where
|
||||
T: Into<Vec<SourceItemOrderingCategory>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for SourceItemOrdering {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SourceItemOrdering {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let items = Vec::<SourceItemOrderingCategory>::deserialize(deserializer)?;
|
||||
let mut items_set = std::collections::HashSet::new();
|
||||
|
||||
for item in &items {
|
||||
if items_set.contains(item) {
|
||||
return Err(de::Error::custom(format!(
|
||||
"The category \"{item:?}\" was enabled more than once in the source ordering configuration."
|
||||
)));
|
||||
}
|
||||
items_set.insert(item);
|
||||
}
|
||||
|
||||
Ok(Self(items))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SourceItemOrdering {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: ser::Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the items that can occur within a module.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SourceItemOrderingModuleItemKind {
|
||||
ExternCrate,
|
||||
Mod,
|
||||
ForeignMod,
|
||||
Use,
|
||||
Macro,
|
||||
GlobalAsm,
|
||||
Static,
|
||||
Const,
|
||||
TyAlias,
|
||||
Enum,
|
||||
Struct,
|
||||
Union,
|
||||
Trait,
|
||||
TraitAlias,
|
||||
Impl,
|
||||
Fn,
|
||||
}
|
||||
|
||||
impl SourceItemOrderingModuleItemKind {
|
||||
pub fn all_variants() -> Vec<Self> {
|
||||
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
||||
use SourceItemOrderingModuleItemKind::*;
|
||||
vec![
|
||||
ExternCrate,
|
||||
Mod,
|
||||
ForeignMod,
|
||||
Use,
|
||||
Macro,
|
||||
GlobalAsm,
|
||||
Static,
|
||||
Const,
|
||||
TyAlias,
|
||||
Enum,
|
||||
Struct,
|
||||
Union,
|
||||
Trait,
|
||||
TraitAlias,
|
||||
Impl,
|
||||
Fn,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the configured ordering of items within a module.
|
||||
///
|
||||
/// The [`Deserialize`] implementation checks that no item kinds have been
|
||||
/// omitted and that there are no duplicates in the user configuration.
|
||||
#[derive(Clone)]
|
||||
pub struct SourceItemOrderingModuleItemGroupings {
|
||||
groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)>,
|
||||
lut: HashMap<SourceItemOrderingModuleItemKind, usize>,
|
||||
}
|
||||
|
||||
impl SourceItemOrderingModuleItemGroupings {
|
||||
fn build_lut(
|
||||
groups: &[(String, Vec<SourceItemOrderingModuleItemKind>)],
|
||||
) -> HashMap<SourceItemOrderingModuleItemKind, usize> {
|
||||
let mut lut = HashMap::new();
|
||||
for (group_index, (_, items)) in groups.iter().enumerate() {
|
||||
for item in items {
|
||||
lut.insert(item.clone(), group_index);
|
||||
}
|
||||
}
|
||||
lut
|
||||
}
|
||||
|
||||
pub fn module_level_order_of(&self, item: &SourceItemOrderingModuleItemKind) -> Option<usize> {
|
||||
self.lut.get(item).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[(&str, &[SourceItemOrderingModuleItemKind])]> for SourceItemOrderingModuleItemGroupings {
|
||||
fn from(value: &[(&str, &[SourceItemOrderingModuleItemKind])]) -> Self {
|
||||
let groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)> =
|
||||
value.iter().map(|item| (item.0.to_string(), item.1.to_vec())).collect();
|
||||
let lut = Self::build_lut(&groups);
|
||||
Self { groups, lut }
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for SourceItemOrderingModuleItemGroupings {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.groups.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SourceItemOrderingModuleItemGroupings {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let groups = Vec::<(String, Vec<SourceItemOrderingModuleItemKind>)>::deserialize(deserializer)?;
|
||||
let items_total: usize = groups.iter().map(|(_, v)| v.len()).sum();
|
||||
let lut = Self::build_lut(&groups);
|
||||
|
||||
let mut expected_items = SourceItemOrderingModuleItemKind::all_variants();
|
||||
for item in lut.keys() {
|
||||
expected_items.retain(|i| i != item);
|
||||
}
|
||||
|
||||
let all_items = SourceItemOrderingModuleItemKind::all_variants();
|
||||
if expected_items.is_empty() && items_total == all_items.len() {
|
||||
let Some(use_group_index) = lut.get(&SourceItemOrderingModuleItemKind::Use) else {
|
||||
return Err(de::Error::custom("Error in internal LUT."));
|
||||
};
|
||||
let Some((_, use_group_items)) = groups.get(*use_group_index) else {
|
||||
return Err(de::Error::custom("Error in internal LUT."));
|
||||
};
|
||||
if use_group_items.len() > 1 {
|
||||
return Err(de::Error::custom(
|
||||
"The group containing the \"use\" item kind may not contain any other item kinds. \
|
||||
The \"use\" items will (generally) be sorted by rustfmt already. \
|
||||
Therefore it makes no sense to implement linting rules that may conflict with rustfmt.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self { groups, lut })
|
||||
} else if items_total != all_items.len() {
|
||||
Err(de::Error::custom(format!(
|
||||
"Some module item kinds were configured more than once, or were missing, in the source ordering configuration. \
|
||||
The module item kinds are: {all_items:?}"
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom(format!(
|
||||
"Not all module item kinds were part of the configured source ordering rule. \
|
||||
All item kinds must be provided in the config, otherwise the required source ordering would remain ambiguous. \
|
||||
The module item kinds are: {all_items:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SourceItemOrderingModuleItemGroupings {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: ser::Serializer,
|
||||
{
|
||||
self.groups.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all kinds of trait associated items.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SourceItemOrderingTraitAssocItemKind {
|
||||
Const,
|
||||
Fn,
|
||||
Type,
|
||||
}
|
||||
|
||||
impl SourceItemOrderingTraitAssocItemKind {
|
||||
pub fn all_variants() -> Vec<Self> {
|
||||
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
||||
use SourceItemOrderingTraitAssocItemKind::*;
|
||||
vec![Const, Fn, Type]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the order in which associated trait items should be ordered.
|
||||
///
|
||||
/// The reason to wrap a `Vec` in a newtype is to be able to implement
|
||||
/// [`Deserialize`]. Implementing `Deserialize` allows for implementing checks
|
||||
/// on configuration completeness at the time of loading the clippy config,
|
||||
/// letting the user know if there's any issues with the config (e.g. not
|
||||
/// listing all item kinds that should be sorted).
|
||||
#[derive(Clone)]
|
||||
pub struct SourceItemOrderingTraitAssocItemKinds(Vec<SourceItemOrderingTraitAssocItemKind>);
|
||||
|
||||
impl SourceItemOrderingTraitAssocItemKinds {
|
||||
pub fn index_of(&self, item: &SourceItemOrderingTraitAssocItemKind) -> Option<usize> {
|
||||
self.0.iter().position(|i| i == item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for SourceItemOrderingTraitAssocItemKinds
|
||||
where
|
||||
T: Into<Vec<SourceItemOrderingTraitAssocItemKind>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for SourceItemOrderingTraitAssocItemKinds {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SourceItemOrderingTraitAssocItemKinds {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let items = Vec::<SourceItemOrderingTraitAssocItemKind>::deserialize(deserializer)?;
|
||||
|
||||
let mut expected_items = SourceItemOrderingTraitAssocItemKind::all_variants();
|
||||
for item in &items {
|
||||
expected_items.retain(|i| i != item);
|
||||
}
|
||||
|
||||
let all_items = SourceItemOrderingTraitAssocItemKind::all_variants();
|
||||
if expected_items.is_empty() && items.len() == all_items.len() {
|
||||
Ok(Self(items))
|
||||
} else if items.len() != all_items.len() {
|
||||
Err(de::Error::custom(format!(
|
||||
"Some trait associated item kinds were configured more than once, or were missing, in the source ordering configuration. \
|
||||
The trait associated item kinds are: {all_items:?}",
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom(format!(
|
||||
"Not all trait associated item kinds were part of the configured source ordering rule. \
|
||||
All item kinds must be provided in the config, otherwise the required source ordering would remain ambiguous. \
|
||||
The trait associated item kinds are: {all_items:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SourceItemOrderingTraitAssocItemKinds {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: ser::Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
// these impls are never actually called but are used by the various config options that default to
|
||||
// empty lists
|
||||
macro_rules! unimplemented_serialize {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue