Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,8 +945,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors: &ErrorCollector,
context: Option<&dyn Fn() -> ErrorContext>,
) {
if let Some(metadata) = target.function_metadata()
&& let Some(deprecation) = &metadata.flags.deprecation
let Some(metadata) = target.function_metadata() else {
return;
};
// A callback's deprecated `__call__` applies to calls, not references to the callback.
let callback = if metadata.flags.deprecation.is_none()
&& let FunctionKind::CallbackProtocol(cls) = &metadata.kind
{
self.instance_as_dunder_call(cls)
} else {
None
};
if let Some(deprecation) = metadata
.flags
.deprecation
.as_deref()
.or_else(|| callback.as_ref().and_then(Type::function_deprecation))
{
let header = format!(
"`{}` is deprecated",
Expand Down
10 changes: 8 additions & 2 deletions pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,8 +904,8 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors: &ErrorCollector,
type_form_context: Option<TypeFormContext<'_>>,
) -> TypeInfo {
// The value of an assignment expression is checked separately.
let mut check_deprecation = !matches!(x, Expr::Named(..));
// Assignment values and branches of conditional and boolean expressions are checked separately.
let mut check_deprecation = !matches!(x, Expr::Named(..) | Expr::If(..) | Expr::BoolOp(..));
let res = match x {
Expr::Name(x) => {
if Ast::is_synthesized_empty_name(x) {
Expand Down Expand Up @@ -1654,6 +1654,12 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {

/// Report references to deprecated classes, functions, and methods.
fn check_for_deprecated_call(&self, ty: &Type, range: TextRange, errors: &ErrorCollector) {
if let Type::Union(union) = ty {
for member in &union.members {
self.check_for_deprecated_call(member, range, errors);
}
return;
}
if ty.property_metadata().is_some() {
// This prevents misfiring deprecation warnings on property setters and deleters.
return;
Expand Down
4 changes: 4 additions & 0 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use pyrefly_types::types::TParamsSource;
use pyrefly_util::display::pluralize;
use pyrefly_util::owner::Owner;
use pyrefly_util::prelude::SliceExt;
use pyrefly_util::prelude::VecExt;
use pyrefly_util::visit::Visit;
use ruff_python_ast::Expr;
use ruff_python_ast::Identifier;
Expand Down Expand Up @@ -1922,6 +1923,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
{
original_decoratee.clone()
}
Type::Union(u) => self.unions(u.members.into_map(|member| {
self.decorate_returned_callable(member, metadata, original_decoratee)
})),
returned_ty => returned_ty,
}
}
Expand Down
90 changes: 90 additions & 0 deletions pyrefly/lib/test/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,96 @@ fn test_env_3_12() -> TestEnv {
})
}

testcase!(
test_deprecated_union_decorator,
r#"
from collections.abc import Callable
from typing import assert_type
from warnings import deprecated

def cache[**P, T](fn: Callable[P, T]) -> Callable[P, T] | Callable[[str], T]: ...

@deprecated("Use another function")
@cache
def old_function(value: str) -> str: ...

assert_type(old_function(""), str) # E: `old_function` is deprecated
_ = old_function # E: `old_function` is deprecated

@cache
@deprecated("Use another function")
def inner_deprecated(value: str) -> str: ...

assert_type(inner_deprecated(""), str) # E: `inner_deprecated` is deprecated
_ = inner_deprecated # E: `inner_deprecated` is deprecated

@cache
def current_function(value: str) -> str: ...

assert_type(current_function(""), str)
_ = current_function
"#,
);

testcase!(
test_deprecated_union_callback_decorator,
r#"
from collections.abc import Callable
from typing import Protocol, assert_type
from warnings import deprecated

class P(Protocol):
@deprecated("Use another callback")
def __call__(self, x: int) -> int: ...

def dec(fn: Callable[[int], int]) -> P | None: ...

@dec
def f(x: int) -> int: ...

if f is not None:
assert_type(f(1), int) # E: Use another callback

def direct_dec(fn: Callable[[int], int]) -> P: ...

@direct_dec
def direct(x: int) -> int: ...

_ = direct
assert_type(direct(1), int) # E: Use another callback

@deprecated("Use another function")
@direct_dec
def both(x: int) -> int: ...

_ = both # E: Use another function
assert_type(both(1), int) # E: Use another function
"#,
);

testcase!(
test_deprecated_union_expression,
r#"
from collections.abc import Callable
from typing import assert_type
from warnings import deprecated

@deprecated("Use new instead")
def old(x: int) -> int: ...

def new(x: int) -> int: ...

def select(flag: bool, cb: Callable[[int], int] | None) -> None:
g = old if flag else new # E: `old` is deprecated
h = cb or old # E: `old` is deprecated
_ = flag and old # E: `old` is deprecated
_ = old if flag else old # E: `old` is deprecated # E: `old` is deprecated
_ = cb or (old if flag else new) # E: `old` is deprecated
assert_type(g(1), int) # E: `old` is deprecated
assert_type(h(1), int) # E: `old` is deprecated
"#,
);

testcase!(
test_deprecated_call_3_12,
test_env_3_12(),
Expand Down
Loading