diff --git a/conformance/third_party/conformance.exp b/conformance/third_party/conformance.exp index bb842bb6d7..837cb9cb1d 100644 --- a/conformance/third_party/conformance.exp +++ b/conformance/third_party/conformance.exp @@ -4935,6 +4935,17 @@ "stop_column": 15, "stop_line": 30 }, + { + "code": -2, + "column": 7, + "concise_description": "`_directives_deprecated_library.Ham` is deprecated", + "description": "`_directives_deprecated_library.Ham` is deprecated\n Use Spam instead", + "line": 34, + "name": "deprecated", + "severity": "warn", + "stop_column": 10, + "stop_line": 34 + }, { "code": -2, "column": 5, diff --git a/crates/pyrefly_types/src/class.rs b/crates/pyrefly_types/src/class.rs index 57b19bc077..3be8c47261 100644 --- a/crates/pyrefly_types/src/class.rs +++ b/crates/pyrefly_types/src/class.rs @@ -32,6 +32,8 @@ use ruff_text_size::TextRange; use starlark_map::small_map::SmallMap; use crate::equality::TypeEq; +use crate::equality::TypeEqCtx; +use crate::function::Deprecation; use crate::types::Substitution; use crate::types::TArgs; use crate::types::TParams; @@ -54,7 +56,12 @@ impl PartialEq for Class { } impl Eq for Class {} -impl TypeEq for Class {} +// Deprecation changes invalidate cached references without changing nominal class identity. +impl TypeEq for Class { + fn type_eq(&self, other: &Self, _ctx: &mut TypeEqCtx) -> bool { + self == other && self.deprecation() == other.deprecation() + } +} impl Ord for Class { fn cmp(&self, other: &Self) -> Ordering { @@ -279,6 +286,7 @@ struct ClassInner { def_index: ClassDefIndex, qname: QName, is_protocol: bool, + deprecation: Option, precomputed_tparams: PrecomputedTParams, } @@ -288,6 +296,7 @@ impl Debug for ClassInner { .field("index", &self.def_index) .field("qname", &self.qname) .field("is_protocol", &self.is_protocol) + .field("deprecation", &self.deprecation) .field("tparams", &self.precomputed_tparams) // We don't print `fields` because it's way too long. .finish_non_exhaustive() @@ -353,11 +362,13 @@ impl Class { module: Module, precomputed_tparams: PrecomputedTParams, is_protocol: bool, + deprecation: Option, ) -> Self { Self(Arc::new(ClassInner { def_index, qname: QName::new(name, parent, module), is_protocol, + deprecation, precomputed_tparams, })) } @@ -382,6 +393,10 @@ impl Class { self.0.is_protocol } + pub fn deprecation(&self) -> Option<&Deprecation> { + self.0.deprecation.as_ref() + } + pub fn precomputed_tparams(&self) -> &PrecomputedTParams { &self.0.precomputed_tparams } diff --git a/crates/pyrefly_types/src/data_frame.rs b/crates/pyrefly_types/src/data_frame.rs index 25c19646fe..ad3311a146 100644 --- a/crates/pyrefly_types/src/data_frame.rs +++ b/crates/pyrefly_types/src/data_frame.rs @@ -143,6 +143,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ), TArgs::default(), ) diff --git a/crates/pyrefly_types/src/display.rs b/crates/pyrefly_types/src/display.rs index f74c8dfa01..ee9614e37c 100644 --- a/crates/pyrefly_types/src/display.rs +++ b/crates/pyrefly_types/src/display.rs @@ -1955,6 +1955,7 @@ pub mod tests { mi, PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/crates/pyrefly_types/src/meta_shape_dsl.rs b/crates/pyrefly_types/src/meta_shape_dsl.rs index d45cfe453f..49ce7f6a1a 100644 --- a/crates/pyrefly_types/src/meta_shape_dsl.rs +++ b/crates/pyrefly_types/src/meta_shape_dsl.rs @@ -3942,6 +3942,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/crates/pyrefly_types/src/series.rs b/crates/pyrefly_types/src/series.rs index 5c8130fec1..413e09509d 100644 --- a/crates/pyrefly_types/src/series.rs +++ b/crates/pyrefly_types/src/series.rs @@ -69,6 +69,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ), TArgs::default(), ) diff --git a/crates/pyrefly_types/src/shape_index.rs b/crates/pyrefly_types/src/shape_index.rs index ed99f16e29..e3c930b011 100644 --- a/crates/pyrefly_types/src/shape_index.rs +++ b/crates/pyrefly_types/src/shape_index.rs @@ -1244,6 +1244,7 @@ mod tests { fake_module("builtins"), PrecomputedTParams::NotGeneric, false, + None, ), TArgs::default(), )) @@ -1297,6 +1298,7 @@ mod tests { fake_module("builtins"), PrecomputedTParams::NotGeneric, false, + None, ), TArgs::new( Arc::new(TParams::new(vec![fake_tparam( diff --git a/crates/pyrefly_types/src/shaped_array.rs b/crates/pyrefly_types/src/shaped_array.rs index 7e8f930e79..cb0576c0ab 100644 --- a/crates/pyrefly_types/src/shaped_array.rs +++ b/crates/pyrefly_types/src/shaped_array.rs @@ -1688,6 +1688,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ), TArgs::default(), ) diff --git a/crates/pyrefly_types/src/stdlib.rs b/crates/pyrefly_types/src/stdlib.rs index a3202794f4..78eed196c2 100644 --- a/crates/pyrefly_types/src/stdlib.rs +++ b/crates/pyrefly_types/src/stdlib.rs @@ -678,6 +678,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/crates/pyrefly_types/src/type_output.rs b/crates/pyrefly_types/src/type_output.rs index 0d16aa61d0..6c3b3655ef 100644 --- a/crates/pyrefly_types/src/type_output.rs +++ b/crates/pyrefly_types/src/type_output.rs @@ -332,6 +332,7 @@ mod tests { mi, PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/crates/pyrefly_types/src/type_var/shape_extension.rs b/crates/pyrefly_types/src/type_var/shape_extension.rs index 5acc235d54..b05965d9ac 100644 --- a/crates/pyrefly_types/src/type_var/shape_extension.rs +++ b/crates/pyrefly_types/src/type_var/shape_extension.rs @@ -228,6 +228,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index 2ae7e29a56..b0433739a1 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -2842,7 +2842,14 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { hint, errors, ).ty, - _ => self.freeform_call_infer(ty.clone(), &args, &kws, x.func.range(), x.arguments.range(), hint, errors).ty, + _ => { + // Match expression inference so deprecation warnings share the value's range. + let mut callee = &*x.func; + while let Expr::Named(named) = callee { + callee = &named.value; + } + self.freeform_call_infer(ty.clone(), &args, &kws, callee.range(), x.arguments.range(), hint, errors).ty + } }}); // TypeIs and TypeGuard functions return bool at runtime match result { diff --git a/pyrefly/lib/alt/class/classdef.rs b/pyrefly/lib/alt/class/classdef.rs index 8664cb3f50..85f3b5bb10 100644 --- a/pyrefly/lib/alt/class/classdef.rs +++ b/pyrefly/lib/alt/class/classdef.rs @@ -26,7 +26,7 @@ use crate::alt::types::class_metadata::ClassDisjointBase; use crate::alt::types::class_metadata::ClassMetadata; use crate::alt::types::class_metadata::ClassMro; use crate::alt::types::class_metadata::EnumMetadata; -use crate::binding::binding::ClassDefData; +use crate::binding::binding::ClassBinding; use crate::binding::binding::KeyAbstractClassCheck; use crate::binding::binding::KeyClassBaseType; use crate::binding::binding::KeyClassDisjointBase; @@ -63,23 +63,18 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } } - pub fn class_definition( - &self, - def_index: ClassDefIndex, - x: &ClassDefData, - parent: &NestingContext, - is_protocol: bool, - tparams_require_binding: bool, - errors: &ErrorCollector, - ) -> Class { + pub fn class_definition(&self, binding: &ClassBinding, errors: &ErrorCollector) -> Class { // Legacy type variables can produce a cycle, so their tparams are left to // the class's `KeyTParams` binding. Everything else is computed here, and // a class that turns out to declare nothing records no tparams at all. - let precomputed_tparams = if tparams_require_binding { + let precomputed_tparams = if binding.tparams_require_binding { PrecomputedTParams::FromBinding } else { - let tparams = - self.calculate_class_tparams_no_legacy(&x.name, x.type_params.as_deref(), errors); + let tparams = self.calculate_class_tparams_no_legacy( + &binding.def.name, + binding.def.type_params.as_deref(), + errors, + ); if tparams.is_empty() { PrecomputedTParams::NotGeneric } else { @@ -87,12 +82,13 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } }; Class::new( - def_index, - x.name.clone(), - parent.dupe(), + binding.def_index, + binding.def.name.clone(), + binding.parent.dupe(), self.module().dupe(), precomputed_tparams, - is_protocol, + binding.is_protocol, + binding.deprecation.as_deref().cloned(), ) } @@ -109,6 +105,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self.module().dupe(), PrecomputedTParams::NotGeneric, false, + None, ) } diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 7b29c2cdab..1da53fe0ac 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -97,7 +97,9 @@ use crate::alt::solve::UntypeContext; use crate::alt::unwrap::HintRef; use crate::alt::unwrap::ListElementHint; use crate::binding::binding::Binding; +use crate::binding::binding::BindingClass; use crate::binding::binding::Key; +use crate::binding::binding::KeyClassMetadata; use crate::binding::binding::KeyYield; use crate::binding::binding::KeyYieldFrom; use crate::binding::narrow::AtomicNarrowOp; @@ -575,8 +577,10 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { // at the end. _ => TypeInfo::of_ty(self.expr_infer_impl_helper(x, hint, errors, type_form_context)), }; - // Check for deprecation - self.check_for_deprecated_call(res.ty(), x.range(), errors); + // The value of an assignment expression is checked separately. + if !matches!(x, Expr::Named(..)) { + self.check_for_deprecated_call(res.ty(), x.range(), errors); + } self.record_type_trace(x.range(), res.ty()); res } @@ -1238,20 +1242,44 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } } - /// Check whether a type corresponds to a deprecated function or method, and if so, log a deprecation warning. + /// Report references to deprecated classes, functions, and methods. fn check_for_deprecated_call(&self, ty: &Type, range: TextRange, errors: &ErrorCollector) { if ty.property_metadata().is_some() { // This prevents misfiring deprecation warnings on property setters and deleters. return; } - let Some(deprecation) = ty.function_deprecation() else { + let deprecation = match ty { + Type::ClassDef(cls) => cls.deprecation(), + _ => ty.function_deprecation(), + }; + let Some(deprecation) = deprecation else { return; }; - let deprecated_function = ty - .to_func_kind() - .map(|func_kind| func_kind.format(self.module().name())); - if let Some(deprecated_function) = deprecated_function { - let header = format!("`{deprecated_function}` is deprecated"); + if let Type::ClassDef(cls) = ty + && cls.module_path() == self.module().path() + && let Some(metadata_idx) = self + .bindings() + .key_to_idx_hashed_opt(Hashed::new(&KeyClassMetadata(cls.index()))) + && let BindingClass::ClassDef(binding) = self + .bindings() + .get(self.bindings().get(metadata_idx).class_idx) + && binding.def.range.contains_range(range) + { + // The definition range includes decorators, type parameters, bases, and the body. + return; + } + let deprecated_name = match ty { + Type::ClassDef(cls) => Some(if cls.module_name() == self.module().name() { + cls.qname().name_relative_to_module() + } else { + cls.qname().module_qualified_name() + }), + _ => ty + .to_func_kind() + .map(|func_kind| func_kind.format(self.module().name())), + }; + if let Some(deprecated_name) = deprecated_name { + let header = format!("`{deprecated_name}` is deprecated"); let detail = deprecation.as_error_detail(); let mut builder = errors.error_builder(range, ErrorKind::Deprecated, header); if let Some(detail) = detail { diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 43c9bf8a10..53244d8661 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -3394,14 +3394,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { errors: &ErrorCollector, ) -> NoneIfRecursive { let cls = match cls { - BindingClass::ClassDef(x) => self.class_definition( - x.def_index, - &x.def, - &x.parent, - x.is_protocol, - x.tparams_require_binding, - errors, - ), + BindingClass::ClassDef(x) => self.class_definition(x, errors), BindingClass::FunctionalClassDef(def_index, x, parent) => { self.functional_class_definition(*def_index, x, parent) } diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 808164f33a..9783362d9e 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -22,6 +22,7 @@ use pyrefly_python::nesting_context::NestingContext; use pyrefly_python::short_identifier::ShortIdentifier; use pyrefly_python::symbol_kind::SymbolKind; use pyrefly_types::function::BodyKind; +use pyrefly_types::function::Deprecation; use pyrefly_types::heap::TypeHeap; use pyrefly_types::meta_shape_dsl::ShapeDslFunction; use pyrefly_types::special_form::SpecialForm; @@ -130,7 +131,7 @@ assert_words!(Binding, 4); assert_words!(BindingExpect, 12); assert_words!(BindingTypeAlias, 6); assert_words!(BindingAnnotation, 11); -assert_words!(BindingClass, 10); +assert_words!(BindingClass, 11); assert_words!(BindingTParams, 9); assert_words!(BindingClassBaseType, 3); assert_words!(BindingClassMetadata, 14); @@ -2098,6 +2099,7 @@ pub struct ClassBinding { pub def_index: ClassDefIndex, pub parent: NestingContext, pub is_protocol: bool, + pub deprecation: Option>, /// Were we able to determine, using only syntactic analysis at bindings time, /// that there can be no legacy tparams? If no, we need a `BindingTParams`, if yes /// we can directly compute the `TParams` from the class def. diff --git a/pyrefly/lib/binding/class.rs b/pyrefly/lib/binding/class.rs index 61bcc5eac7..d1215eeffe 100644 --- a/pyrefly/lib/binding/class.rs +++ b/pyrefly/lib/binding/class.rs @@ -82,6 +82,7 @@ use crate::binding::scope::FlowStyle; use crate::binding::scope::Scope; use crate::binding::shape_type::ShapeDeclarationOwner; use crate::config::error_kind::ErrorKind; +use crate::export::deprecation::parse_deprecation; use crate::export::special::SpecialExport; use crate::types::class::ClassDefIndex; use crate::types::class::ClassFieldProperties; @@ -249,6 +250,11 @@ impl<'a> BindingsBuilder<'a> { synthesized_base_classes: Vec<(TextRange, Idx)>, ) { let (mut class_object, class_indices) = self.class_object_and_indices(&x.name); + let deprecation = x + .decorator_list + .iter() + .find_map(|decorator| parse_deprecation(&decorator.expression)) + .map(Box::new); let mut pydantic_config_dict = PydanticConfigDict::default(); let docstring_range = Docstring::range_from_stmts(x.body.as_slice()); let body = mem::take(&mut x.body); @@ -556,7 +562,8 @@ impl<'a> BindingsBuilder<'a> { } fields.reserve(0); // Attempt to shrink to capacity - self.metadata.get_class_mut(class_indices.def_index).fields = ClassFields::new(fields); + let metadata = self.metadata.get_class_mut(class_indices.def_index); + metadata.fields = ClassFields::new(fields); self.insert_binding_idx( class_indices.class_idx, BindingClass::ClassDef(ClassBinding { @@ -564,6 +571,7 @@ impl<'a> BindingsBuilder<'a> { def: ClassDefData::new(x), parent: parent.dupe(), is_protocol: has_protocol_base, + deprecation, tparams_require_binding, docstring_range, }), diff --git a/pyrefly/lib/solver/solver.rs b/pyrefly/lib/solver/solver.rs index 71a523460d..82ab1fb916 100644 --- a/pyrefly/lib/solver/solver.rs +++ b/pyrefly/lib/solver/solver.rs @@ -4776,6 +4776,7 @@ mod tests { module, PrecomputedTParams::NotGeneric, false, + None, ), targs, ) diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 10a8bc4548..45885caf3a 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -116,6 +116,287 @@ old_function() # E: `old_function` is deprecated "#, ); +testcase!( + test_deprecated_class, + r#" +from warnings import deprecated + +@deprecated("Use NewClass instead") +class OldClass: + def method(self) -> int: + return 1 + +instance = OldClass() # E: Use NewClass instead +instance.method() +alias = OldClass # E: `OldClass` is deprecated +alias() # E: `OldClass` is deprecated + +@deprecated("Use NewGeneric instead") +class OldGeneric[T]: ... + +OldGeneric[int]() # E: `OldGeneric` is deprecated + +class Outer: + @deprecated("Use NewClass instead") + class Inner: ... + +Outer.Inner() # E: `Outer.Inner` is deprecated + +class Subclass(OldClass): ... # E: `OldClass` is deprecated +Subclass() + +class CurrentClass: ... +CurrentClass() + "#, +); + +testcase!( + test_deprecated_class_header_and_nested, + r#" +from warnings import deprecated + +class Base[T]: ... + +@deprecated("x") +class Old(Base["Old"]): + class Inner: + def f(self) -> "Old": ... + +@deprecated("x") +class Bounded[T: "Bounded"]: ... + "#, +); + +testcase!( + test_deprecated_class_annotation_forms, + r#" +from warnings import deprecated + +@deprecated("x") +class Old: ... + +def f(a: Old | None, b: type[Old], c: object) -> None: # E: `Old` is deprecated # E: `Old` is deprecated + if isinstance(c, Old): # E: `Old` is deprecated + pass + try: + pass + except Exception as e: + pass + "#, +); + +testcase!( + test_deprecated_class_and_init, + r#" +from warnings import deprecated + +@deprecated("class") +class Old: + @deprecated("init") + def __init__(self) -> None: ... + +Old() # E: `Old` is deprecated # E: `Old.__init__` is deprecated + "#, +); + +testcase!( + test_deprecated_class_qualified_decorator, + r#" +import dataclasses +import enum +import typing +import typing_extensions + +@typing_extensions.deprecated("x") +class A: ... + +@typing_extensions.deprecated("x") +@dataclasses.dataclass +class D: + x: int + +@typing_extensions.deprecated("x") +class E(enum.Enum): + X = 1 + +@typing_extensions.deprecated("x") +class P(typing.Protocol): ... + +A() # E: `A` is deprecated +D(1) # E: `D` is deprecated +E.X # E: `E` is deprecated +def g(p: P) -> None: ... # E: `P` is deprecated + "#, +); + +testcase!( + test_deprecated_walrus_reference, + r#" +from warnings import deprecated + +@deprecated("Use New instead") +class Old: ... + +(alias := Old) # E: `Old` is deprecated +alias() # E: `Old` is deprecated +(called_alias := Old)() # E: `Old` is deprecated +(outer_alias := (inner_alias := Old)) # E: `Old` is deprecated +(instance := Old()) # E: `Old` is deprecated + +@deprecated("Use new_function instead") +def old_function() -> None: ... + +(function_alias := old_function) # E: `old_function` is deprecated +function_alias() # E: `old_function` is deprecated +(called_function_alias := old_function)() # E: `old_function` is deprecated +(outer_function := (inner_function := old_function))() # E: `old_function` is deprecated + "#, +); + +testcase!( + test_deprecated_class_returned_instance, + r#" +from warnings import deprecated + +@deprecated("Use New instead") +class Old: + def method(self) -> None: ... + +class Current: + def foo(self) -> Old: ... # E: `Old` is deprecated + +Current().foo() +instance = Current().foo() +instance.method() + "#, +); + +testcase!( + test_deprecated_class_self_reference, + r#" +from __future__ import annotations +from warnings import deprecated + +@deprecated("Use New instead") +class Old: + def foo(self) -> "Old": + return Old() + + def bar(self) -> Old: + return self.foo() + + @classmethod + def make(cls) -> Old: + return Old() + + @staticmethod + def make_static() -> Old: + return Old() + + def nested(self) -> Old: + def make() -> Old: + return Old() + return make() + +def make() -> Old: # E: `Old` is deprecated + return Old() # E: `Old` is deprecated + +class Current: + def make(self) -> Old: # E: `Old` is deprecated + return Old() # E: `Old` is deprecated + "#, +); + +testcase!( + test_deprecated_class_self_reference_in_bases, + r#" +from warnings import deprecated + +class Base[T]: ... + +@deprecated("Use New instead") +class Old(Base["Old"]): ... + +class Current(Base[Old]): ... # E: `Old` is deprecated + +Old() # E: `Old` is deprecated + "#, +); + +testcase!( + test_deprecated_class_self_reference_in_type_parameters, + r#" +from warnings import deprecated + +@deprecated("Use New instead") +class Bound[T: "Bound"]: ... + +@deprecated("Use New instead") +class Constraints[T: ("Constraints", int)]: ... + +@deprecated("Use New instead") +class Default[T = "Default"]: ... + +class Current[T: Bound]: ... # E: `Bound` is deprecated + +Bound() # E: `Bound` is deprecated +Constraints() # E: `Constraints` is deprecated +Default() # E: `Default` is deprecated + "#, +); + +testcase!( + test_deprecated_class_self_reference_in_decorators, + r#" +from typing import Callable, cast +from warnings import deprecated + +def identity[T](cls: type[T]) -> type[T]: + return cls + +@cast("Callable[[type[Old]], type[Old]]", identity) +@deprecated("Use New instead") +class Old: ... + +Old() # E: `Old` is deprecated + "#, +); + +testcase!( + test_deprecated_generic_class_self_reference, + r#" +from warnings import deprecated + +@deprecated("Use New instead") +class Old[T]: + def clone(self) -> "Old[T]": + return Old[T]() + + def other(self) -> "Other": # E: `Other` is deprecated + return Other() # E: `Other` is deprecated + +@deprecated("Use New instead") +class Other: + def clone(self) -> "Other": + return Other() + +Old[int]() # E: `Old` is deprecated + "#, +); + +testcase!( + test_deprecated_class_3_12, + test_env_3_12(), + r#" +from typing_extensions import deprecated + +@deprecated("Use NewClass instead", category=None) +class OldClass: ... + +OldClass() # E: `OldClass` is deprecated + "#, +); + testcase!( test_deprecated_function_reference, r#" diff --git a/pyrefly/lib/test/decorators.rs b/pyrefly/lib/test/decorators.rs index 33a86f6f71..4f24fe0a6b 100644 --- a/pyrefly/lib/test/decorators.rs +++ b/pyrefly/lib/test/decorators.rs @@ -210,7 +210,7 @@ from typing import assert_type import abc class C(abc.ABC): - @abc.abstractclassmethod + @abc.abstractclassmethod # E: `abc.abstractclassmethod` is deprecated def f(cls) -> int: return 42 @@ -244,7 +244,7 @@ from typing import assert_type import abc class C(abc.ABC): - @abc.abstractstaticmethod + @abc.abstractstaticmethod # E: `abc.abstractstaticmethod` is deprecated def f() -> int: return 42 @@ -275,7 +275,7 @@ from enum import member, nonmember @abstractmethod # E: can only be used on methods @staticmethod # E: can only be used on methods @classmethod # E: can only be used on methods -@abstractstaticmethod # E: can only be used on methods +@abstractstaticmethod # E: can only be used on methods # E: `abc.abstractstaticmethod` is deprecated @property # E: can only be used on methods @final # E: can only be used on methods @override # E: can only be used on methods diff --git a/pyrefly/lib/test/descriptors.rs b/pyrefly/lib/test/descriptors.rs index e6ae8bb6db..ae867427a9 100644 --- a/pyrefly/lib/test/descriptors.rs +++ b/pyrefly/lib/test/descriptors.rs @@ -125,7 +125,7 @@ testcase!( from typing import assert_type from abc import ABC, abstractproperty # E: `abstractproperty` is deprecated class C(ABC): - @abstractproperty + @abstractproperty # E: `abc.abstractproperty` is deprecated def foo(self) -> int: return 42 def f(c: C): diff --git a/pyrefly/lib/test/imports.rs b/pyrefly/lib/test/imports.rs index 0703744f9a..7e5452de8e 100644 --- a/pyrefly/lib/test/imports.rs +++ b/pyrefly/lib/test/imports.rs @@ -1163,7 +1163,7 @@ fn env_class_x_deprecated() -> TestEnv { from warnings import deprecated @deprecated("Don't use this") class X: ... -x: X = X() +x: X = X() # E: `X` is deprecated # E: `X` is deprecated "#, ) } @@ -1174,18 +1174,27 @@ testcase!( r#" from foo import X # E: `X` is deprecated -x = X() +x = X() # E: `foo.X` is deprecated "#, ); testcase!( - bug = "When something is imported via *, we should warn on usage not on import", test_import_star_deprecated_class_warn, env_class_x_deprecated(), r#" from foo import * -x = X() +x = X() # E: `foo.X` is deprecated +"#, +); + +testcase!( + test_import_module_deprecated_class_warn, + env_class_x_deprecated(), + r#" +import foo + +x = foo.X() # E: `foo.X` is deprecated "#, ); @@ -1265,7 +1274,7 @@ from mypkg import MyClass # E: `MyClass` is deprecated from mypkg import my_func # E: `my_func` is deprecated from mypkg import NonDeprecatedClass -_ = MyClass() +_ = MyClass() # E: `mypkg._private.MyClass` is deprecated my_func() # E: `mypkg._private.my_func` is deprecated "#, ); @@ -1305,7 +1314,7 @@ testcase!( r#" from pkg.outer import MiddleClass # E: `MiddleClass` is deprecated -_ = MiddleClass() +_ = MiddleClass() # E: `pkg.origin.DepClass` is deprecated "#, ); diff --git a/pyrefly/lib/test/incremental.rs b/pyrefly/lib/test/incremental.rs index c534ce55b4..622e4e68a4 100644 --- a/pyrefly/lib/test/incremental.rs +++ b/pyrefly/lib/test/incremental.rs @@ -2727,8 +2727,11 @@ fn test_class_deprecation_metadata_change_invalidates() { // foo.C is NOT deprecated initially i.set("foo", "class C:\n x: int = 1"); - // # E: comment is present for the second check when deprecation warning appears - i.set("main", "from foo import C # E:\nc = C()"); + // The second check expects deprecation warnings at the import and constructor call. + i.set( + "main", + "from foo import C # E: `C` is deprecated\nc = C() # E: `foo.C` is deprecated", + ); i.check_ignoring_expectations(&["foo", "main"], &["foo", "main"]); // Add @deprecated decorator to class - main should be recomputed @@ -2739,6 +2742,66 @@ fn test_class_deprecation_metadata_change_invalidates() { i.check(&["foo", "main"], &["foo", "main"]); } +/// A class reference depends on deprecation metadata even when no constructor is called. +#[test] +fn test_class_reference_deprecation_metadata_change_invalidates() { + let mut i = Incremental::new(); + + i.set("foo", "class C: pass"); + i.set("main", "import foo\ncls = foo.C # E: use D instead"); + i.check_ignoring_expectations(&["foo", "main"], &["foo", "main"]); + + i.set( + "foo", + "from warnings import deprecated\n@deprecated('use D instead')\nclass C: pass", + ); + i.check(&["foo", "main"], &["foo", "main"]); +} + +/// Class aliases carry deprecation changes through their exported type. +#[test] +fn test_class_alias_deprecation_change_invalidates() { + let mut i = Incremental::new(); + i.set("foo", "class C: pass\nAlias = C"); + i.set("main", "import foo\ncls = foo.Alias # E: use D instead"); + i.check_ignoring_expectations(&["foo", "main"], &["foo", "main"]); + + i.set( + "foo", + "from warnings import deprecated\n@deprecated('use D instead')\nclass C: pass\nAlias = C # E: use D instead", + ); + i.check(&["foo", "main"], &["foo", "main"]); + + i.set( + "foo", + "from warnings import deprecated\n@deprecated('use E instead')\nclass C: pass\nAlias = C # E: use E instead", + ); + let result = i.check_ignoring_expectations(&["foo", "main"], &["foo", "main"]); + let errors = result.errors.collect_display_errors(); + assert_eq!(errors.len(), 2); + assert!( + errors + .iter() + .all(|error| error.msg().contains("use E instead")) + ); + + i.set("foo", "class C: pass\nAlias = C"); + let result = i.check_ignoring_expectations(&["foo", "main"], &["foo", "main"]); + assert!(result.errors.collect_display_errors().is_empty()); +} + +/// Reading a class object does not depend on the types of its methods. +#[test] +fn test_class_reference_method_change_does_not_invalidate() { + let mut i = Incremental::new(); + i.set("foo", "class C:\n def method(self) -> int: return 1"); + i.set("main", "import foo\ncls = foo.C"); + i.check(&["foo", "main"], &["foo", "main"]); + + i.set("foo", "class C:\n def method(self) -> str: return 'new'"); + i.check(&["foo", "main"], &["foo"]); +} + /// Test that when an export changes from direct definition to re-export, dependents are invalidated. /// /// This tests the `reexport_source` metadata dependency. When an export changes from being diff --git a/pyrefly/lib/tsp/type_conversion.rs b/pyrefly/lib/tsp/type_conversion.rs index 4b122dd3c0..8d93ff99e9 100644 --- a/pyrefly/lib/tsp/type_conversion.rs +++ b/pyrefly/lib/tsp/type_conversion.rs @@ -207,6 +207,7 @@ fn test_class(module_name: ModuleName, name: &str) -> PyreflyClassType { module, PrecomputedTParams::NotGeneric, false, + None, ); PyreflyClassType::new(class, TArgs::default()) } diff --git a/test/errors.md b/test/errors.md index 302b141118..46d4dd0ad1 100644 --- a/test/errors.md +++ b/test/errors.md @@ -16,6 +16,17 @@ Invalid --search-path: `*/does_not_exist` does not exist (glob) [1] ``` +## We do not report errors from imported modules by default + +```scrut +$ touch $TMPDIR/pyrefly.toml && \ +> echo "x: str = 12" > $TMPDIR/shown1.py && \ +> echo "import shown1; y: int = shown1.x" > $TMPDIR/shown2.py && \ +> $PYREFLY check --python-version 3.13.0 $TMPDIR/shown2.py --output-format=min-text --min-severity=warn +ERROR */shown2.py:1:* (glob) +[1] +``` + ## We do report from nested with --check-all ```scrut @@ -23,11 +34,22 @@ $ touch $TMPDIR/pyrefly.toml && \ > echo "x: str = 12" > $TMPDIR/shown1.py && \ > echo "import shown1; y: int = shown1.x" > $TMPDIR/shown2.py && \ > $PYREFLY check --python-version 3.13.0 $TMPDIR/shown2.py --check-all --output-format=min-text --min-severity=warn + WARN ast.pyi:1230:16-21: `slice` is deprecated [deprecated] + WARN ast.pyi:1234:13-18: `slice` is deprecated [deprecated] + WARN ast.pyi:2176:36-44: `ExtSlice` is deprecated [deprecated] + WARN ast.pyi:2177:33-38: `Index` is deprecated [deprecated] + WARN ast.pyi:2178:33-38: `Suite` is deprecated [deprecated] + WARN ast.pyi:2179:35-42: `AugLoad` is deprecated [deprecated] + WARN ast.pyi:2180:36-44: `AugStore` is deprecated [deprecated] + WARN ast.pyi:2181:33-38: `Param` is deprecated [deprecated] + WARN importlib/abc.pyi:83:50-64: `ResourceLoader` is deprecated [deprecated] WARN importlib/abc.pyi:147:9-41: `ResourceReader` is deprecated [deprecated] WARN importlib/machinery.pyi:14:5-51: `WindowsRegistryFinder` is deprecated [deprecated] WARN importlib/resources/__init__.pyi:49:9-29: `contents` is deprecated [deprecated] WARN importlib/resources/__init__.pyi:79:41-73: `ResourceReader` is deprecated [deprecated] WARN importlib/resources/_common.pyi:8:41-55: `ResourceReader` is deprecated [deprecated] + WARN importlib/resources/_common.pyi:31:59-73: `importlib.resources.abc.ResourceReader` is deprecated [deprecated] + WARN importlib/resources/abc.pyi:58:32-46: `ResourceReader` is deprecated [deprecated] */shown*.py:1:* (glob) */shown*.py:1:* (glob) [1]