[red-knot] Add definitions and limited type inference for exception handlers (#13267)

This commit is contained in:
Alex Waygood 2024-09-09 07:35:15 -04:00 committed by GitHub
parent 346dbf45b5
commit 1eb3e4057f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 190 additions and 3 deletions

View file

@ -880,6 +880,30 @@ where
self.current_match_case.as_mut().unwrap().index += 1; self.current_match_case.as_mut().unwrap().index += 1;
} }
fn visit_except_handler(&mut self, except_handler: &'ast ast::ExceptHandler) {
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
let ast::ExceptHandlerExceptHandler {
name: symbol_name,
type_: handled_exceptions,
body,
range: _,
} = except_handler;
if let Some(handled_exceptions) = handled_exceptions {
self.visit_expr(handled_exceptions);
}
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = self.add_or_update_symbol(symbol_name.id.clone(), SymbolFlags::IS_DEFINED);
self.add_definition(symbol, except_handler);
}
self.visit_body(body);
}
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]

View file

@ -50,6 +50,7 @@ pub(crate) enum DefinitionNodeRef<'a> {
Parameter(ast::AnyParameterRef<'a>), Parameter(ast::AnyParameterRef<'a>),
WithItem(WithItemDefinitionNodeRef<'a>), WithItem(WithItemDefinitionNodeRef<'a>),
MatchPattern(MatchPatternDefinitionNodeRef<'a>), MatchPattern(MatchPatternDefinitionNodeRef<'a>),
ExceptHandler(&'a ast::ExceptHandlerExceptHandler),
} }
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> { impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
@ -130,6 +131,12 @@ impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
} }
} }
impl<'a> From<&'a ast::ExceptHandlerExceptHandler> for DefinitionNodeRef<'a> {
fn from(node: &'a ast::ExceptHandlerExceptHandler) -> Self {
Self::ExceptHandler(node)
}
}
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) struct ImportFromDefinitionNodeRef<'a> { pub(crate) struct ImportFromDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::StmtImportFrom, pub(crate) node: &'a ast::StmtImportFrom,
@ -248,6 +255,9 @@ impl DefinitionNodeRef<'_> {
identifier: AstNodeRef::new(parsed, identifier), identifier: AstNodeRef::new(parsed, identifier),
index, index,
}), }),
DefinitionNodeRef::ExceptHandler(handler) => {
DefinitionKind::ExceptHandler(AstNodeRef::new(parsed, handler))
}
} }
} }
@ -280,6 +290,7 @@ impl DefinitionNodeRef<'_> {
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => { Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
identifier.into() identifier.into()
} }
Self::ExceptHandler(handler) => handler.into(),
} }
} }
} }
@ -300,6 +311,7 @@ pub enum DefinitionKind {
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>), ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
WithItem(WithItemDefinitionKind), WithItem(WithItemDefinitionKind),
MatchPattern(MatchPatternDefinitionKind), MatchPattern(MatchPatternDefinitionKind),
ExceptHandler(AstNodeRef<ast::ExceptHandlerExceptHandler>),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -478,3 +490,9 @@ impl From<&ast::Identifier> for DefinitionNodeKey {
Self(NodeKey::from_node(identifier)) Self(NodeKey::from_node(identifier))
} }
} }
impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self {
Self(NodeKey::from_node(handler))
}
}

View file

@ -425,6 +425,9 @@ impl<'db> TypeInferenceBuilder<'db> {
definition, definition,
); );
} }
DefinitionKind::ExceptHandler(handler) => {
self.infer_except_handler_definition(handler, definition);
}
} }
} }
@ -743,11 +746,29 @@ impl<'db> TypeInferenceBuilder<'db> {
} = try_statement; } = try_statement;
self.infer_body(body); self.infer_body(body);
for handler in handlers { for handler in handlers {
let ast::ExceptHandler::ExceptHandler(handler) = handler; let ast::ExceptHandler::ExceptHandler(handler) = handler;
self.infer_optional_expression(handler.type_.as_deref()); let ast::ExceptHandlerExceptHandler {
self.infer_body(&handler.body); type_: handled_exceptions,
name: symbol_name,
body,
range: _,
} = handler;
// If `symbol_name` is `Some()` and `handled_exceptions` is `None`,
// it's invalid syntax (something like `except as e:`).
// However, it's obvious that the user *wanted* `e` to be bound here,
// so we'll have created a definition in the semantic-index stage anyway.
if symbol_name.is_some() {
self.infer_definition(handler);
} else {
self.infer_optional_expression(handled_exceptions.as_deref());
}
self.infer_body(body);
} }
self.infer_body(orelse); self.infer_body(orelse);
self.infer_body(finalbody); self.infer_body(finalbody);
} }
@ -797,6 +818,29 @@ impl<'db> TypeInferenceBuilder<'db> {
self.types.definitions.insert(definition, context_expr_ty); self.types.definitions.insert(definition, context_expr_ty);
} }
fn infer_except_handler_definition(
&mut self,
handler: &'db ast::ExceptHandlerExceptHandler,
definition: Definition<'db>,
) {
let node_ty = handler
.type_
.as_deref()
.map(|ty| self.infer_expression(ty))
.unwrap_or(Type::Unknown);
// TODO: anything that's a consistent subtype of
// `type[BaseException] | tuple[type[BaseException], ...]` should be valid;
// anything else should be invalid --Alex
let symbol_ty = match node_ty {
Type::Any | Type::Unknown => node_ty,
Type::Class(class_ty) => Type::Instance(class_ty),
_ => Type::Unknown,
};
self.types.definitions.insert(definition, symbol_ty);
}
fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) {
let ast::StmtMatch { let ast::StmtMatch {
range: _, range: _,
@ -4180,6 +4224,108 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn except_handler_single_exception() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
import re
try:
x
except NameError as e:
pass
except re.error as f:
pass
",
)?;
assert_public_ty(&db, "src/a.py", "e", "NameError");
assert_public_ty(&db, "src/a.py", "f", "error");
assert_file_diagnostics(&db, "src/a.py", &[]);
Ok(())
}
#[test]
fn unknown_type_in_except_handler_does_not_cause_spurious_diagnostic() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
from nonexistent_module import foo
try:
x
except foo as e:
pass
",
)?;
assert_file_diagnostics(
&db,
"src/a.py",
&["Cannot resolve import 'nonexistent_module'."],
);
assert_public_ty(&db, "src/a.py", "foo", "Unknown");
assert_public_ty(&db, "src/a.py", "e", "Unknown");
Ok(())
}
#[test]
fn except_handler_multiple_exceptions() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
EXCEPTIONS = (AttributeError, TypeError)
try:
x
except (RuntimeError, OSError) as e:
pass
except EXCEPTIONS as f:
pass
",
)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
// For these TODOs we need support for `tuple` types:
// TODO: Should be `RuntimeError | OSError` --Alex
assert_public_ty(&db, "src/a.py", "e", "Unknown");
// TODO: Should be `AttributeError | TypeError` --Alex
assert_public_ty(&db, "src/a.py", "e", "Unknown");
Ok(())
}
#[test]
fn exception_handler_with_invalid_syntax() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
try:
x
except as e:
pass
",
)?;
assert_file_diagnostics(&db, "src/a.py", &[]);
assert_public_ty(&db, "src/a.py", "e", "Unknown");
Ok(())
}
#[test] #[test]
fn invalid_iterable() { fn invalid_iterable() {
let mut db = setup_db(); let mut db = setup_db();

View file

@ -33,7 +33,6 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"Use double quotes for strings", "Use double quotes for strings",
"Use double quotes for strings", "Use double quotes for strings",
"Use double quotes for strings", "Use double quotes for strings",
"/src/tomllib/_parser.py:628:75: Name 'e' used when not defined.",
]; ];
fn get_test_file(name: &str) -> TestFile { fn get_test_file(name: &str) -> TestFile {