|
1 | 1 | use ruff_python_ast::name::Name; |
2 | 2 | use ruff_python_ast::{self as ast, NodeIndex}; |
3 | 3 | use smallvec::SmallVec; |
| 4 | +use strum::IntoEnumIterator; |
4 | 5 |
|
5 | 6 | use super::TypeInferenceBuilder; |
| 7 | +use crate::TypeQualifiers; |
6 | 8 | use crate::semantic_index::definition::Definition; |
7 | 9 | use crate::types::class::{ClassLiteral, DynamicTypedDictAnchor, DynamicTypedDictLiteral}; |
8 | 10 | use crate::types::diagnostic::{ |
9 | | - INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, |
| 11 | + INVALID_ARGUMENT_TYPE, INVALID_TYPE_FORM, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS, |
| 12 | + UNKNOWN_ARGUMENT, |
10 | 13 | }; |
| 14 | +use crate::types::special_form::TypeQualifier; |
11 | 15 | use crate::types::typed_dict::{TypedDictSchema, functional_typed_dict_field}; |
12 | | -use crate::types::{IntersectionType, KnownClass, Type, TypeContext}; |
| 16 | +use crate::types::{IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext}; |
13 | 17 |
|
14 | 18 | impl<'db> TypeInferenceBuilder<'db, '_> { |
15 | 19 | /// Infer a `TypedDict(name, fields)` call expression. |
@@ -124,7 +128,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { |
124 | 128 | } |
125 | 129 | "extra_items" => { |
126 | 130 | if definition.is_none() { |
127 | | - self.infer_annotation_expression(&kw.value, self.deferred_state); |
| 131 | + self.infer_extra_items_kwarg(&kw.value); |
128 | 132 | } |
129 | 133 | } |
130 | 134 | unknown_kwarg => { |
@@ -293,7 +297,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { |
293 | 297 | return TypedDictSchema::default(); |
294 | 298 | }; |
295 | 299 |
|
296 | | - let annotation = self.infer_annotation_expression(&item.value, self.deferred_state); |
| 300 | + let annotation = self.infer_typeddict_field(&item.value); |
297 | 301 |
|
298 | 302 | schema.insert( |
299 | 303 | Name::new(key_literal.value(db)), |
@@ -321,16 +325,54 @@ impl<'db> TypeInferenceBuilder<'db, '_> { |
321 | 325 | if let Some(ast::Expr::Dict(dict_expr)) = arguments.args.get(1) { |
322 | 326 | for ast::DictItem { key, value } in dict_expr { |
323 | 327 | if key.is_some() { |
324 | | - self.infer_annotation_expression(value, self.deferred_state); |
| 328 | + self.infer_typeddict_field(value); |
325 | 329 | } |
326 | 330 | } |
327 | 331 | } |
328 | 332 |
|
329 | 333 | if let Some(extra_items_kwarg) = arguments.find_keyword("extra_items") { |
330 | | - self.infer_annotation_expression(&extra_items_kwarg.value, self.deferred_state); |
| 334 | + self.infer_extra_items_kwarg(&extra_items_kwarg.value); |
331 | 335 | } |
332 | 336 | } |
333 | 337 |
|
| 338 | + fn infer_typeddict_field(&mut self, value: &ast::Expr) -> TypeAndQualifiers<'db> { |
| 339 | + let annotation = self.infer_annotation_expression(value, self.deferred_state); |
| 340 | + for qualifier in TypeQualifier::iter() { |
| 341 | + if !qualifier.is_valid_in_typeddict_field() |
| 342 | + && annotation |
| 343 | + .qualifiers |
| 344 | + .contains(TypeQualifiers::from(qualifier)) |
| 345 | + && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, value) |
| 346 | + { |
| 347 | + let mut diagnostic = builder.into_diagnostic(format_args!( |
| 348 | + "Type qualifier `{qualifier}` is not valid in a TypedDict field" |
| 349 | + )); |
| 350 | + diagnostic.info( |
| 351 | + "Only `Required`, `NotRequired` and `ReadOnly` are valid in this context", |
| 352 | + ); |
| 353 | + } |
| 354 | + } |
| 355 | + annotation |
| 356 | + } |
| 357 | + |
| 358 | + fn infer_extra_items_kwarg(&mut self, value: &ast::Expr) -> TypeAndQualifiers<'db> { |
| 359 | + let annotation = self.infer_annotation_expression(value, self.deferred_state); |
| 360 | + for qualifier in TypeQualifier::iter() { |
| 361 | + if qualifier != TypeQualifier::ReadOnly |
| 362 | + && annotation |
| 363 | + .qualifiers |
| 364 | + .contains(TypeQualifiers::from(qualifier)) |
| 365 | + && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, value) |
| 366 | + { |
| 367 | + let mut diagnostic = builder.into_diagnostic(format_args!( |
| 368 | + "Type qualifier `{qualifier}` is not valid in a TypedDict `extra_items` argument" |
| 369 | + )); |
| 370 | + diagnostic.info("`ReadOnly` is the only permitted type qualifier here"); |
| 371 | + } |
| 372 | + } |
| 373 | + annotation |
| 374 | + } |
| 375 | + |
334 | 376 | /// Infer all non-type expressions in the `fields` argument of a functional `TypedDict` definition, |
335 | 377 | /// and emit diagnostics for invalid field keys. Type expressions are not inferred during this pass, |
336 | 378 | /// because it must be deferred for` TypedDict` definitions that may hold recursive references to |
|
0 commit comments