diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no…_(b07503f9b773ea61).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no…_(b07503f9b773ea61).snap new file mode 100644 index 0000000000..b8243d669f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no…_(b07503f9b773ea61).snap @@ -0,0 +1,85 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: sync.md - With statements - Accidental use of non-async `with` +mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Manager: + 2 | async def __aenter__(self): ... + 3 | async def __aexit__(self, *args): ... + 4 | + 5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" + 6 | with Manager(): + 7 | ... + 8 | class Manager: + 9 | async def __aenter__(self): ... +10 | async def __aexit__(self, typ: str, exc, traceback): ... +11 | +12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +13 | with Manager(): +14 | ... +15 | class Manager: +16 | async def __aenter__(self, wrong_extra_arg): ... +17 | async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ... +18 | +19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +20 | with Manager(): +21 | ... +``` + +# Diagnostics + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:6:6 + | +5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and... +6 | with Manager(): + | ^^^^^^^^^ +7 | ... +8 | class Manager: + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:13:6 + | +12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an... +13 | with Manager(): + | ^^^^^^^^^ +14 | ... +15 | class Manager: + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:20:6 + | +19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an... +20 | with Manager(): + | ^^^^^^^^^ +21 | ... + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md index b180d6f55c..89d0b281da 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/sync.md +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -149,3 +149,45 @@ context_expr = Manager() with context_expr as f: reveal_type(f) # revealed: str ``` + +## Accidental use of non-async `with` + + + +If a synchronous `with` statement is used on a type with `__aenter__` and `__aexit__`, we show a +diagnostic hint that the user might have intended to use `asnyc with` instead. + +```py +class Manager: + async def __aenter__(self): ... + async def __aexit__(self, *args): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` + +The sub-diagnostic is also provided if the signatures of `__aenter__` and `__aexit__` do not match +the expected signatures for a context manager: + +```py +class Manager: + async def __aenter__(self): ... + async def __aexit__(self, typ: str, exc, traceback): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` + +Similarly, we also show the hint if the functions have the wrong number of arguments: + +```py +class Manager: + async def __aenter__(self, wrong_extra_arg): ... + async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index fcab31b7a7..5a0f3f9f1e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6133,12 +6133,31 @@ impl<'db> ContextManagerError<'db> { } => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"), }; - builder.into_diagnostic( + let mut diag = builder.into_diagnostic( format_args!( "Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}", context_expression = context_expression_type.display(db) ), ); + + // If `__aenter__` and `__aexit__` are available, the user may have intended to use `async with` instead of `with`: + if let ( + Ok(_) | Err(CallDunderError::CallError(..)), + Ok(_) | Err(CallDunderError::CallError(..)), + ) = ( + context_expression_type.try_call_dunder(db, "__aenter__", CallArgumentTypes::none()), + context_expression_type.try_call_dunder( + db, + "__aexit__", + CallArgumentTypes::positional([Type::unknown(), Type::unknown(), Type::unknown()]), + ), + ) { + diag.info(format_args!( + "Objects of type `{context_expression}` can be used as async context managers", + context_expression = context_expression_type.display(db) + )); + diag.info("Consider using `async with` here"); + } } }