diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index a7b6b6f90..6768ccdc2 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -55,7 +55,7 @@ fn eval_code<'a>( _ => expr.eval(vm)?, }; - output = ops::join(output, value).at(span)?; + output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; if let Some(event) = &vm.flow { warn_for_discarded_content(&mut vm.engine, event, &output); diff --git a/crates/typst-eval/src/flow.rs b/crates/typst-eval/src/flow.rs index b5ba487f5..0e7d3e63c 100644 --- a/crates/typst-eval/src/flow.rs +++ b/crates/typst-eval/src/flow.rs @@ -83,7 +83,8 @@ impl Eval for ast::WhileLoop<'_> { } let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; + let span = body.span(); + output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; match vm.flow { Some(FlowEvent::Break(_)) => { @@ -129,7 +130,9 @@ impl Eval for ast::ForLoop<'_> { let body = self.body(); let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; + let span = body.span(); + output = + ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; match vm.flow { Some(FlowEvent::Break(_)) => { diff --git a/crates/typst-eval/src/ops.rs b/crates/typst-eval/src/ops.rs index ebbd67430..f2a8f6c64 100644 --- a/crates/typst-eval/src/ops.rs +++ b/crates/typst-eval/src/ops.rs @@ -1,4 +1,4 @@ -use typst_library::diag::{At, HintedStrResult, SourceResult}; +use typst_library::diag::{At, DeprecationSink, HintedStrResult, SourceResult}; use typst_library::foundations::{ops, IntoValue, Value}; use typst_syntax::ast::{self, AstNode}; @@ -23,22 +23,22 @@ impl Eval for ast::Binary<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { match self.op() { - ast::BinOp::Add => apply_binary(self, vm, ops::add), + ast::BinOp::Add => apply_binary_with_sink(self, vm, ops::add), ast::BinOp::Sub => apply_binary(self, vm, ops::sub), ast::BinOp::Mul => apply_binary(self, vm, ops::mul), ast::BinOp::Div => apply_binary(self, vm, ops::div), ast::BinOp::And => apply_binary(self, vm, ops::and), ast::BinOp::Or => apply_binary(self, vm, ops::or), - ast::BinOp::Eq => apply_binary(self, vm, ops::eq), - ast::BinOp::Neq => apply_binary(self, vm, ops::neq), + ast::BinOp::Eq => apply_binary_with_sink(self, vm, ops::eq), + ast::BinOp::Neq => apply_binary_with_sink(self, vm, ops::neq), ast::BinOp::Lt => apply_binary(self, vm, ops::lt), ast::BinOp::Leq => apply_binary(self, vm, ops::leq), ast::BinOp::Gt => apply_binary(self, vm, ops::gt), ast::BinOp::Geq => apply_binary(self, vm, ops::geq), - ast::BinOp::In => apply_binary(self, vm, ops::in_), - ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in), + ast::BinOp::In => apply_binary_with_sink(self, vm, ops::in_), + ast::BinOp::NotIn => apply_binary_with_sink(self, vm, ops::not_in), ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)), - ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add), + ast::BinOp::AddAssign => apply_assignment_with_sink(self, vm, ops::add), ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub), ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul), ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div), @@ -65,6 +65,18 @@ fn apply_binary( op(lhs, rhs).at(binary.span()) } +/// Apply a basic binary operation, with the possiblity of deprecations. +fn apply_binary_with_sink( + binary: ast::Binary, + vm: &mut Vm, + op: impl Fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult, +) -> SourceResult { + let span = binary.span(); + let lhs = binary.lhs().eval(vm)?; + let rhs = binary.rhs().eval(vm)?; + op(lhs, rhs, &mut (&mut vm.engine, span)).at(span) +} + /// Apply an assignment operation. fn apply_assignment( binary: ast::Binary, @@ -89,3 +101,23 @@ fn apply_assignment( *location = op(lhs, rhs).at(binary.span())?; Ok(Value::None) } + +/// Apply an assignment operation, with the possiblity of deprecations. +fn apply_assignment_with_sink( + binary: ast::Binary, + vm: &mut Vm, + op: fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult, +) -> SourceResult { + let rhs = binary.rhs().eval(vm)?; + let location = binary.lhs().access(vm)?; + let lhs = std::mem::take(&mut *location); + let mut sink = vec![]; + let span = binary.span(); + *location = op(lhs, rhs, &mut (&mut sink, span)).at(span)?; + if !sink.is_empty() { + for warning in sink { + vm.engine.sink.warn(warning); + } + } + Ok(Value::None) +} diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 49cbd02c6..bf8dc0e4f 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -232,18 +232,42 @@ impl From for SourceDiagnostic { /// Destination for a deprecation message when accessing a deprecated value. pub trait DeprecationSink { /// Emits the given deprecation message into this sink. - fn emit(self, message: &str); + fn emit(&mut self, message: &str); + + /// Emits the given deprecation message into this sink, with the given + /// hints. + fn emit_with_hints(&mut self, message: &str, hints: &[&str]); } impl DeprecationSink for () { - fn emit(self, _: &str) {} + fn emit(&mut self, _: &str) {} + fn emit_with_hints(&mut self, _: &str, _: &[&str]) {} +} + +impl DeprecationSink for (&mut Vec, Span) { + fn emit(&mut self, message: &str) { + self.0.push(SourceDiagnostic::warning(self.1, message)); + } + + fn emit_with_hints(&mut self, message: &str, hints: &[&str]) { + self.0.push( + SourceDiagnostic::warning(self.1, message) + .with_hints(hints.iter().copied().map(Into::into)), + ); + } } impl DeprecationSink for (&mut Engine<'_>, Span) { - /// Emits the deprecation message as a warning. - fn emit(self, message: &str) { + fn emit(&mut self, message: &str) { self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); } + + fn emit_with_hints(&mut self, message: &str, hints: &[&str]) { + self.0.sink.warn( + SourceDiagnostic::warning(self.1, message) + .with_hints(hints.iter().copied().map(Into::into)), + ); + } } /// A part of a diagnostic's [trace](SourceDiagnostic::trace). diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index aad7266bc..8c921a13d 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use typst_syntax::{Span, Spanned}; -use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::diag::{ + bail, At, DeprecationSink, HintedStrResult, SourceDiagnostic, SourceResult, StrResult, +}; use crate::engine::Engine; use crate::foundations::{ cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, @@ -143,6 +145,11 @@ impl Array { Ok(self.iter().cloned().cycle().take(count).collect()) } + + /// The internal implementation of [`Array::contains`]. + pub fn contains_impl(&self, value: &Value, sink: &mut dyn DeprecationSink) -> bool { + self.0.iter().any(|v| ops::equal(v, value, sink)) + } } #[scope] @@ -290,10 +297,12 @@ impl Array { #[func] pub fn contains( &self, + engine: &mut Engine, + span: Span, /// The value to search for. value: Value, ) -> bool { - self.0.contains(&value) + self.contains_impl(&value, &mut (engine, span)) } /// Searches for an item for which the given function returns `{true}` and @@ -576,6 +585,8 @@ impl Array { #[func] pub fn sum( self, + engine: &mut Engine, + span: Span, /// What to return if the array is empty. Must be set if the array can /// be empty. #[named] @@ -587,7 +598,7 @@ impl Array { .or(default) .ok_or("cannot calculate sum of empty array with no default")?; for item in iter { - acc = ops::add(acc, item)?; + acc = ops::add(acc, item, &mut (&mut *engine, span))?; } Ok(acc) } @@ -686,6 +697,8 @@ impl Array { #[func] pub fn join( self, + engine: &mut Engine, + span: Span, /// A value to insert between each item of the array. #[default] separator: Option, @@ -701,13 +714,18 @@ impl Array { for (i, value) in self.into_iter().enumerate() { if i > 0 { if i + 1 == len && last.is_some() { - result = ops::join(result, last.take().unwrap())?; + result = ops::join( + result, + last.take().unwrap(), + &mut (&mut *engine, span), + )?; } else { - result = ops::join(result, separator.clone())?; + result = + ops::join(result, separator.clone(), &mut (&mut *engine, span))?; } } - result = ops::join(result, value)?; + result = ops::join(result, value, &mut (&mut *engine, span))?; } Ok(result) @@ -862,13 +880,14 @@ impl Array { self, engine: &mut Engine, context: Tracked, + span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. #[named] key: Option, ) -> SourceResult { let mut out = EcoVec::with_capacity(self.0.len()); - let mut key_of = |x: Value| match &key { + let key_of = |engine: &mut Engine, x: Value| match &key { // NOTE: We are relying on `comemo`'s memoization of function // evaluation to not excessively reevaluate the `key`. Some(f) => f.call(engine, context, [x]), @@ -879,14 +898,18 @@ impl Array { // 1. We would like to preserve the order of the elements. // 2. We cannot hash arbitrary `Value`. 'outer: for value in self { - let key = key_of(value.clone())?; + let key = key_of(&mut *engine, value.clone())?; if out.is_empty() { out.push(value); continue; } for second in out.iter() { - if ops::equal(&key, &key_of(second.clone())?) { + if ops::equal( + &key, + &key_of(&mut *engine, second.clone())?, + &mut (&mut *engine, span), + ) { continue 'outer; } } diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 6c2408446..4fa3c99ef 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -5,7 +5,7 @@ use std::cmp::Ordering; use ecow::eco_format; use typst_utils::Numeric; -use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::diag::{bail, DeprecationSink, HintedStrResult, StrResult}; use crate::foundations::{ format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, }; @@ -21,7 +21,7 @@ macro_rules! mismatch { } /// Join a value with another value. -pub fn join(lhs: Value, rhs: Value) -> StrResult { +pub fn join(lhs: Value, rhs: Value, sink: &mut dyn DeprecationSink) -> StrResult { use Value::*; Ok(match (lhs, rhs) { (a, None) => a, @@ -39,6 +39,17 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), (Args(a), Args(b)) => Args(a + b), + + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_str_join(sink); + Str(format_str!("{a}{b}")) + } + (Str(a), Type(b)) => { + warn_type_str_join(sink); + Str(format_str!("{a}{b}")) + } + (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -88,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult { } /// Compute the sum of two values. -pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { +pub fn add( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { use Value::*; Ok(match (lhs, rhs) { (a, None) => a, @@ -156,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Datetime(a), Duration(b)) => Datetime(a + b), (Duration(a), Datetime(b)) => Datetime(b + a), + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_str_add(sink); + Str(format_str!("{a}{b}")) + } + (Str(a), Type(b)) => { + warn_type_str_add(sink); + Str(format_str!("{a}{b}")) + } + (Dyn(a), Dyn(b)) => { // Alignments can be summed. if let (Some(&a), Some(&b)) = @@ -394,13 +419,21 @@ pub fn or(lhs: Value, rhs: Value) -> HintedStrResult { } /// Compute whether two values are equal. -pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult { - Ok(Value::Bool(equal(&lhs, &rhs))) +pub fn eq( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + Ok(Value::Bool(equal(&lhs, &rhs, sink))) } /// Compute whether two values are unequal. -pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult { - Ok(Value::Bool(!equal(&lhs, &rhs))) +pub fn neq( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + Ok(Value::Bool(!equal(&lhs, &rhs, sink))) } macro_rules! comparison { @@ -419,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater); comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); /// Determine whether two values are equal. -pub fn equal(lhs: &Value, rhs: &Value) -> bool { +pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool { use Value::*; match (lhs, rhs) { // Compare reflexively. @@ -463,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { rat == rel.rel && rel.abs.is_zero() } + // Type compatibility. + (Type(ty), Str(str)) | (Str(str), Type(ty)) => { + warn_type_str_equal(sink); + ty.compat_name() == str.as_str() + } + _ => false, } } @@ -534,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult { } /// Test whether one value is "in" another one. -pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult { - if let Some(b) = contains(&lhs, &rhs) { +pub fn in_( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + if let Some(b) = contains(&lhs, &rhs, sink) { Ok(Value::Bool(b)) } else { mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) @@ -543,8 +586,12 @@ pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult { } /// Test whether one value is "not in" another one. -pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult { - if let Some(b) = contains(&lhs, &rhs) { +pub fn not_in( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + if let Some(b) = contains(&lhs, &rhs, sink) { Ok(Value::Bool(!b)) } else { mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) @@ -552,13 +599,27 @@ pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult { } /// Test for containment. -pub fn contains(lhs: &Value, rhs: &Value) -> Option { +pub fn contains( + lhs: &Value, + rhs: &Value, + sink: &mut dyn DeprecationSink, +) -> Option { use Value::*; match (lhs, rhs) { (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Dyn(a), Str(b)) => a.downcast::().map(|regex| regex.is_match(b)), (Str(a), Dict(b)) => Some(b.contains(a)), - (a, Array(b)) => Some(b.contains(a.clone())), + (a, Array(b)) => Some(b.contains_impl(a, sink)), + + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_in_str(sink); + Some(b.as_str().contains(a.compat_name())) + } + (Type(a), Dict(b)) => { + warn_type_in_dict(sink); + Some(b.contains(a.compat_name())) + } _ => Option::None, } @@ -568,3 +629,46 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option { fn too_large() -> &'static str { "value is too large" } + +#[cold] +fn warn_type_str_add(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "adding strings and types is deprecated", + &["convert the type to a string with `str` first"], + ); +} + +#[cold] +fn warn_type_str_join(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "joining strings and types is deprecated", + &["convert the type to a string with `str` first"], + ); +} + +#[cold] +fn warn_type_str_equal(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "comparing strings with types is deprecated", + &[ + "compare with the literal type instead", + "this comparison will always return `false` in future Typst releases", + ], + ); +} + +#[cold] +fn warn_type_in_str(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "checking whether a type is contained in a string is deprecated", + &["this compatibility behavior only exists because `type` used to return a string"], + ); +} + +#[cold] +fn warn_type_in_dict(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "checking whether a type is contained in a dictionary is deprecated", + &["this compatibility behavior only exists because `type` used to return a string"], + ); +} diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index e1ce61b8a..ad6da323d 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -300,7 +300,7 @@ impl Binding { /// As the `sink` /// - pass `()` to ignore the message. /// - pass `(&mut engine, span)` to emit a warning into the engine. - pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { + pub fn read_checked(&self, mut sink: impl DeprecationSink) -> &Value { if let Some(message) = self.deprecation { sink.emit(message); } diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 40f7003c3..4747775ac 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -44,6 +44,16 @@ use crate::foundations::{ /// #type(int) \ /// #type(type) /// ``` +/// +/// # Compatibility +/// In Typst 0.7 and lower, the `type` function returned a string instead of a +/// type. Compatibility with the old way will remain until Typst 0.14 to give +/// package authors time to upgrade. +/// +/// - Checks like `{int == "integer"}` evaluate to `{true}` +/// - Adding/joining a type and string will yield a string +/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}` +/// if the dictionary has a string key matching the type's name #[ty(scope, cast)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Type(Static); @@ -106,6 +116,14 @@ impl Type { } } +// Type compatibility. +impl Type { + /// The type's backward-compatible name. + pub fn compat_name(&self) -> &str { + self.long_name() + } +} + #[scope] impl Type { /// Determines a value's type. diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 854c2486e..ba5b14730 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -292,7 +292,8 @@ impl Repr for Value { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { - ops::equal(self, other) + // No way to emit deprecation warnings here :( + ops::equal(self, other, &mut ()) } } diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 78a8f897e..5639f95be 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -313,6 +313,9 @@ feature flag. functions directly accepting both paths and bytes - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor of `integral.inter` +- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 now emits warnings. It will be + removed in Typst 0.14. ## Removals - Removed `style` function and `styles` argument of [`measure`], use a [context] @@ -324,9 +327,6 @@ feature flag. - Removed compatibility behavior where [`counter.display`] worked without [context] **(Breaking change)** - Removed compatibility behavior of [`locate`] **(Breaking change)** -- Removed compatibility behavior of type/str comparisons - (e.g. `{int == "integer"}`) which was temporarily introduced in Typst 0.8 - **(Breaking change)** ## Development - The `typst::compile` function is now generic and can return either a diff --git a/tests/src/world.rs b/tests/src/world.rs index 5c2678328..56ebc8683 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -251,6 +251,6 @@ fn lines( (1..=count) .map(|n| numbering.apply(engine, context, &[n])) .collect::>()? - .join(Some('\n'.into_value()), None) + .join(engine, span, Some('\n'.into_value()), None) .at(span) } diff --git a/tests/suite/foundations/type.typ b/tests/suite/foundations/type.typ index 068b858de..60f9d0ef2 100644 --- a/tests/suite/foundations/type.typ +++ b/tests/suite/foundations/type.typ @@ -2,6 +2,60 @@ #test(type(1), int) #test(type(ltr), direction) #test(type(10 / 3), float) +#test(type(10) == int, true) +#test(type(10) != int, false) + +--- type-string-compatibility-add --- +// Warning: 7-23 adding strings and types is deprecated +// Hint: 7-23 convert the type to a string with `str` first +#test("is " + type(10), "is integer") +// Warning: 7-23 adding strings and types is deprecated +// Hint: 7-23 convert the type to a string with `str` first +#test(type(10) + " is", "integer is") + +--- type-string-compatibility-join --- +// Warning: 16-24 joining strings and types is deprecated +// Hint: 16-24 convert the type to a string with `str` first +#test({ "is "; type(10) }, "is integer") +// Warning: 19-24 joining strings and types is deprecated +// Hint: 19-24 convert the type to a string with `str` first +#test({ type(10); " is" }, "integer is") + +--- type-string-compatibility-equal --- +// Warning: 7-28 comparing strings with types is deprecated +// Hint: 7-28 compare with the literal type instead +// Hint: 7-28 this comparison will always return `false` in future Typst releases +#test(type(10) == "integer", true) +// Warning: 7-26 comparing strings with types is deprecated +// Hint: 7-26 compare with the literal type instead +// Hint: 7-26 this comparison will always return `false` in future Typst releases +#test(type(10) != "float", true) + +--- type-string-compatibility-in-array --- +// Warning: 7-35 comparing strings with types is deprecated +// Hint: 7-35 compare with the literal type instead +// Hint: 7-35 this comparison will always return `false` in future Typst releases +#test(int in ("integer", "string"), true) +// Warning: 7-37 comparing strings with types is deprecated +// Hint: 7-37 compare with the literal type instead +// Hint: 7-37 this comparison will always return `false` in future Typst releases +#test(float in ("integer", "string"), false) + +--- type-string-compatibility-in-str --- +// Warning: 7-35 checking whether a type is contained in a string is deprecated +// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string +#test(int in "integers or strings", true) +// Warning: 7-35 checking whether a type is contained in a string is deprecated +// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string +#test(str in "integers or strings", true) +// Warning: 7-37 checking whether a type is contained in a string is deprecated +// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string +#test(float in "integers or strings", false) + +--- type-string-compatibility-in-dict --- +// Warning: 7-37 checking whether a type is contained in a dictionary is deprecated +// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string +#test(int in (integer: 1, string: 2), true) --- issue-3110-type-constructor --- // Let the error message report the type name.