Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,20 @@

# t-strings are not native literals
str(t"hey")

# UP018 - Extended detections
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test for a single line implicit concatenated string or bytes literal that's followed by an attribute access

str("A" "B")
str("A" "B").lower()
str(
"A"
"B"
)
str(object="!")
complex(1j)
complex(real=1j)
complex()
complex(0j)
complex(real=0j)
(complex(0j)).real
complex(1j).real
complex(real=1j).real
76 changes: 57 additions & 19 deletions crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum LiteralType {
Int,
Float,
Bool,
Complex,
}

impl FromStr for LiteralType {
Expand All @@ -28,6 +29,7 @@ impl FromStr for LiteralType {
"int" => Ok(LiteralType::Int),
"float" => Ok(LiteralType::Float),
"bool" => Ok(LiteralType::Bool),
"complex" => Ok(LiteralType::Complex),
_ => Err(()),
}
}
Expand Down Expand Up @@ -63,6 +65,15 @@ impl LiteralType {
}
.into(),
LiteralType::Bool => ast::ExprBooleanLiteral::default().into(),
LiteralType::Complex => ast::ExprNumberLiteral {
value: ast::Number::Complex {
real: 0.0,
imag: 0.0,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
}
.into(),
}
}
}
Expand All @@ -78,7 +89,7 @@ impl TryFrom<LiteralExpressionRef<'_>> for LiteralType {
match value {
ast::Number::Int(_) => Ok(LiteralType::Int),
ast::Number::Float(_) => Ok(LiteralType::Float),
ast::Number::Complex { .. } => Err(()),
ast::Number::Complex { .. } => Ok(LiteralType::Complex),
}
}
LiteralExpressionRef::BooleanLiteral(_) => Ok(LiteralType::Bool),
Expand All @@ -97,12 +108,13 @@ impl fmt::Display for LiteralType {
LiteralType::Int => fmt.write_str("int"),
LiteralType::Float => fmt.write_str("float"),
LiteralType::Bool => fmt.write_str("bool"),
LiteralType::Complex => fmt.write_str("complex"),
}
}
}

/// ## What it does
/// Checks for unnecessary calls to `str`, `bytes`, `int`, `float`, and `bool`.
/// Checks for unnecessary calls to `str`, `bytes`, `int`, `float`, `bool`, and `complex`.
///
/// ## Why is this bad?
/// The mentioned constructors can be replaced with their respective literal
Expand All @@ -127,6 +139,7 @@ impl fmt::Display for LiteralType {
/// - [Python documentation: `int`](https://docs.python.org/3/library/functions.html#int)
/// - [Python documentation: `float`](https://docs.python.org/3/library/functions.html#float)
/// - [Python documentation: `bool`](https://docs.python.org/3/library/functions.html#bool)
/// - [Python documentation: `complex`](https://docs.python.org/3/library/functions.html#complex)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.193")]
pub(crate) struct NativeLiterals {
Expand All @@ -148,10 +161,26 @@ impl AlwaysFixableViolation for NativeLiterals {
LiteralType::Int => "Replace with integer literal".to_string(),
LiteralType::Float => "Replace with float literal".to_string(),
LiteralType::Bool => "Replace with boolean literal".to_string(),
LiteralType::Complex => "Replace with complex literal".to_string(),
}
}
}

/// Returns `true` if the keyword argument is redundant for the given builtin.
fn is_redundant_keyword(builtin: &str, keyword: &ast::Keyword) -> bool {
let Some(arg) = keyword.arg.as_ref() else {
return false;
};
match builtin {
"str" => arg == "object",
// Python 3.14 emits a `SyntaxWarning` for `complex(real=1j)`. While this
// does change the behavior, upgrading it to 1j is very much in the spirit of this rule
// and removing the `SyntaxWarning` is a nice side effect.
"complex" => arg == "real",
_ => false,
}
}

/// UP018
pub(crate) fn native_literals(
checker: &Checker,
Expand All @@ -171,17 +200,21 @@ pub(crate) fn native_literals(
node_index: _,
} = call;

if !keywords.is_empty() || args.len() > 1 {
return;
}

let tokens = checker.tokens();
let semantic = checker.semantic();

let Some(builtin) = semantic.resolve_builtin_symbol(func) else {
return;
};

let call_arg = match (args.as_ref(), keywords.as_ref()) {
([], []) => None,
([arg], []) => Some(arg),
([], [keyword]) if is_redundant_keyword(builtin, keyword) => Some(&keyword.value),
_ => return,
};

let tokens = checker.tokens();

let Ok(literal_type) = LiteralType::from_str(builtin) else {
return;
};
Expand All @@ -198,30 +231,27 @@ pub(crate) fn native_literals(
}
}

match args.first() {
match call_arg {
None => {
// Do not suggest fix for attribute access on an int like `int().attribute`
// Ex) `int().denominator` is valid but `0.denominator` is not
if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) {
return;
}

let mut diagnostic =
checker.report_diagnostic(NativeLiterals { literal_type }, call.range());

let expr = literal_type.as_zero_value_expr(checker);
let content = checker.generator().expr(&expr);
let mut content = checker.generator().expr(&expr);

// Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float
// Ex) `(0).denominator` is valid but `0.denominator` is not
if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) {
content = format!("({content})");
}

diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
content,
call.range(),
)));
}
Some(arg) => {
let (has_unary_op, literal_expr) = if let Some(literal_expr) = arg.as_literal_expr() {
// Skip implicit concatenated strings.
if literal_expr.is_implicit_concatenated() {
return;
}
(false, literal_expr)
} else if let Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::UAdd | UnaryOp::USub,
Expand Down Expand Up @@ -269,6 +299,14 @@ pub(crate) fn native_literals(
// Expressions including newlines must be parenthesised to be valid syntax
(_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"),

// Implicitly concatenated strings spanning multiple lines must be parenthesized
(_, LiteralType::Str | LiteralType::Bytes, _)
if literal_expr.is_implicit_concatenated()
&& find_newline(arg_code).is_some() =>
{
format!("({arg_code})")
}

// Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float
// Ex) `(7).denominator` is valid but `7.denominator` is not
// Note that floats do not have this problem
Expand Down
Loading
Loading