Skip to content
Open
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
11 changes: 11 additions & 0 deletions conformance/third_party/conformance.exp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion crates/pyrefly_types/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -279,6 +286,7 @@ struct ClassInner {
def_index: ClassDefIndex,
qname: QName,
is_protocol: bool,
deprecation: Option<Deprecation>,
precomputed_tparams: PrecomputedTParams,
}

Expand All @@ -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()
Expand Down Expand Up @@ -353,11 +362,13 @@ impl Class {
module: Module,
precomputed_tparams: PrecomputedTParams,
is_protocol: bool,
deprecation: Option<Deprecation>,
) -> Self {
Self(Arc::new(ClassInner {
def_index,
qname: QName::new(name, parent, module),
is_protocol,
deprecation,
precomputed_tparams,
}))
}
Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/data_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
),
TArgs::default(),
)
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,7 @@ pub mod tests {
mi,
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/meta_shape_dsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3942,6 +3942,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
),
TArgs::default(),
)
Expand Down
2 changes: 2 additions & 0 deletions crates/pyrefly_types/src/shape_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,7 @@ mod tests {
fake_module("builtins"),
PrecomputedTParams::NotGeneric,
false,
None,
),
TArgs::default(),
))
Expand Down Expand Up @@ -1297,6 +1298,7 @@ mod tests {
fake_module("builtins"),
PrecomputedTParams::NotGeneric,
false,
None,
),
TArgs::new(
Arc::new(TParams::new(vec![fake_tparam(
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/shaped_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
),
TArgs::default(),
)
Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/type_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ mod tests {
mi,
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
1 change: 1 addition & 0 deletions crates/pyrefly_types/src/type_var/shape_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ mod tests {
module,
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
9 changes: 8 additions & 1 deletion pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 14 additions & 17 deletions pyrefly/lib/alt/class/classdef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,36 +63,32 @@ 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 {
PrecomputedTParams::Precomputed(Arc::new(tparams))
}
};
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(),
)
}

Expand All @@ -109,6 +105,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
self.module().dupe(),
PrecomputedTParams::NotGeneric,
false,
None,
)
}

Expand Down
46 changes: 37 additions & 9 deletions pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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(),
Comment thread
connernilsen marked this conversation as resolved.
};
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 {
Expand Down
9 changes: 1 addition & 8 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3394,14 +3394,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors: &ErrorCollector,
) -> NoneIfRecursive<Class> {
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)
}
Expand Down
4 changes: 3 additions & 1 deletion pyrefly/lib/binding/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2098,6 +2099,7 @@ pub struct ClassBinding {
pub def_index: ClassDefIndex,
pub parent: NestingContext,
pub is_protocol: bool,
pub deprecation: Option<Box<Deprecation>>,
/// 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.
Expand Down
10 changes: 9 additions & 1 deletion pyrefly/lib/binding/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -249,6 +250,11 @@ impl<'a> BindingsBuilder<'a> {
synthesized_base_classes: Vec<(TextRange, Idx<KeyClass>)>,
) {
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);
Expand Down Expand Up @@ -556,14 +562,16 @@ 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 {
def_index: class_indices.def_index,
def: ClassDefData::new(x),
parent: parent.dupe(),
is_protocol: has_protocol_base,
deprecation,
tparams_require_binding,
docstring_range,
}),
Expand Down
Loading
Loading