Skip to content

Commit 21c223b

Browse files
bxffAlexWaygood
authored andcommitted
[ruff] Fix false positive for re.split with empty string pattern (RUF055) (#23634)
## Summary Fixes #23629. `re.split("", s)` is flagged by RUF055 and auto-fixed to `s.split("")`, but `str.split("")` raises `ValueError: empty separator` while `re.split("", s)` succeeds (returning `["", "a", "b", "c", ""]`). The same applies to bytes (`rb""`). This adds a guard to skip the diagnostic when the separator pattern is an empty string or bytes literal specifically for `re.split` calls. Other `re` functions (`sub`, `match`, `search`, `fullmatch`) are not affected — their `str` equivalents all handle empty strings equivalently. ## Test Plan Added test cases for empty string and bytes patterns in `RUF055_0.py` and `RUF055_3.py`. Verified that no diagnostics are emitted for these cases and all existing RUF055 snapshot tests continue to pass: ``` cargo test -p ruff_linter -- "preview_rules::rule_unnecessaryregularexpression" test result: ok. 4 passed; 0 failed; 0 ignored ```
1 parent 347452f commit 21c223b

9 files changed

Lines changed: 386 additions & 117 deletions

File tree

crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,8 @@ def dashrepl(matchobj):
9898
re.sub(r'abc', "", s)
9999
re.sub(r"""abc""", "", s)
100100
re.sub(r'''abc''', "", s)
101+
102+
# Empty pattern: re.split("", s) should not be flagged because
103+
# str.split("") raises ValueError while re.split("", s) succeeds
104+
re.split("", s)
105+
re.split(r"", s)

crates/ruff_linter/resources/test/fixtures/ruff/RUF055_3.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@
2121
re.match(rb"ab[c]", b_src)
2222
re.search(rb"ab[c]", b_src)
2323
re.fullmatch(rb"ab[c]", b_src)
24-
re.split(rb"ab[c]", b_src)
24+
re.split(rb"ab[c]", b_src)
25+
26+
# Empty pattern: re.split(rb"", b_src) should not be flagged
27+
re.split(rb"", b_src)

crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall)
118118
return;
119119
}
120120

121+
// `str.split("")` raises `ValueError: empty separator` while `re.split("", s)` succeeds,
122+
// so skip the diagnostic for `re.split` with an empty pattern.
123+
if matches!(re_func.kind, ReFuncKind::Split) && literal.is_empty() {
124+
return;
125+
}
126+
121127
// Now we know the pattern is a string literal with no metacharacters, so
122128
// we can proceed with the str method replacement.
123129
let new_expr = re_func.replacement();
@@ -362,6 +368,15 @@ enum Literal<'a> {
362368
Bytes(&'a ExprBytesLiteral),
363369
}
364370

371+
impl Literal<'_> {
372+
fn is_empty(&self) -> bool {
373+
match self {
374+
Literal::Str(str_lit) => str_lit.value.is_empty(),
375+
Literal::Bytes(bytes_lit) => bytes_lit.value.is_empty(),
376+
}
377+
}
378+
}
379+
365380
/// Try to resolve `name` to either a string or bytes literal in `semantic`.
366381
fn resolve_literal<'a>(name: &'a Expr, semantic: &'a SemanticModel) -> Option<Literal<'a>> {
367382
if let Some(str_lit) = resolve_string_literal(name, semantic) {

crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ help: Replace with `s.replace(r'abc', "")`
243243
98 + s.replace(r'abc', "")
244244
99 | re.sub(r"""abc""", "", s)
245245
100 | re.sub(r'''abc''', "", s)
246+
101 |
246247

247248
RUF055 [*] Plain string pattern passed to `re` function
248249
--> RUF055_0.py:99:1
@@ -260,6 +261,8 @@ help: Replace with `s.replace(r"""abc""", "")`
260261
- re.sub(r"""abc""", "", s)
261262
99 + s.replace(r"""abc""", "")
262263
100 | re.sub(r'''abc''', "", s)
264+
101 |
265+
102 | # Empty pattern: re.split("", s) should not be flagged because
263266

264267
RUF055 [*] Plain string pattern passed to `re` function
265268
--> RUF055_0.py:100:1
@@ -268,10 +271,15 @@ RUF055 [*] Plain string pattern passed to `re` function
268271
99 | re.sub(r"""abc""", "", s)
269272
100 | re.sub(r'''abc''', "", s)
270273
| ^^^^^^^^^^^^^^^^^^^^^^^^^
274+
101 |
275+
102 | # Empty pattern: re.split("", s) should not be flagged because
271276
|
272277
help: Replace with `s.replace(r'''abc''', "")`
273278
97 | # these double as tests for preserving raw string quoting style
274279
98 | re.sub(r'abc', "", s)
275280
99 | re.sub(r"""abc""", "", s)
276281
- re.sub(r'''abc''', "", s)
277282
100 + s.replace(r'''abc''', "")
283+
101 |
284+
102 | # Empty pattern: re.split("", s) should not be flagged because
285+
103 | # str.split("") raises ValueError while re.split("", s) succeeds

crates/ty/docs/rules.md

Lines changed: 151 additions & 101 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/enums.md

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ class Answer(Enum):
7878

7979
non_member_1: int
8080

81-
# TODO: this could be considered an error:
82-
non_member_1: str = "some value"
81+
non_member_1: str = "some value" # error: [invalid-enum-member-annotation]
8382

8483
# revealed: tuple[Literal["YES"], Literal["NO"]]
8584
reveal_type(enum_members(Answer))
@@ -100,6 +99,103 @@ class Answer(Enum):
10099
reveal_type(enum_members(Answer))
101100
```
102101

102+
### Annotated enum members
103+
104+
The [typing spec] states that enum members should not have explicit type annotations. Type checkers
105+
should report an error for annotated enum members because the annotation is misleading — the actual
106+
type of an enum member is the enum class itself, not the annotated type.
107+
108+
```py
109+
from enum import Enum
110+
111+
class Pet(Enum):
112+
CAT = 1
113+
DOG: int = 2 # error: [invalid-enum-member-annotation] "Type annotation on enum member `DOG` is not allowed"
114+
BIRD: str = "bird" # error: [invalid-enum-member-annotation]
115+
```
116+
117+
Bare `Final` annotations are allowed (they don't specify a type):
118+
119+
```py
120+
from enum import Enum
121+
from typing import Final
122+
123+
class Pet(Enum):
124+
CAT: Final = 1 # OK
125+
DOG: Final = 2 # OK
126+
```
127+
128+
But `Final` with a type argument is not allowed:
129+
130+
```py
131+
from enum import Enum
132+
from typing import Final
133+
134+
class Pet(Enum):
135+
CAT: Final[int] = 1 # error: [invalid-enum-member-annotation]
136+
DOG: Final[str] = "woof" # error: [invalid-enum-member-annotation]
137+
```
138+
139+
`enum.member` used as value wrapper is the standard way to declare members explicitly:
140+
141+
```toml
142+
[environment]
143+
python-version = "3.11"
144+
```
145+
146+
```py
147+
from enum import Enum, member
148+
149+
class Pet(Enum):
150+
CAT = member(1) # OK
151+
```
152+
153+
Dunder and private names are not enum members, so they don't trigger the diagnostic:
154+
155+
```py
156+
from enum import Enum
157+
158+
class Pet(Enum):
159+
CAT = 1
160+
__private: int = 2 # OK: dunder/private names are never members
161+
__module__: str = "my_module" # OK
162+
```
163+
164+
Pure declarations (annotations without values) are non-members and are fine:
165+
166+
```py
167+
from enum import Enum
168+
169+
class Pet(Enum):
170+
CAT = 1
171+
species: str # OK: no value, so this is a non-member declaration
172+
```
173+
174+
The check also works for subclasses of `Enum`:
175+
176+
```py
177+
from enum import IntEnum, StrEnum
178+
179+
class Status(IntEnum):
180+
OK: int = 200 # error: [invalid-enum-member-annotation]
181+
NOT_FOUND = 404 # OK
182+
183+
class Color(StrEnum):
184+
RED: str = "red" # error: [invalid-enum-member-annotation]
185+
GREEN = "green" # OK
186+
```
187+
188+
Special sunder names like `_value_` and `_ignore_` are not flagged:
189+
190+
```py
191+
from enum import Enum
192+
193+
class Pet(Enum):
194+
_value_: int = 0 # OK: `_value_` is a special enum name
195+
_ignore_: str = "TEMP" # OK: `_ignore_` is a special enum name
196+
CAT = 1
197+
```
198+
103199
### Declared `_value_` annotation
104200

105201
If a `_value_` annotation is defined on an `Enum` class, all enum member values must be compatible
@@ -814,7 +910,7 @@ class Answer(Enum):
814910

815911
def is_yes(self) -> bool:
816912
return self == Answer.YES
817-
constant: int = 1
913+
constant: int = 1 # error: [invalid-enum-member-annotation]
818914

819915
reveal_type(Answer.YES.is_yes()) # revealed: bool
820916
reveal_type(Answer.YES.constant) # revealed: int
@@ -1353,3 +1449,4 @@ class MyEnum[T](MyEnumBase):
13531449
- Documentation: <https://docs.python.org/3/library/enum.html>
13541450

13551451
[class-private names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
1452+
[typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
8383
registry.register_lint(&INVALID_CONTEXT_MANAGER);
8484
registry.register_lint(&INVALID_DECLARATION);
8585
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
86+
registry.register_lint(&INVALID_ENUM_MEMBER_ANNOTATION);
8687
registry.register_lint(&INVALID_GENERIC_ENUM);
8788
registry.register_lint(&INVALID_GENERIC_CLASS);
8889
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
@@ -1193,6 +1194,49 @@ declare_lint! {
11931194
}
11941195
}
11951196

1197+
declare_lint! {
1198+
/// ## What it does
1199+
/// Checks for enum members that have explicit type annotations.
1200+
///
1201+
/// ## Why is this bad?
1202+
/// The [typing spec] states that type checkers should infer a literal type
1203+
/// for all enum members. An explicit type annotation on an enum member is
1204+
/// misleading because the annotated type will be incorrect — the actual
1205+
/// runtime type is the enum class itself, not the annotated type.
1206+
///
1207+
/// In CPython's `enum` module, annotated assignments with values are still
1208+
/// treated as members at runtime, but the annotation is meaningless and
1209+
/// will confuse readers of the code.
1210+
///
1211+
/// ## Examples
1212+
/// ```python
1213+
/// from enum import Enum
1214+
///
1215+
/// class Pet(Enum):
1216+
/// CAT = 1 # OK
1217+
/// DOG: int = 2 # Error: enum members should not be annotated
1218+
/// ```
1219+
///
1220+
/// Use instead:
1221+
/// ```python
1222+
/// from enum import Enum
1223+
///
1224+
/// class Pet(Enum):
1225+
/// CAT = 1
1226+
/// DOG = 2
1227+
/// ```
1228+
///
1229+
/// ## References
1230+
/// - [Typing spec: Enum members](https://typing.python.org/en/latest/spec/enums.html#enum-members)
1231+
///
1232+
/// [typing spec]: https://typing.python.org/en/latest/spec/enums.html#enum-members
1233+
pub(crate) static INVALID_ENUM_MEMBER_ANNOTATION = {
1234+
summary: "detects type annotations on enum members",
1235+
status: LintStatus::stable("0.0.16"),
1236+
default_level: Level::Error,
1237+
}
1238+
}
1239+
11961240
declare_lint! {
11971241
/// ## What it does
11981242
/// Checks for enum classes that are also generic.

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,18 @@ use crate::types::diagnostic::{
7474
DATACLASS_FIELD_ORDER, DUPLICATE_BASE, DUPLICATE_KW_ONLY, FINAL_ON_NON_METHOD,
7575
FINAL_WITHOUT_VALUE, INCONSISTENT_MRO, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE,
7676
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DATACLASS,
77-
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_GENERIC_ENUM, INVALID_KEY,
78-
INVALID_LEGACY_POSITIONAL_PARAMETER, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
79-
INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
80-
INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS,
81-
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION,
82-
INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT,
83-
INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, MISSING_ARGUMENT,
84-
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSSIBLY_MISSING_ATTRIBUTE,
85-
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
86-
TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNKNOWN_ARGUMENT,
87-
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
88-
UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
77+
INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_GENERIC_CLASS,
78+
INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_POSITIONAL_PARAMETER,
79+
INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
80+
INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
81+
INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
82+
INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS,
83+
INVALID_TYPE_VARIABLE_DEFAULT, INVALID_TYPED_DICT_HEADER, INVALID_TYPED_DICT_STATEMENT,
84+
IncompatibleBases, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED,
85+
POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
86+
SUBCLASS_OF_FINAL_CLASS, TOO_MANY_POSITIONAL_ARGUMENTS, TypedDictDeleteErrorKind,
87+
UNDEFINED_REVEAL, UNKNOWN_ARGUMENT, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
88+
UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
8989
hint_if_stdlib_attribute_exists_on_other_versions,
9090
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
9191
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
@@ -9635,6 +9635,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
96359635
&DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(inferred_ty)),
96369636
);
96379637
} else {
9638+
// Check for annotated enum members. The typing spec states that enum
9639+
// members should not have explicit type annotations.
9640+
if let Some(name_expr) = target.as_name_expr()
9641+
&& !name_expr.id.starts_with("__")
9642+
&& !matches!(name_expr.id.as_str(), "_ignore_" | "_value_" | "_name_")
9643+
// Not bare Final (bare Final is allowed on enum members)
9644+
&& !(declared.qualifiers.contains(TypeQualifiers::FINAL)
9645+
&& matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown)))
9646+
// Not enum.member annotation
9647+
&& !matches!(
9648+
declared.inner_type(),
9649+
Type::NominalInstance(instance)
9650+
if instance.has_known_class(self.db(), KnownClass::Member)
9651+
)
9652+
// Value type would be an enum member at runtime (exclude callables,
9653+
// functions, and descriptors which are never members)
9654+
&& !matches!(inferred_ty, Type::Callable(_) | Type::FunctionLiteral(_))
9655+
{
9656+
let current_scope_id = self.scope().file_scope_id(self.db());
9657+
let current_scope = self.index.scope(current_scope_id);
9658+
if current_scope.kind() == ScopeKind::Class
9659+
&& let Some(class) =
9660+
nearest_enclosing_class(self.db(), self.index, self.scope())
9661+
&& is_enum_class_by_inheritance(self.db(), class)
9662+
{
9663+
if let Some(builder) = self
9664+
.context
9665+
.report_lint(&INVALID_ENUM_MEMBER_ANNOTATION, annotation)
9666+
{
9667+
builder.into_diagnostic(format_args!(
9668+
"Type annotation on enum member `{}` is not allowed",
9669+
&name_expr.id
9670+
));
9671+
}
9672+
}
9673+
}
9674+
96389675
self.add_declaration_with_binding(
96399676
target.into(),
96409677
definition,

ty.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)