mirror of https://github.com/astral-sh/ruff
Merge e73b3a52b6 into b0bc990cbf
This commit is contained in:
commit
36cca9717b
|
|
@ -1,16 +1,23 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use ruff_python_ast::name::Name;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
|
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef};
|
use ruff_python_ast::{
|
||||||
|
self as ast, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, LiteralExpressionRef,
|
||||||
|
Operator,
|
||||||
|
};
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::SemanticModel;
|
||||||
use ruff_python_semantic::analyze::typing::traverse_union;
|
use ruff_python_semantic::analyze::typing::traverse_union;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::Violation;
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::snippet::SourceCodeSnippet;
|
use crate::fix::snippet::SourceCodeSnippet;
|
||||||
|
use crate::importer::ImportRequest;
|
||||||
|
use crate::{FixAvailability, Violation};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for redundant unions between a `Literal` and a builtin supertype of
|
/// Checks for redundant unions between a `Literal` and a builtin supertype of
|
||||||
|
|
@ -41,14 +48,18 @@ use crate::fix::snippet::SourceCodeSnippet;
|
||||||
pub(crate) struct RedundantLiteralUnion {
|
pub(crate) struct RedundantLiteralUnion {
|
||||||
literal: SourceCodeSnippet,
|
literal: SourceCodeSnippet,
|
||||||
builtin_type: ExprType,
|
builtin_type: ExprType,
|
||||||
|
union_kind: UnionKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Violation for RedundantLiteralUnion {
|
impl Violation for RedundantLiteralUnion {
|
||||||
|
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||||
|
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let RedundantLiteralUnion {
|
let RedundantLiteralUnion {
|
||||||
literal,
|
literal,
|
||||||
builtin_type,
|
builtin_type,
|
||||||
|
..
|
||||||
} = self;
|
} = self;
|
||||||
if let Some(literal) = literal.full_display() {
|
if let Some(literal) = literal.full_display() {
|
||||||
format!("`Literal[{literal}]` is redundant in a union with `{builtin_type}`")
|
format!("`Literal[{literal}]` is redundant in a union with `{builtin_type}`")
|
||||||
|
|
@ -56,18 +67,59 @@ impl Violation for RedundantLiteralUnion {
|
||||||
format!("`Literal` is redundant in a union with `{builtin_type}`")
|
format!("`Literal` is redundant in a union with `{builtin_type}`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fix_title(&self) -> Option<String> {
|
||||||
|
let RedundantLiteralUnion {
|
||||||
|
literal,
|
||||||
|
builtin_type,
|
||||||
|
union_kind,
|
||||||
|
} = self;
|
||||||
|
if let Some(literal) = literal.full_display() {
|
||||||
|
match union_kind {
|
||||||
|
UnionKind::TypingUnion => Some(format!(
|
||||||
|
"Replace `typing.Union[Literal[{literal}], {builtin_type}]` with `{builtin_type}`"
|
||||||
|
)),
|
||||||
|
UnionKind::PEP604 => Some(format!(
|
||||||
|
"Replace `Literal[{literal}] | {builtin_type}` with `{builtin_type}`"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(format!("Replace with `{builtin_type}`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PYI051
|
/// PYI051
|
||||||
pub(crate) fn redundant_literal_union<'a>(checker: &Checker, union: &'a Expr) {
|
pub(crate) fn redundant_literal_union<'a>(checker: &Checker, union: &'a Expr) {
|
||||||
let mut typing_literal_exprs = Vec::new();
|
let mut typing_literal_exprs = Vec::new();
|
||||||
let mut builtin_types_in_union = FxHashSet::default();
|
let mut builtin_types_in_union = FxHashSet::default();
|
||||||
|
let mut literal_subscript = None;
|
||||||
|
let mut literal_exprs = Vec::new();
|
||||||
|
let subscript = union.as_subscript_expr();
|
||||||
|
let union_kind = match subscript {
|
||||||
|
Some(subscript) => {
|
||||||
|
if !checker
|
||||||
|
.semantic()
|
||||||
|
.match_typing_expr(&subscript.value, "Union")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UnionKind::TypingUnion
|
||||||
|
}
|
||||||
|
None => UnionKind::PEP604,
|
||||||
|
};
|
||||||
|
|
||||||
// Adds a member to `literal_exprs` for each value in a `Literal`, and any builtin types
|
// Adds a member to `literal_exprs` for each value in a `Literal`, and any builtin types
|
||||||
// to `builtin_types_in_union`.
|
// to `builtin_types_in_union`.
|
||||||
let mut func = |expr: &'a Expr, _parent: &'a Expr| {
|
let mut func = |expr: &'a Expr, _parent: &'a Expr| {
|
||||||
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
||||||
if checker.semantic().match_typing_expr(value, "Literal") {
|
if checker.semantic().match_typing_expr(value, "Literal") {
|
||||||
|
literal_exprs.push(expr);
|
||||||
|
|
||||||
|
if literal_subscript.is_none() {
|
||||||
|
literal_subscript = Some(value.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
if let Expr::Tuple(tuple) = &**slice {
|
if let Expr::Tuple(tuple) = &**slice {
|
||||||
typing_literal_exprs.extend(tuple);
|
typing_literal_exprs.extend(tuple);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -85,23 +137,226 @@ pub(crate) fn redundant_literal_union<'a>(checker: &Checker, union: &'a Expr) {
|
||||||
|
|
||||||
traverse_union(&mut func, checker.semantic(), union);
|
traverse_union(&mut func, checker.semantic(), union);
|
||||||
|
|
||||||
|
let Some(literal_subscript) = literal_subscript else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diagnostics: Vec<(RedundantLiteralUnion, TextRange)> = Vec::new();
|
||||||
|
let mut non_redundant_literal_types = Vec::new();
|
||||||
|
|
||||||
for typing_literal_expr in typing_literal_exprs {
|
for typing_literal_expr in typing_literal_exprs {
|
||||||
let Some(literal_type) = match_literal_type(typing_literal_expr) else {
|
let Some(literal_type) = match_literal_type(typing_literal_expr) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if builtin_types_in_union.contains(&literal_type) {
|
if builtin_types_in_union.contains(&literal_type) {
|
||||||
checker.report_diagnostic(
|
diagnostics.push((
|
||||||
RedundantLiteralUnion {
|
RedundantLiteralUnion {
|
||||||
literal: SourceCodeSnippet::from_str(
|
literal: SourceCodeSnippet::from_str(
|
||||||
checker.locator().slice(typing_literal_expr),
|
checker.locator().slice(typing_literal_expr),
|
||||||
),
|
),
|
||||||
builtin_type: literal_type,
|
builtin_type: literal_type,
|
||||||
|
union_kind,
|
||||||
},
|
},
|
||||||
typing_literal_expr.range(),
|
typing_literal_expr.range(),
|
||||||
);
|
));
|
||||||
|
} else {
|
||||||
|
non_redundant_literal_types.push(typing_literal_expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if checker.settings().preview.is_disabled() {
|
||||||
|
for (kind, range) in diagnostics {
|
||||||
|
let _ = checker.report_diagnostic(kind, range);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_literal_expr_types: Vec<LiteralExprType<'a>> = Vec::new();
|
||||||
|
|
||||||
|
// Group all the non-redundant literal types together based on the `Literals`
|
||||||
|
let mut func = |expr: &'a Expr, _parent: &'a Expr| {
|
||||||
|
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
||||||
|
if checker.semantic().match_typing_expr(value, "Literal") {
|
||||||
|
let mut group = Vec::new();
|
||||||
|
|
||||||
|
if let Expr::Tuple(tuple) = &**slice {
|
||||||
|
for tuple_slice in tuple {
|
||||||
|
if non_redundant_literal_types.contains(&tuple_slice) {
|
||||||
|
group.push(tuple_slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if non_redundant_literal_types.contains(&slice.as_ref()) {
|
||||||
|
group.push(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !group.is_empty() {
|
||||||
|
new_literal_expr_types.push(LiteralExprType::NonRedundantTypes(group.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(_) = match_builtin_type(expr, checker.semantic()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
new_literal_expr_types.push(LiteralExprType::BuiltinType(expr));
|
||||||
|
};
|
||||||
|
|
||||||
|
traverse_union(&mut func, checker.semantic(), union);
|
||||||
|
|
||||||
|
// This generates new individual `Literal` exprs and builtins
|
||||||
|
let mut new_exprs = Vec::new();
|
||||||
|
for group in new_literal_expr_types {
|
||||||
|
match group {
|
||||||
|
LiteralExprType::BuiltinType(expr) => {
|
||||||
|
new_exprs.push(expr.to_owned());
|
||||||
|
}
|
||||||
|
LiteralExprType::NonRedundantTypes(group) => {
|
||||||
|
let new_literal_expr = Expr::Subscript(ast::ExprSubscript {
|
||||||
|
node_index: Default::default(),
|
||||||
|
value: Box::new(literal_subscript.clone()),
|
||||||
|
range: TextRange::default(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
slice: Box::new(if group.len() > 1 {
|
||||||
|
Expr::Tuple(ast::ExprTuple {
|
||||||
|
node_index: Default::default(),
|
||||||
|
elts: group.into_iter().cloned().collect(),
|
||||||
|
range: TextRange::default(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
parenthesized: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let Some(group) = group.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
group.to_owned().clone()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
new_exprs.push(new_literal_expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let applicability = if checker.comment_ranges().intersects(union.range()) {
|
||||||
|
Applicability::Unsafe
|
||||||
|
} else {
|
||||||
|
Applicability::Safe
|
||||||
|
};
|
||||||
|
|
||||||
|
for (kind, range) in diagnostics {
|
||||||
|
let mut diagnostic = checker.report_diagnostic(kind, range);
|
||||||
|
match union_kind {
|
||||||
|
UnionKind::PEP604 => {
|
||||||
|
diagnostic.try_set_optional_fix(|| -> anyhow::Result<Option<Fix>> {
|
||||||
|
Ok(generate_pep604_fix(
|
||||||
|
checker,
|
||||||
|
&new_exprs,
|
||||||
|
union,
|
||||||
|
applicability,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
UnionKind::TypingUnion => {
|
||||||
|
diagnostic.try_set_optional_fix(|| -> anyhow::Result<Option<Fix>> {
|
||||||
|
generate_typing_union_fix(checker, &new_exprs, union, applicability)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_pep604_fix(
|
||||||
|
checker: &Checker,
|
||||||
|
new_exprs: &[Expr],
|
||||||
|
union: &Expr,
|
||||||
|
applicability: Applicability,
|
||||||
|
) -> Option<Fix> {
|
||||||
|
if let [new_expr] = new_exprs {
|
||||||
|
return Some(Fix::applicable_edit(
|
||||||
|
Edit::range_replacement(checker.generator().expr(new_expr), union.range()),
|
||||||
|
applicability,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_expr = new_exprs.iter().fold(None, |acc, right| {
|
||||||
|
if let Some(left) = acc {
|
||||||
|
Some(Expr::BinOp(ExprBinOp {
|
||||||
|
node_index: Default::default(),
|
||||||
|
left: Box::new(left),
|
||||||
|
op: Operator::BitOr,
|
||||||
|
right: Box::new(right.clone()),
|
||||||
|
range: TextRange::default(),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Some(right.clone())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(Fix::applicable_edit(
|
||||||
|
Edit::range_replacement(checker.generator().expr(&new_expr), union.range()),
|
||||||
|
applicability,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_typing_union_fix(
|
||||||
|
checker: &Checker,
|
||||||
|
new_exprs: &[Expr],
|
||||||
|
union: &Expr,
|
||||||
|
applicability: Applicability,
|
||||||
|
) -> Result<Option<Fix>> {
|
||||||
|
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||||
|
&ImportRequest::import_from("typing", "Union"),
|
||||||
|
union.range().start(),
|
||||||
|
checker.semantic(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]`
|
||||||
|
let new_expr = Expr::Subscript(ExprSubscript {
|
||||||
|
node_index: Default::default(),
|
||||||
|
range: TextRange::default(),
|
||||||
|
value: Box::new(Expr::Name(ExprName {
|
||||||
|
node_index: Default::default(),
|
||||||
|
id: Name::new(binding),
|
||||||
|
ctx: ExprContext::Store,
|
||||||
|
range: TextRange::default(),
|
||||||
|
})),
|
||||||
|
slice: Box::new(if new_exprs.len() > 1 {
|
||||||
|
Expr::Tuple(ast::ExprTuple {
|
||||||
|
node_index: Default::default(),
|
||||||
|
elts: new_exprs.to_vec(),
|
||||||
|
range: TextRange::default(),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
parenthesized: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let Some(new_exprs) = new_exprs.first() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
new_exprs.clone()
|
||||||
|
}),
|
||||||
|
ctx: ExprContext::Load,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Some(Fix::applicable_edits(
|
||||||
|
Edit::range_replacement(checker.generator().expr(&new_expr), union.range()),
|
||||||
|
[import_edit],
|
||||||
|
applicability,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum LiteralExprType<'a> {
|
||||||
|
NonRedundantTypes(Vec<&'a Expr>),
|
||||||
|
BuiltinType(&'a Expr),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum UnionKind {
|
||||||
|
/// E.g., `typing.Union[int, str]`
|
||||||
|
TypingUnion,
|
||||||
|
/// E.g., `int | str`
|
||||||
|
PEP604,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
|
|
|
||||||
|
help: Replace `Literal["foo"] | str` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.py:5:37
|
--> PYI051.py:5:37
|
||||||
|
|
@ -21,6 +22,7 @@ PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"bar"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.py:5:45
|
--> PYI051.py:5:45
|
||||||
|
|
@ -31,6 +33,7 @@ PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"foo"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[5]` is redundant in a union with `int`
|
PYI051 `Literal[5]` is redundant in a union with `int`
|
||||||
--> PYI051.py:6:37
|
--> PYI051.py:6:37
|
||||||
|
|
@ -42,6 +45,7 @@ PYI051 `Literal[5]` is redundant in a union with `int`
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[5], int]` with `int`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.py:6:67
|
--> PYI051.py:6:67
|
||||||
|
|
@ -53,6 +57,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.py:7:37
|
--> PYI051.py:7:37
|
||||||
|
|
@ -64,6 +69,7 @@ PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"str_bytes"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[42]` is redundant in a union with `int`
|
PYI051 `Literal[42]` is redundant in a union with `int`
|
||||||
--> PYI051.py:7:51
|
--> PYI051.py:7:51
|
||||||
|
|
@ -75,6 +81,7 @@ PYI051 `Literal[42]` is redundant in a union with `int`
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[42], int]` with `int`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.py:8:76
|
--> PYI051.py:8:76
|
||||||
|
|
@ -86,6 +93,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.py:9:81
|
--> PYI051.py:9:81
|
||||||
|
|
@ -96,6 +104,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.py:10:69
|
--> PYI051.py:10:69
|
||||||
|
|
@ -107,6 +116,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
11 |
|
11 |
|
||||||
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[1J]` is redundant in a union with `complex`
|
PYI051 `Literal[1J]` is redundant in a union with `complex`
|
||||||
--> PYI051.py:12:31
|
--> PYI051.py:12:31
|
||||||
|
|
@ -118,6 +128,7 @@ PYI051 `Literal[1J]` is redundant in a union with `complex`
|
||||||
13 |
|
13 |
|
||||||
14 | # OK
|
14 | # OK
|
||||||
|
|
|
|
||||||
|
help: Replace `Literal[1J] | complex` with `complex`
|
||||||
|
|
||||||
PYI051 `Literal[3.14]` is redundant in a union with `float`
|
PYI051 `Literal[3.14]` is redundant in a union with `float`
|
||||||
--> PYI051.py:12:53
|
--> PYI051.py:12:53
|
||||||
|
|
@ -129,3 +140,4 @@ PYI051 `Literal[3.14]` is redundant in a union with `float`
|
||||||
13 |
|
13 |
|
||||||
14 | # OK
|
14 | # OK
|
||||||
|
|
|
|
||||||
|
help: Replace `typing.Union[Literal[3.14], float]` with `float`
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `Literal["foo"] | str` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.pyi:5:37
|
--> PYI051.pyi:5:37
|
||||||
|
|
@ -21,6 +22,7 @@ PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[b"bar"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.pyi:5:45
|
--> PYI051.pyi:5:45
|
||||||
|
|
@ -31,6 +33,7 @@ PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
||||||
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[b"foo"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[5]` is redundant in a union with `int`
|
PYI051 `Literal[5]` is redundant in a union with `int`
|
||||||
--> PYI051.pyi:6:37
|
--> PYI051.pyi:6:37
|
||||||
|
|
@ -42,6 +45,7 @@ PYI051 `Literal[5]` is redundant in a union with `int`
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[5], int]` with `int`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.pyi:6:67
|
--> PYI051.pyi:6:67
|
||||||
|
|
@ -53,6 +57,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
||||||
--> PYI051.pyi:7:37
|
--> PYI051.pyi:7:37
|
||||||
|
|
@ -64,6 +69,7 @@ PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[b"str_bytes"], bytes]` with `bytes`
|
||||||
|
|
||||||
PYI051 `Literal[42]` is redundant in a union with `int`
|
PYI051 `Literal[42]` is redundant in a union with `int`
|
||||||
--> PYI051.pyi:7:51
|
--> PYI051.pyi:7:51
|
||||||
|
|
@ -75,6 +81,7 @@ PYI051 `Literal[42]` is redundant in a union with `int`
|
||||||
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[42], int]` with `int`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.pyi:8:76
|
--> PYI051.pyi:8:76
|
||||||
|
|
@ -86,6 +93,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.pyi:9:81
|
--> PYI051.pyi:9:81
|
||||||
|
|
@ -96,6 +104,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
--> PYI051.pyi:10:69
|
--> PYI051.pyi:10:69
|
||||||
|
|
@ -107,6 +116,7 @@ PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
11 |
|
11 |
|
||||||
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
PYI051 `Literal[1J]` is redundant in a union with `complex`
|
PYI051 `Literal[1J]` is redundant in a union with `complex`
|
||||||
--> PYI051.pyi:12:31
|
--> PYI051.pyi:12:31
|
||||||
|
|
@ -118,6 +128,7 @@ PYI051 `Literal[1J]` is redundant in a union with `complex`
|
||||||
13 |
|
13 |
|
||||||
14 | # OK
|
14 | # OK
|
||||||
|
|
|
|
||||||
|
= help: Replace `Literal[1J] | complex` with `complex`
|
||||||
|
|
||||||
PYI051 `Literal[3.14]` is redundant in a union with `float`
|
PYI051 `Literal[3.14]` is redundant in a union with `float`
|
||||||
--> PYI051.pyi:12:53
|
--> PYI051.pyi:12:53
|
||||||
|
|
@ -129,3 +140,4 @@ PYI051 `Literal[3.14]` is redundant in a union with `float`
|
||||||
13 |
|
13 |
|
||||||
14 | # OK
|
14 | # OK
|
||||||
|
|
|
|
||||||
|
= help: Replace `typing.Union[Literal[3.14], float]` with `float`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||||
|
assertion_line: 137
|
||||||
|
---
|
||||||
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
|
--> PYI051.pyi:4:18
|
||||||
|
|
|
||||||
|
2 | from typing import Literal, TypeAlias, Union
|
||||||
|
3 |
|
||||||
|
4 | A: str | Literal["foo"]
|
||||||
|
| ^^^^^
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
|
|
||||||
|
help: Replace `Literal["foo"] | str` with `str`
|
||||||
|
|
||||||
|
PYI051 `Literal[b"bar"]` is redundant in a union with `bytes`
|
||||||
|
--> PYI051.pyi:5:37
|
||||||
|
|
|
||||||
|
4 | A: str | Literal["foo"]
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
| ^^^^^^
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"bar"], bytes]` with `bytes`
|
||||||
|
|
||||||
|
PYI051 `Literal[b"foo"]` is redundant in a union with `bytes`
|
||||||
|
--> PYI051.pyi:5:45
|
||||||
|
|
|
||||||
|
4 | A: str | Literal["foo"]
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
| ^^^^^^
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"foo"], bytes]` with `bytes`
|
||||||
|
|
||||||
|
PYI051 `Literal[5]` is redundant in a union with `int`
|
||||||
|
--> PYI051.pyi:6:37
|
||||||
|
|
|
||||||
|
4 | A: str | Literal["foo"]
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
| ^
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[5], int]` with `int`
|
||||||
|
|
||||||
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
|
--> PYI051.pyi:6:67
|
||||||
|
|
|
||||||
|
4 | A: str | Literal["foo"]
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
| ^^^^^
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
|
PYI051 `Literal[b"str_bytes"]` is redundant in a union with `bytes`
|
||||||
|
--> PYI051.pyi:7:37
|
||||||
|
|
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[b"str_bytes"], bytes]` with `bytes`
|
||||||
|
|
||||||
|
PYI051 `Literal[42]` is redundant in a union with `int`
|
||||||
|
--> PYI051.pyi:7:51
|
||||||
|
|
|
||||||
|
5 | B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
| ^^
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[42], int]` with `int`
|
||||||
|
|
||||||
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
|
--> PYI051.pyi:8:76
|
||||||
|
|
|
||||||
|
6 | C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
| ^^^^^
|
||||||
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
|
--> PYI051.pyi:9:81
|
||||||
|
|
|
||||||
|
7 | D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
| ^^^^^
|
||||||
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
|
PYI051 `Literal["foo"]` is redundant in a union with `str`
|
||||||
|
--> PYI051.pyi:10:69
|
||||||
|
|
|
||||||
|
8 | E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||||
|
9 | F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
| ^^^^^
|
||||||
|
11 |
|
||||||
|
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal["foo"], str]` with `str`
|
||||||
|
|
||||||
|
PYI051 `Literal[1J]` is redundant in a union with `complex`
|
||||||
|
--> PYI051.pyi:12:31
|
||||||
|
|
|
||||||
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
11 |
|
||||||
|
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||||
|
| ^^
|
||||||
|
13 |
|
||||||
|
14 | # OK
|
||||||
|
|
|
||||||
|
help: Replace `Literal[1J] | complex` with `complex`
|
||||||
|
|
||||||
|
PYI051 `Literal[3.14]` is redundant in a union with `float`
|
||||||
|
--> PYI051.pyi:12:53
|
||||||
|
|
|
||||||
|
10 | G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||||
|
11 |
|
||||||
|
12 | def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||||
|
| ^^^^
|
||||||
|
13 |
|
||||||
|
14 | # OK
|
||||||
|
|
|
||||||
|
help: Replace `typing.Union[Literal[3.14], float]` with `float`
|
||||||
Loading…
Reference in New Issue