[ruff] Exempt NewType calls where the original type is immutable (RUF009) (#15588)
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

## Summary

Resolves #6447.

## Test Plan

`cargo nextest run` and `cargo insta test`.
This commit is contained in:
InSync 2025-01-20 21:44:47 +07:00 committed by GitHub
parent 134fefa945
commit 4cfa355519
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 124 additions and 3 deletions

View file

@ -69,3 +69,31 @@ class IntConversionDescriptor:
@dataclass @dataclass
class InventoryItem: class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100) quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
# Regression tests for:
# https://github.com/astral-sh/ruff/issues/6447
from typing import NewType
ListOfStrings = NewType("ListOfStrs", list[str])
StringsToInts = NewType("IntsToStrings", dict[str, int])
SpecialString = NewType(name="SpecialString", tp=str)
NegativeInteger = NewType("NegInt", tp=int)
Invalid1 = NewType(*Foo)
Invalid2 = NewType("Invalid2", name=Foo)
Invalid3 = NewType("Invalid3", name=Foo, lorem="ipsum")
@dataclass
class DataclassWithNewTypeFields:
# Errors
a: ListOfStrings = ListOfStrings([])
b: StringsToInts = StringsToInts()
c: Invalid1 = Invalid1()
d: Invalid2 = Invalid2()
e: Invalid3 = Invalid3()
# No errors
e: SpecialString = SpecialString("Lorem ipsum")
f: NegativeInteger = NegativeInteger(-110)

View file

@ -1,9 +1,10 @@
use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_ast::{self as ast, Expr, ExprCall, Stmt, StmtAssign};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_semantic::analyze::typing::is_immutable_func; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_immutable_func};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -137,6 +138,7 @@ pub(crate) fn function_call_in_dataclass_default(
|| is_class_var_annotation(annotation, checker.semantic()) || is_class_var_annotation(annotation, checker.semantic())
|| is_immutable_func(func, checker.semantic(), &extend_immutable_calls) || is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
|| is_descriptor_class(func, checker.semantic()) || is_descriptor_class(func, checker.semantic())
|| is_immutable_newtype_call(func, checker.semantic(), &extend_immutable_calls)
{ {
continue; continue;
} }
@ -156,3 +158,46 @@ fn any_annotated(class_body: &[Stmt]) -> bool {
.iter() .iter()
.any(|stmt| matches!(stmt, Stmt::AnnAssign(..))) .any(|stmt| matches!(stmt, Stmt::AnnAssign(..)))
} }
fn is_immutable_newtype_call(
func: &Expr,
semantic: &SemanticModel,
extend_immutable_calls: &[QualifiedName],
) -> bool {
let Expr::Name(name) = func else {
return false;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return false;
};
if !binding.kind.is_assignment() {
return false;
}
let Some(Stmt::Assign(StmtAssign { value, .. })) = binding.statement(semantic) else {
return false;
};
let Expr::Call(ExprCall {
func, arguments, ..
}) = value.as_ref()
else {
return false;
};
if !semantic.match_typing_expr(func, "NewType") {
return false;
}
if arguments.len() != 2 {
return false;
}
let Some(original_type) = arguments.find_argument_value("tp", 1) else {
return false;
};
is_immutable_annotation(original_type, semantic, extend_immutable_calls)
}

View file

@ -1,6 +1,5 @@
--- ---
source: crates/ruff_linter/src/rules/ruff/mod.rs source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
--- ---
RUF009.py:20:41: RUF009 Do not perform function call `default_function` in dataclass defaults RUF009.py:20:41: RUF009 Do not perform function call `default_function` in dataclass defaults
| |
@ -41,3 +40,52 @@ RUF009.py:45:34: RUF009 Do not perform function call `ImmutableType` in dataclas
46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES 46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES 47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
| |
RUF009.py:91:24: RUF009 Do not perform function call `ListOfStrings` in dataclass defaults
|
89 | class DataclassWithNewTypeFields:
90 | # Errors
91 | a: ListOfStrings = ListOfStrings([])
| ^^^^^^^^^^^^^^^^^ RUF009
92 | b: StringsToInts = StringsToInts()
93 | c: Invalid1 = Invalid1()
|
RUF009.py:92:24: RUF009 Do not perform function call `StringsToInts` in dataclass defaults
|
90 | # Errors
91 | a: ListOfStrings = ListOfStrings([])
92 | b: StringsToInts = StringsToInts()
| ^^^^^^^^^^^^^^^ RUF009
93 | c: Invalid1 = Invalid1()
94 | d: Invalid2 = Invalid2()
|
RUF009.py:93:19: RUF009 Do not perform function call `Invalid1` in dataclass defaults
|
91 | a: ListOfStrings = ListOfStrings([])
92 | b: StringsToInts = StringsToInts()
93 | c: Invalid1 = Invalid1()
| ^^^^^^^^^^ RUF009
94 | d: Invalid2 = Invalid2()
95 | e: Invalid3 = Invalid3()
|
RUF009.py:94:19: RUF009 Do not perform function call `Invalid2` in dataclass defaults
|
92 | b: StringsToInts = StringsToInts()
93 | c: Invalid1 = Invalid1()
94 | d: Invalid2 = Invalid2()
| ^^^^^^^^^^ RUF009
95 | e: Invalid3 = Invalid3()
|
RUF009.py:95:19: RUF009 Do not perform function call `Invalid3` in dataclass defaults
|
93 | c: Invalid1 = Invalid1()
94 | d: Invalid2 = Invalid2()
95 | e: Invalid3 = Invalid3()
| ^^^^^^^^^^ RUF009
96 |
97 | # No errors
|