From 1639ff48674309798ccfd3870152496111d3b044 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 9 Oct 2026 20:57:02 +0900 Subject: [PATCH 01/11] add test --- pyrefly/lib/test/calls.rs | 47 +++++++++++++++++++++++++++++++++++++ pyrefly/lib/test/imports.rs | 21 ++++++++++++----- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 10a8bc4548..682c8e4a3f 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -116,6 +116,53 @@ 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_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/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 "#, ); From ce7b412f1008a74d74221e46379349563d97629e Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 9 Oct 2026 20:57:05 +0900 Subject: [PATCH 02/11] impl --- pyrefly/lib/alt/expr.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 7b29c2cdab..9247560d78 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1238,20 +1238,31 @@ 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) => self.get_metadata_for_class(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"); + 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 { From e59110d1cba882bd910e028184ca6747ad4c50a1 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 9 Oct 2026 23:23:39 +0900 Subject: [PATCH 03/11] update test --- conformance/third_party/conformance.exp | 11 +++++++++++ pyrefly/lib/test/decorators.rs | 6 +++--- pyrefly/lib/test/descriptors.rs | 2 +- pyrefly/lib/test/incremental.rs | 7 +++++-- 4 files changed, 20 insertions(+), 6 deletions(-) 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/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/incremental.rs b/pyrefly/lib/test/incremental.rs index c534ce55b4..2e7b7b781a 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 From 3915a6ec855dae21df28b053a5c06445be7a8a5d Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 10 Oct 2026 02:49:11 +0900 Subject: [PATCH 04/11] read binding metadata without forcing full class metadata. --- pyrefly/lib/alt/answers.rs | 10 +++++----- pyrefly/lib/alt/answers_solver.rs | 16 +++++++++++----- pyrefly/lib/alt/expr.rs | 4 +++- pyrefly/lib/binding/class.rs | 9 ++++++++- pyrefly/lib/binding/metadata.rs | 3 +++ pyrefly/lib/state/state.rs | 9 +++++---- pyrefly/lib/test/incremental.rs | 16 ++++++++++++++++ 7 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pyrefly/lib/alt/answers.rs b/pyrefly/lib/alt/answers.rs index 6c686f6f39..cc1030a731 100644 --- a/pyrefly/lib/alt/answers.rs +++ b/pyrefly/lib/alt/answers.rs @@ -58,6 +58,7 @@ use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::bindings::Bindings; use crate::binding::metadata::BindingsMetadata; +use crate::binding::metadata::ClassMetadata; use crate::binding::table::TableKeyed; use crate::config::base::RecursionLimitConfig; use crate::dispatch_anyidx; @@ -79,7 +80,6 @@ use crate::table_mut_for_each; use crate::table_try_for_each; use crate::types::callable::Callable; use crate::types::class::Class; -use crate::types::class::ClassFields; use crate::types::equality::TypeEq; use crate::types::equality::TypeEqCtx; use crate::types::heap::TypeHeap; @@ -1218,8 +1218,8 @@ pub trait LookupAnswer: Sized { false } - /// Look up the class fields for a class, which may be defined in another - /// module. The fields are populated during the binding phase and can be + /// Look up binding metadata for a class, which may be defined in another + /// module. The metadata is populated during the binding phase and can be /// queried without going through the solve code path. /// /// Returns `None` if the `ClassDefIndex` is stale (e.g., the target module @@ -1227,8 +1227,8 @@ pub trait LookupAnswer: Sized { /// /// Implementations must register a class-level dependency so that /// incremental rebuilds properly invalidate dependents when class - /// fields change. - fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields>; + /// metadata changes. + fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata>; } impl Answers { diff --git a/pyrefly/lib/alt/answers_solver.rs b/pyrefly/lib/alt/answers_solver.rs index d16ecae850..e3cd38e785 100644 --- a/pyrefly/lib/alt/answers_solver.rs +++ b/pyrefly/lib/alt/answers_solver.rs @@ -77,6 +77,7 @@ use crate::binding::binding::LambdaParamId; use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::bindings::Bindings; +use crate::binding::metadata::ClassMetadata; use crate::binding::table::TableKeyed; use crate::config::base::RecursionLimitConfig; use crate::config::base::RecursionOverflowHandler; @@ -2225,20 +2226,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self.bindings().module() } - /// Look up the fields of a class from binding metadata. + /// Look up class metadata collected during binding without solving class bases or decorators. /// /// For same-module classes, reads directly from local bindings metadata. - /// For cross-module classes, delegates to `LookupAnswer::get_class_fields` + /// For cross-module classes, delegates to `LookupAnswer::get_class_binding_metadata` /// which caches metadata per module and registers class-level dependencies /// for proper incremental invalidation. /// /// Returns `None` if the `ClassDefIndex` is stale (cross-module only; /// same-module indices are always valid). - pub fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { + pub fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata> { if cls.module_path() == self.module().path() { - return Some(&self.bindings().metadata().get_class(cls.index()).fields); + return Some(self.bindings().metadata().get_class(cls.index())); } - self.answers.get_class_fields(cls) + self.answers.get_class_binding_metadata(cls) + } + + /// Look up the fields collected during class binding. + pub fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { + Some(&self.get_class_binding_metadata(cls)?.fields) } pub(crate) fn set_lambda_param_type(&self, id: LambdaParamId, ty: Type) { diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 9247560d78..658b053e6b 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1245,7 +1245,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { return; } let deprecation = match ty { - Type::ClassDef(cls) => self.get_metadata_for_class(cls).deprecation(), + Type::ClassDef(cls) => self + .get_class_binding_metadata(cls) + .and_then(|metadata| metadata.deprecation.as_ref()), _ => ty.function_deprecation(), }; let Some(deprecation) = deprecation else { diff --git a/pyrefly/lib/binding/class.rs b/pyrefly/lib/binding/class.rs index 61bcc5eac7..0cb4f5d93b 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,10 @@ 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)); 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 +561,9 @@ 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); + metadata.deprecation = deprecation; self.insert_binding_idx( class_indices.class_idx, BindingClass::ClassDef(ClassBinding { diff --git a/pyrefly/lib/binding/metadata.rs b/pyrefly/lib/binding/metadata.rs index 3a69a99a55..385124dd11 100644 --- a/pyrefly/lib/binding/metadata.rs +++ b/pyrefly/lib/binding/metadata.rs @@ -15,6 +15,7 @@ use std::sync::Arc; use pyrefly_types::class::ClassDefIndex; use pyrefly_types::class::ClassFields; +use pyrefly_types::function::Deprecation; use pyrefly_types::meta_shape_dsl::ShapeDslFunction; use ruff_python_ast::name::Name; @@ -31,6 +32,8 @@ pub struct ClassMetadata { /// because at binding time we cannot know that so we have to treat assignment /// as potentially defining a field that would not otherwise exist. pub fields: ClassFields, + /// The decorator message is available without solving class metadata. + pub deprecation: Option, } /// Metadata collected during the binding phase for all classes in a module. diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index 9485a326d3..7fd371df5e 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -103,6 +103,7 @@ use crate::binding::binding::Keyed; use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::metadata::BindingsMetadata; +use crate::binding::metadata::ClassMetadata; use crate::binding::scope::builtin_module_for_name; use crate::binding::table::TableKeyed; use crate::config::config::ConfigFile; @@ -2795,10 +2796,10 @@ pub(crate) struct TransactionHandle<'a> { /// than Handle, since ModulePath uniquely identifies the target module within /// a TransactionHandle (module name is derivable, sys_info is invariant). deferred_deps: RefCell>, - /// Cache of cross-module `BindingsMetadata` for class field lookups. + /// Cache of cross-module `BindingsMetadata` for class metadata lookups. /// Keyed by `ArcId::id()` (pointer-as-usize) to avoid atomic refcount /// operations and to get a cheap 8-byte hash key. - /// Uses `UnsafeCell` because we need to return `&ClassFields` references + /// Uses `UnsafeCell` because we need to return `&ClassMetadata` references /// into the cached `Arc` values. This is safe because: /// 1. `TransactionHandle` is single-threaded (not `Sync`). /// 2. The cache is append-only — entries are never removed or replaced, @@ -3356,7 +3357,7 @@ impl<'a> LookupAnswer for TransactionHandle<'a> { } } - fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { + fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata> { // Register a class-level dependency via get_module, which handles // both module resolution and dep tracking through deferred_deps. let module_data = self @@ -3391,7 +3392,7 @@ impl<'a> LookupAnswer for TransactionHandle<'a> { // ClassDefIndex may be stale if the target module was rebuilt with // fewer classes during this epoch (transient inconsistency that // resolves in the next epoch when rdeps are invalidated). - Some(&metadata.get_class_checked(cls.index())?.fields) + metadata.get_class_checked(cls.index()) } } diff --git a/pyrefly/lib/test/incremental.rs b/pyrefly/lib/test/incremental.rs index 2e7b7b781a..729997ed12 100644 --- a/pyrefly/lib/test/incremental.rs +++ b/pyrefly/lib/test/incremental.rs @@ -2742,6 +2742,22 @@ 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"]); +} + /// 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 From fa0bdf6dd6af32b3355683492321bb059deb5558 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 14 Oct 2026 04:36:50 +0900 Subject: [PATCH 05/11] skip depre --- test/errors.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/errors.md b/test/errors.md index 302b141118..700fd89db4 100644 --- a/test/errors.md +++ b/test/errors.md @@ -18,16 +18,13 @@ Invalid --search-path: `*/does_not_exist` does not exist (glob) ## We do report from nested with --check-all +Ignore deprecation warnings from bundled stubs so this test checks errors in the user files. + ```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 --check-all --output-format=min-text --min-severity=warn - 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] +> $PYREFLY check --python-version 3.13.0 $TMPDIR/shown2.py --check-all --output-format=min-text --min-severity=warn --ignore=deprecated */shown*.py:1:* (glob) */shown*.py:1:* (glob) [1] From 64f4d90d8001d3e2a7c25e17c68418132971b7a8 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 14 Oct 2026 04:38:14 +0900 Subject: [PATCH 06/11] fix severity --- test/errors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/errors.md b/test/errors.md index 700fd89db4..e4c77fad03 100644 --- a/test/errors.md +++ b/test/errors.md @@ -18,13 +18,13 @@ Invalid --search-path: `*/does_not_exist` does not exist (glob) ## We do report from nested with --check-all -Ignore deprecation warnings from bundled stubs so this test checks errors in the user files. +Show only errors so warnings from bundled stubs do not affect this test. ```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 --check-all --output-format=min-text --min-severity=warn --ignore=deprecated +> $PYREFLY check --python-version 3.13.0 $TMPDIR/shown2.py --check-all --output-format=min-text --min-severity=error */shown*.py:1:* (glob) */shown*.py:1:* (glob) [1] From 454ed6a00149afebb1999ae190ee45836e6a2128 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 14 Oct 2026 23:01:57 +0900 Subject: [PATCH 07/11] add depre field like is_protocol --- crates/pyrefly_types/src/class.rs | 17 +++++- crates/pyrefly_types/src/data_frame.rs | 1 + crates/pyrefly_types/src/display.rs | 1 + crates/pyrefly_types/src/meta_shape_dsl.rs | 1 + crates/pyrefly_types/src/series.rs | 1 + crates/pyrefly_types/src/shape_index.rs | 2 + crates/pyrefly_types/src/shaped_array.rs | 1 + crates/pyrefly_types/src/stdlib.rs | 1 + crates/pyrefly_types/src/type_output.rs | 1 + .../src/type_var/shape_extension.rs | 1 + pyrefly/lib/alt/answers.rs | 10 ++-- pyrefly/lib/alt/answers_solver.rs | 16 ++--- pyrefly/lib/alt/class/classdef.rs | 31 +++++----- pyrefly/lib/alt/expr.rs | 14 ++++- pyrefly/lib/alt/solve.rs | 9 +-- pyrefly/lib/binding/binding.rs | 4 +- pyrefly/lib/binding/class.rs | 2 +- pyrefly/lib/binding/metadata.rs | 3 - pyrefly/lib/solver/solver.rs | 1 + pyrefly/lib/state/state.rs | 9 ++- pyrefly/lib/test/calls.rs | 58 +++++++++++++++++++ pyrefly/lib/test/incremental.rs | 44 ++++++++++++++ pyrefly/lib/tsp/type_conversion.rs | 1 + test/errors.md | 31 +++++++++- 24 files changed, 202 insertions(+), 58 deletions(-) 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/answers.rs b/pyrefly/lib/alt/answers.rs index cc1030a731..6c686f6f39 100644 --- a/pyrefly/lib/alt/answers.rs +++ b/pyrefly/lib/alt/answers.rs @@ -58,7 +58,6 @@ use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::bindings::Bindings; use crate::binding::metadata::BindingsMetadata; -use crate::binding::metadata::ClassMetadata; use crate::binding::table::TableKeyed; use crate::config::base::RecursionLimitConfig; use crate::dispatch_anyidx; @@ -80,6 +79,7 @@ use crate::table_mut_for_each; use crate::table_try_for_each; use crate::types::callable::Callable; use crate::types::class::Class; +use crate::types::class::ClassFields; use crate::types::equality::TypeEq; use crate::types::equality::TypeEqCtx; use crate::types::heap::TypeHeap; @@ -1218,8 +1218,8 @@ pub trait LookupAnswer: Sized { false } - /// Look up binding metadata for a class, which may be defined in another - /// module. The metadata is populated during the binding phase and can be + /// Look up the class fields for a class, which may be defined in another + /// module. The fields are populated during the binding phase and can be /// queried without going through the solve code path. /// /// Returns `None` if the `ClassDefIndex` is stale (e.g., the target module @@ -1227,8 +1227,8 @@ pub trait LookupAnswer: Sized { /// /// Implementations must register a class-level dependency so that /// incremental rebuilds properly invalidate dependents when class - /// metadata changes. - fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata>; + /// fields change. + fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields>; } impl Answers { diff --git a/pyrefly/lib/alt/answers_solver.rs b/pyrefly/lib/alt/answers_solver.rs index e3cd38e785..d16ecae850 100644 --- a/pyrefly/lib/alt/answers_solver.rs +++ b/pyrefly/lib/alt/answers_solver.rs @@ -77,7 +77,6 @@ use crate::binding::binding::LambdaParamId; use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::bindings::Bindings; -use crate::binding::metadata::ClassMetadata; use crate::binding::table::TableKeyed; use crate::config::base::RecursionLimitConfig; use crate::config::base::RecursionOverflowHandler; @@ -2226,25 +2225,20 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self.bindings().module() } - /// Look up class metadata collected during binding without solving class bases or decorators. + /// Look up the fields of a class from binding metadata. /// /// For same-module classes, reads directly from local bindings metadata. - /// For cross-module classes, delegates to `LookupAnswer::get_class_binding_metadata` + /// For cross-module classes, delegates to `LookupAnswer::get_class_fields` /// which caches metadata per module and registers class-level dependencies /// for proper incremental invalidation. /// /// Returns `None` if the `ClassDefIndex` is stale (cross-module only; /// same-module indices are always valid). - pub fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata> { + pub fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { if cls.module_path() == self.module().path() { - return Some(self.bindings().metadata().get_class(cls.index())); + return Some(&self.bindings().metadata().get_class(cls.index()).fields); } - self.answers.get_class_binding_metadata(cls) - } - - /// Look up the fields collected during class binding. - pub fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { - Some(&self.get_class_binding_metadata(cls)?.fields) + self.answers.get_class_fields(cls) } pub(crate) fn set_lambda_param_type(&self, id: LambdaParamId, ty: Type) { diff --git a/pyrefly/lib/alt/class/classdef.rs b/pyrefly/lib/alt/class/classdef.rs index 8664cb3f50..27c5096373 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.clone(), ) } @@ -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 658b053e6b..569e59a9f1 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -97,6 +97,7 @@ 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::KeyYield; use crate::binding::binding::KeyYieldFrom; @@ -1245,14 +1246,21 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { return; } let deprecation = match ty { - Type::ClassDef(cls) => self - .get_class_binding_metadata(cls) - .and_then(|metadata| metadata.deprecation.as_ref()), + Type::ClassDef(cls) => cls.deprecation(), _ => ty.function_deprecation(), }; let Some(deprecation) = deprecation else { return; }; + if let Type::ClassDef(cls) = ty + && cls.module_path() == self.module().path() + && let Some(class_idx) = self.bindings().enclosing_class(range) + && let BindingClass::ClassDef(binding) = self.bindings().get(class_idx) + && binding.def_index == cls.index() + { + // References inside the defining class are part of its implementation. + return; + } let deprecated_name = match ty { Type::ClassDef(cls) => Some(if cls.module_name() == self.module().name() { cls.qname().name_relative_to_module() 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..ca28c14712 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, 13); 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 0cb4f5d93b..188bd336a4 100644 --- a/pyrefly/lib/binding/class.rs +++ b/pyrefly/lib/binding/class.rs @@ -563,7 +563,6 @@ impl<'a> BindingsBuilder<'a> { fields.reserve(0); // Attempt to shrink to capacity let metadata = self.metadata.get_class_mut(class_indices.def_index); metadata.fields = ClassFields::new(fields); - metadata.deprecation = deprecation; self.insert_binding_idx( class_indices.class_idx, BindingClass::ClassDef(ClassBinding { @@ -571,6 +570,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/binding/metadata.rs b/pyrefly/lib/binding/metadata.rs index 385124dd11..3a69a99a55 100644 --- a/pyrefly/lib/binding/metadata.rs +++ b/pyrefly/lib/binding/metadata.rs @@ -15,7 +15,6 @@ use std::sync::Arc; use pyrefly_types::class::ClassDefIndex; use pyrefly_types::class::ClassFields; -use pyrefly_types::function::Deprecation; use pyrefly_types::meta_shape_dsl::ShapeDslFunction; use ruff_python_ast::name::Name; @@ -32,8 +31,6 @@ pub struct ClassMetadata { /// because at binding time we cannot know that so we have to treat assignment /// as potentially defining a field that would not otherwise exist. pub fields: ClassFields, - /// The decorator message is available without solving class metadata. - pub deprecation: Option, } /// Metadata collected during the binding phase for all classes in a module. 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/state/state.rs b/pyrefly/lib/state/state.rs index 7fd371df5e..9485a326d3 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -103,7 +103,6 @@ use crate::binding::binding::Keyed; use crate::binding::bindings::BindingEntry; use crate::binding::bindings::BindingTable; use crate::binding::metadata::BindingsMetadata; -use crate::binding::metadata::ClassMetadata; use crate::binding::scope::builtin_module_for_name; use crate::binding::table::TableKeyed; use crate::config::config::ConfigFile; @@ -2796,10 +2795,10 @@ pub(crate) struct TransactionHandle<'a> { /// than Handle, since ModulePath uniquely identifies the target module within /// a TransactionHandle (module name is derivable, sys_info is invariant). deferred_deps: RefCell>, - /// Cache of cross-module `BindingsMetadata` for class metadata lookups. + /// Cache of cross-module `BindingsMetadata` for class field lookups. /// Keyed by `ArcId::id()` (pointer-as-usize) to avoid atomic refcount /// operations and to get a cheap 8-byte hash key. - /// Uses `UnsafeCell` because we need to return `&ClassMetadata` references + /// Uses `UnsafeCell` because we need to return `&ClassFields` references /// into the cached `Arc` values. This is safe because: /// 1. `TransactionHandle` is single-threaded (not `Sync`). /// 2. The cache is append-only — entries are never removed or replaced, @@ -3357,7 +3356,7 @@ impl<'a> LookupAnswer for TransactionHandle<'a> { } } - fn get_class_binding_metadata(&self, cls: &Class) -> Option<&ClassMetadata> { + fn get_class_fields(&self, cls: &Class) -> Option<&ClassFields> { // Register a class-level dependency via get_module, which handles // both module resolution and dep tracking through deferred_deps. let module_data = self @@ -3392,7 +3391,7 @@ impl<'a> LookupAnswer for TransactionHandle<'a> { // ClassDefIndex may be stale if the target module was rebuilt with // fewer classes during this epoch (transient inconsistency that // resolves in the next epoch when rdeps are invalidated). - metadata.get_class_checked(cls.index()) + Some(&metadata.get_class_checked(cls.index())?.fields) } } diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 682c8e4a3f..1fec2dd660 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -150,6 +150,64 @@ CurrentClass() "#, ); +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_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(), diff --git a/pyrefly/lib/test/incremental.rs b/pyrefly/lib/test/incremental.rs index 729997ed12..622e4e68a4 100644 --- a/pyrefly/lib/test/incremental.rs +++ b/pyrefly/lib/test/incremental.rs @@ -2758,6 +2758,50 @@ fn test_class_reference_deprecation_metadata_change_invalidates() { 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 e4c77fad03..46d4dd0ad1 100644 --- a/test/errors.md +++ b/test/errors.md @@ -16,15 +16,40 @@ Invalid --search-path: `*/does_not_exist` does not exist (glob) [1] ``` -## We do report from nested with --check-all +## We do not report errors from imported modules by default -Show only errors so warnings from bundled stubs do not affect this test. +```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 $ 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=error +> $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] From be744d25f5d00c2d104889cb9318ebafc457a24b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 17 Oct 2026 11:49:10 +0900 Subject: [PATCH 08/11] first comment --- pyrefly/lib/alt/expr.rs | 13 ++++++--- pyrefly/lib/test/calls.rs | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 569e59a9f1..e55c88dde2 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -99,6 +99,7 @@ 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; @@ -1254,11 +1255,15 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { }; if let Type::ClassDef(cls) = ty && cls.module_path() == self.module().path() - && let Some(class_idx) = self.bindings().enclosing_class(range) - && let BindingClass::ClassDef(binding) = self.bindings().get(class_idx) - && binding.def_index == cls.index() + && 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) { - // References inside the defining class are part of its implementation. + // The definition range includes decorators, type parameters, bases, and the body. return; } let deprecated_name = match ty { diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 1fec2dd660..b4f5b91aed 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -186,6 +186,61 @@ class Current: "#, ); +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 bf566cc0b60b74eaddb34b95c10998fbc1f1ecfc Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 17 Oct 2026 11:49:38 +0900 Subject: [PATCH 09/11] box field --- pyrefly/lib/alt/class/classdef.rs | 2 +- pyrefly/lib/binding/binding.rs | 4 ++-- pyrefly/lib/binding/class.rs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/alt/class/classdef.rs b/pyrefly/lib/alt/class/classdef.rs index 27c5096373..85f3b5bb10 100644 --- a/pyrefly/lib/alt/class/classdef.rs +++ b/pyrefly/lib/alt/class/classdef.rs @@ -88,7 +88,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self.module().dupe(), precomputed_tparams, binding.is_protocol, - binding.deprecation.clone(), + binding.deprecation.as_deref().cloned(), ) } diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index ca28c14712..9783362d9e 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -131,7 +131,7 @@ assert_words!(Binding, 4); assert_words!(BindingExpect, 12); assert_words!(BindingTypeAlias, 6); assert_words!(BindingAnnotation, 11); -assert_words!(BindingClass, 13); +assert_words!(BindingClass, 11); assert_words!(BindingTParams, 9); assert_words!(BindingClassBaseType, 3); assert_words!(BindingClassMetadata, 14); @@ -2099,7 +2099,7 @@ pub struct ClassBinding { pub def_index: ClassDefIndex, pub parent: NestingContext, pub is_protocol: bool, - pub deprecation: Option, + 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 188bd336a4..d1215eeffe 100644 --- a/pyrefly/lib/binding/class.rs +++ b/pyrefly/lib/binding/class.rs @@ -253,7 +253,8 @@ impl<'a> BindingsBuilder<'a> { let deprecation = x .decorator_list .iter() - .find_map(|decorator| parse_deprecation(&decorator.expression)); + .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); From 490834c570ed4f9c4df0ac0537f1a590a04a670d Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 17 Oct 2026 11:53:35 +0900 Subject: [PATCH 10/11] walrus --- pyrefly/lib/alt/call.rs | 9 ++++++++- pyrefly/lib/alt/expr.rs | 6 ++++-- pyrefly/lib/test/calls.rs | 42 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) 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/expr.rs b/pyrefly/lib/alt/expr.rs index e55c88dde2..1da53fe0ac 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -577,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 } diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index b4f5b91aed..0889ea457a 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -150,6 +150,48 @@ CurrentClass() "#, ); +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 a0f5292ce08275f655bd290e39cf6aeeed68e157 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 17 Oct 2026 12:12:57 +0900 Subject: [PATCH 11/11] more test --- pyrefly/lib/test/calls.rs | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 0889ea457a..45885caf3a 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -150,6 +150,85 @@ 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#"