Skip to content

Commit c31e542

Browse files
committed
diagnostics
1 parent bfbf431 commit c31e542

3 files changed

Lines changed: 136 additions & 18 deletions

File tree

crates/ty_python_semantic/resources/mdtest/enums.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1437,7 +1437,7 @@ reveal_type(enum_members(E3)) # revealed: Unknown
14371437
from enum import Enum
14381438
from ty_extensions import enum_members
14391439

1440-
# this is invalid at runtime: TypeError
1440+
# error: [too-many-positional-arguments]
14411441
Color = Enum("Color", "RED", "GREEN", "BLUE")
14421442

14431443
reveal_type(enum_members(Color)) # revealed: Unknown
@@ -1467,6 +1467,53 @@ def make_enum(name: str, labels: tuple[str, ...]) -> type[Enum]:
14671467
return result
14681468
```
14691469

1470+
### Non-string name
1471+
1472+
```py
1473+
from enum import Enum
1474+
1475+
# error: [invalid-argument-type]
1476+
Color = Enum(123, "RED GREEN BLUE")
1477+
```
1478+
1479+
### Unknown keyword arguments
1480+
1481+
```py
1482+
from enum import Enum
1483+
1484+
# error: [unknown-argument]
1485+
Color = Enum("Color", "RED GREEN BLUE", bad_kwarg=True)
1486+
```
1487+
1488+
### `boundary` keyword (Python 3.11+)
1489+
1490+
#### Available on 3.11+
1491+
1492+
```toml
1493+
[environment]
1494+
python-version = "3.11"
1495+
```
1496+
1497+
```py
1498+
from enum import Flag
1499+
1500+
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
1501+
```
1502+
1503+
#### Rejected before 3.11
1504+
1505+
```toml
1506+
[environment]
1507+
python-version = "3.10"
1508+
```
1509+
1510+
```py
1511+
from enum import Flag
1512+
1513+
# error: [unknown-argument]
1514+
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
1515+
```
1516+
14701517
### StrEnum function syntax
14711518

14721519
```toml
@@ -1555,6 +1602,24 @@ reveal_type(Perm.WRITE.value) # revealed: Literal[2]
15551602
reveal_type(Perm.EXECUTE.value) # revealed: Literal[4]
15561603
```
15571604

1605+
### Large start value (overflow guard)
1606+
1607+
Values that would overflow `i64` should gracefully widen to `int`.
1608+
1609+
```py
1610+
from enum import Enum, Flag
1611+
1612+
Big = Enum("Big", "A B", start=9223372036854775807)
1613+
1614+
reveal_type(Big.A.value) # revealed: Literal[9223372036854775807]
1615+
reveal_type(Big.B.value) # revealed: int
1616+
1617+
BigFlag = Flag("BigFlag", "X Y", start=4611686018427387904)
1618+
1619+
reveal_type(BigFlag.X.value) # revealed: Literal[4611686018427387904]
1620+
reveal_type(BigFlag.Y.value) # revealed: int
1621+
```
1622+
15581623
## Exhaustiveness checking
15591624

15601625
## `if` statements

crates/ty_python_semantic/src/types/class/enum_literal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ impl<'db> DynamicEnumLiteral<'db> {
102102
Span::from(self.scope(db).file(db)).with_range(self.header_range(db))
103103
}
104104

105+
#[expect(clippy::unused_self)]
105106
pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
106-
let _ = self;
107107
KnownClass::EnumType.to_class_literal(db)
108108
}
109109

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

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use ruff_python_ast::name::Name;
2-
use ruff_python_ast::{self as ast, NodeIndex};
2+
use ruff_python_ast::{self as ast, NodeIndex, PythonVersion};
33

44
use crate::{
5-
Db,
5+
Db, Program,
66
semantic_index::definition::Definition,
77
types::{
88
ClassLiteral, KnownClass, Type, TypeContext,
99
class::{DynamicEnumAnchor, DynamicEnumLiteral, EnumSpec},
10+
diagnostic::{INVALID_ARGUMENT_TYPE, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT},
1011
infer::TypeInferenceBuilder,
1112
subclass_of::SubclassOfType,
1213
},
@@ -43,9 +44,24 @@ fn enum_auto_value<'db>(
4344
match base_class {
4445
KnownClass::StrEnum => Type::string_literal(db, &name.to_lowercase()),
4546
KnownClass::Flag | KnownClass::IntFlag => {
46-
Type::int_literal(start << i64::try_from(index).unwrap_or(0))
47+
let shift = i64::try_from(index).ok();
48+
let headroom = if start >= 0 {
49+
start.leading_zeros().saturating_sub(1)
50+
} else {
51+
start.leading_ones().saturating_sub(1)
52+
};
53+
shift
54+
.and_then(|s| u32::try_from(s).ok())
55+
.filter(|&s| s <= headroom)
56+
.and_then(|s| start.checked_shl(s))
57+
.map(Type::int_literal)
58+
.unwrap_or_else(|| KnownClass::Int.to_instance(db))
4759
}
48-
_ => Type::int_literal(start + i64::try_from(index).unwrap_or(0)),
60+
_ => i64::try_from(index)
61+
.ok()
62+
.and_then(|i| start.checked_add(i))
63+
.map(Type::int_literal)
64+
.unwrap_or_else(|| KnownClass::Int.to_instance(db)),
4965
}
5066
}
5167

@@ -69,10 +85,17 @@ fn dict_auto_value<'db>(
6985
if last_int_value <= 0 {
7086
Type::int_literal(1)
7187
} else {
72-
Type::int_literal(1 << (i64::BITS - last_int_value.leading_zeros()))
88+
let shift = i64::BITS - last_int_value.leading_zeros();
89+
1_i64
90+
.checked_shl(shift)
91+
.map(Type::int_literal)
92+
.unwrap_or_else(|| KnownClass::Int.to_instance(db))
7393
}
7494
}
75-
_ => Type::int_literal(last_int_value + 1),
95+
_ => last_int_value
96+
.checked_add(1)
97+
.map(Type::int_literal)
98+
.unwrap_or_else(|| KnownClass::Int.to_instance(db)),
7699
}
77100
}
78101

@@ -87,17 +110,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
87110
let args = &call_expr.arguments.args;
88111
let keywords = &call_expr.arguments.keywords;
89112

90-
// bail out on unknown keywords so normal overload resolution can diagnose them
91-
let has_unknown_keyword = keywords.iter().any(|kw| {
92-
kw.arg.as_ref().is_some_and(|name| {
93-
!matches!(
113+
let base_name = base_class.name(db);
114+
let python_version = Program::get(db).python_version(db);
115+
116+
for kw in keywords {
117+
if let Some(name) = &kw.arg {
118+
let is_valid_keyword = matches!(
94119
name.as_str(),
95-
"value" | "names" | "start" | "type" | "module" | "qualname" | "boundary"
96-
)
97-
})
98-
});
99-
if has_unknown_keyword {
100-
return None;
120+
"value" | "names" | "start" | "type" | "module" | "qualname"
121+
) || (name.as_str() == "boundary"
122+
&& python_version >= PythonVersion::PY311);
123+
if !is_valid_keyword {
124+
if let Some(builder) = self.context.report_lint(&UNKNOWN_ARGUMENT, kw) {
125+
builder.into_diagnostic(format_args!(
126+
"Argument `{name}` does not match any known parameter of function `{base_name}`",
127+
));
128+
}
129+
}
130+
}
101131
}
102132

103133
let value_kw = call_expr.arguments.find_keyword("value");
@@ -131,6 +161,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
131161
// Non-literal name: return type[base_class] without creating a
132162
// DynamicEnumLiteral. This matches the typeshed overload return type.
133163
if !name_arg.is_string_literal_expr() {
164+
let name_type = self.expression_type(name_arg);
165+
if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db))
166+
&& let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg)
167+
{
168+
let mut diagnostic = builder.into_diagnostic(format_args!(
169+
"Invalid argument to parameter `value` of `{base_name}()`"
170+
));
171+
diagnostic.set_primary_message(format_args!(
172+
"Expected `str`, found `{}`",
173+
name_type.display(db)
174+
));
175+
}
134176
return SubclassOfType::try_from_type(db, base_class.to_class_literal(db));
135177
}
136178

@@ -151,6 +193,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
151193
// Only 1 extra positional arg is allowed (the `names` parameter).
152194
// `Enum("Color", "RED", "GREEN")` is invalid at runtime.
153195
let has_too_many_positional = args.len() > 2;
196+
if has_too_many_positional {
197+
if let Some(builder) = self
198+
.context
199+
.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, &args[2])
200+
{
201+
builder.into_diagnostic(format_args!(
202+
"Too many positional arguments to function `{base_name}`: expected 2, got {}",
203+
args.len(),
204+
));
205+
}
206+
}
154207

155208
// without `names`, this is a value-lookup call, not functional enum creation
156209
let names_arg = names_arg?;

0 commit comments

Comments
 (0)