[ty] Add hint if async context manager is used in non-async with statement (#18299)

# Summary

Adds a subdiagnostic hint in the following scenario where a
synchronous `with` is used with an async context manager:
```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__`"
# note: Objects of type `Manager` *can* be used as async context managers
# note: Consider using `async with` here
with Manager():
    ...
```

closes https://github.com/astral-sh/ty/issues/508

## Test Plan

New MD snapshot tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
lipefree 2025-05-26 21:34:47 +02:00 committed by GitHub
parent 62ef96f51e
commit 1d20cf9570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 147 additions and 1 deletions

View file

@ -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");
}
}
}