From e47f798971f11cd8f3c59e8bdcf2371044455975 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 16 Feb 2023 10:41:19 +0100 Subject: [PATCH] Warn on use of inferred quote type variable bounds This kind of inference is not reliable. We can only consider the bounds from type constructor where the type variable is defined but any other constraints are ignored. Therefore it is not possible to properly infer the type bounds of the type variable. The solution is simple, write the bounds explicitly and just check that those bounds conform to the use site of the type variable. --- .../tools/dotc/typer/QuotesAndSplices.scala | 21 ++++++-- .../quote-type-var-with-bounds.check | 49 +++++++++++++++++++ .../quote-type-var-with-bounds.scala | 26 ++++++++++ .../quote-type-variable-no-inference-2.check | 15 ++++-- .../quote-type-variable-no-inference-2.scala | 2 + 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.check create mode 100644 tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 04509124bd7c..24c39e36c207 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -164,18 +164,28 @@ trait QuotesAndSplices { case pt: TypeBounds => pt case _ => TypeBounds.empty - def warnOnInferredBounds(typeSym: Symbol) = + def openQuote = if ctx.mode.is(Mode.QuotedExprPattern) then "'{" else "'[" + def closeQuote = if ctx.mode.is(Mode.QuotedExprPattern) then "}" else "]" + + def warnOnUnusedBounds(typeSym: Symbol) = if !(typeSymInfo =:= TypeBounds.empty) && !(typeSym.info <:< typeSymInfo) then - val (openQuote, closeQuote) = if ctx.mode.is(Mode.QuotedExprPattern) then ("'{", "}") else ("'[", "]") report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly:\n $openQuote $typeSym${typeSym.info & typeSymInfo}; ... $closeQuote", tree.srcPos) + def warnOnInferredBounds(typeSym: Symbol) = + if !(typeSymInfo =:= TypeBounds.empty) then + if ctx.mode.is(Mode.QuotedExprPattern) then // TODO drop this guard once SIP-53 is non-experimental + // TODO state in the message when this will no longer work. + // - We could stabilize SIP-53 and start deprecation warnings for type quoted patterns in 3.4.0. + // - Then we could drop type variable inference 3.5.0. + report.deprecationWarning(em"Type variable `$tree` has partially inferred bounds$typeSymInfo.\n\nConsider defining bounds explicitly:\n $openQuote $typeSym$typeSymInfo; ... $closeQuote", tree.srcPos) + getQuotedPatternTypeVariable(tree.name.asTypeName) match case Some(typeSym) => checkExperimentalFeature( "support for multiple references to the same type (without backticks) in quoted type patterns (SIP-53)", tree.srcPos, "\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html") - warnOnInferredBounds(typeSym) + warnOnUnusedBounds(typeSym) ref(typeSym) case None => val spliceContext = quotePatternSpliceContext @@ -183,6 +193,7 @@ trait QuotesAndSplices { val nameOfSyntheticGiven = PatMatGivenVarName.fresh(tree.name.toTermName) val expr = untpd.cpy.Ident(tree)(nameOfSyntheticGiven) val typeSym = newSymbol(spliceContext.owner, name, EmptyFlags, typeSymInfo, NoSymbol, tree.span) + warnOnInferredBounds(typeSym) typeSym.addAnnotation(Annotation(New(ref(defn.QuotedRuntimePatterns_patternTypeAnnot.typeRef)).withSpan(tree.span))) addQuotedPatternTypeVariable(typeSym) val pat = typedPattern(expr, defn.QuotedTypeClass.typeRef.appliedTo(typeSym.typeRef))(using spliceContext) @@ -536,8 +547,12 @@ trait QuotesAndSplices { object QuotesAndSplices { import tpd._ + private enum QuotePattenKind: + case Expr, Type + /** Key for mapping from quoted pattern type variable names into their symbol */ private val TypeVariableKey = new Property.Key[collection.mutable.Map[TypeName, Symbol]] + private val QuotePattenKindKey = new Property.Key[QuotePattenKind] /** Get the symbol for the quoted pattern type variable if it exists */ def getQuotedPatternTypeVariable(name: TypeName)(using Context): Option[Symbol] = diff --git a/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.check b/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.check new file mode 100644 index 000000000000..ef1ade8da645 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.check @@ -0,0 +1,49 @@ +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:13:18 ---------------------------------- +13 | case '{ $x: C[t] } => // error + | ^ + | Type variable `t` has partially inferred bounds <: Int. + | + | Consider defining bounds explicitly: + | '{ type t <: Int; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:14:18 ---------------------------------- +14 | case '{ $x: D[t] } => // error + | ^ + | Type variable `t` has partially inferred bounds >: Null <: String. + | + | Consider defining bounds explicitly: + | '{ type t >: Null <: String; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:17:18 ---------------------------------- +17 | case '{ $x: E[t, t] } => // error + | ^ + | Type variable `t` has partially inferred bounds <: Int. + | + | Consider defining bounds explicitly: + | '{ type t <: Int; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:19:18 ---------------------------------- +19 | case '{ $x: F[t, t] } => // error // error + | ^ + | Type variable `t` has partially inferred bounds <: Int. + | + | Consider defining bounds explicitly: + | '{ type t <: Int; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:19:21 ---------------------------------- +19 | case '{ $x: F[t, t] } => // error // error + | ^ + | Ignored bound <: String + | + | Consider defining bounds explicitly: + | '{ type t <: Int & String; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:20:36 ---------------------------------- +20 | case '{ type t <: Int; $x: F[t, t] } => // error + | ^ + | Ignored bound <: String + | + | Consider defining bounds explicitly: + | '{ type t <: Int & String; ... } +-- Error: tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala:26:17 ---------------------------------- +26 | case '[ F[t, t] ] => // error // will have a second error with SIP-53 + | ^ + | Ignored bound <: String + | + | Consider defining bounds explicitly: + | '[ type t <: Int & String; ... ] diff --git a/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala b/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala new file mode 100644 index 000000000000..a88cc53ee3bf --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/quote-type-var-with-bounds.scala @@ -0,0 +1,26 @@ +// scalac: -deprecation + +import scala.quoted.* + +class C[T <: Int] +class D[T >: Null <: String] +class E[T <: Int, U <: Int] +class F[T <: Int, U <: String] + +def test[T: Type](e: Expr[Any])(using Quotes) = + e match + case '{ $x: t } => + case '{ $x: C[t] } => // error + case '{ $x: D[t] } => // error + case '{ type t <: Int; $x: C[t] } => + + case '{ $x: E[t, t] } => // error + + case '{ $x: F[t, t] } => // error // error + case '{ type t <: Int; $x: F[t, t] } => // error + + Type.of[T] match + case '[ C[t] ] => // will have an error with SIP-53 + case '[ D[t] ] => // will have an error with SIP-53 + case '[ E[t, t] ] => // will have an error with SIP-53 + case '[ F[t, t] ] => // error // will have a second error with SIP-53 diff --git a/tests/neg-macros/quote-type-variable-no-inference-2.check b/tests/neg-macros/quote-type-variable-no-inference-2.check index 2099e12a1a6e..2328d5f8de7d 100644 --- a/tests/neg-macros/quote-type-variable-no-inference-2.check +++ b/tests/neg-macros/quote-type-variable-no-inference-2.check @@ -1,12 +1,19 @@ --- Warning: tests/neg-macros/quote-type-variable-no-inference-2.scala:5:22 --------------------------------------------- -5 | case '{ $_ : F[t, t]; () } => // warn // error +-- Deprecation Warning: tests/neg-macros/quote-type-variable-no-inference-2.scala:7:19 --------------------------------- +7 | case '{ $_ : F[t, t]; () } => // warn // error + | ^ + | Type variable `t` has partially inferred bounds <: Int. + | + | Consider defining bounds explicitly: + | '{ type t <: Int; ... } +-- Warning: tests/neg-macros/quote-type-variable-no-inference-2.scala:7:22 --------------------------------------------- +7 | case '{ $_ : F[t, t]; () } => // warn // error | ^ | Ignored bound <: Double | | Consider defining bounds explicitly: | '{ type t <: Int & Double; ... } --- [E057] Type Mismatch Error: tests/neg-macros/quote-type-variable-no-inference-2.scala:5:12 -------------------------- -5 | case '{ $_ : F[t, t]; () } => // warn // error +-- [E057] Type Mismatch Error: tests/neg-macros/quote-type-variable-no-inference-2.scala:7:12 -------------------------- +7 | case '{ $_ : F[t, t]; () } => // warn // error | ^ | Type argument t does not conform to upper bound Double in inferred type F[t, t] | diff --git a/tests/neg-macros/quote-type-variable-no-inference-2.scala b/tests/neg-macros/quote-type-variable-no-inference-2.scala index 1cb0d3dab7b3..6f7b428cf8a6 100644 --- a/tests/neg-macros/quote-type-variable-no-inference-2.scala +++ b/tests/neg-macros/quote-type-variable-no-inference-2.scala @@ -1,3 +1,5 @@ +// scalac: -deprecation + import scala.quoted.* def test2(x: Expr[Any])(using Quotes) =