Skip to content

Commit 7a1b476

Browse files
committed
Disallow inferred into types as implicit conversion target types
1 parent f841cf6 commit 7a1b476

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed

compiler/src/dotty/tools/dotc/core/Types.scala

+19
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,25 @@ object Types extends TypeUtils {
452452
case AppliedType(tycon: TypeRef, arg :: Nil) => defn.isInto(tycon.symbol)
453453
case _ => false
454454

455+
/** Is this type a legal target type for an implicit conversion, so that
456+
* no `implicitConversions` language import is necessary?
457+
*/
458+
def isConversionTargetType(using Context): Boolean =
459+
dealias(KeepTypeVars | KeepOpaques).match
460+
case _: AppliedType =>
461+
isInto
462+
case tp: AndOrType =>
463+
tp.tp1.isConversionTargetType && tp.tp2.isConversionTargetType
464+
case tp: TypeVar =>
465+
false
466+
case tp: MatchType =>
467+
val tp1 = tp.reduced
468+
(tp1 ne tp) && tp1.isConversionTargetType
469+
case tp: RefinedType =>
470+
tp.parent.isConversionTargetType
471+
case _ =>
472+
false
473+
455474
/** Is this the type of a method that has a repeated parameter type as
456475
* last parameter type?
457476
*/

compiler/src/dotty/tools/dotc/typer/Checking.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1146,7 +1146,7 @@ trait Checking {
11461146
if sym.name == nme.apply
11471147
&& sym.owner.derivesFrom(defn.ConversionClass)
11481148
&& !sym.info.isErroneous
1149-
&& !expected.isInto
1149+
&& !expected.isConversionTargetType
11501150
then
11511151
def conv = methPart(tree) match
11521152
case Select(qual, _) => qual.symbol.orElse(sym.owner)

docs/_docs/reference/experimental/into.md

+49-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import scala.language.implicitConversions
1818
in any code that uses them as implicit conversions (code that calls conversions explicitly is not affected). If the import is missing, a feature warning is currently issued, and this will become an error in future versions of Scala 3. The motivation for this restriction is two-fold:
1919

2020
- Code with hidden implicit conversions is hard to understand and might have correctness or performance issues that go undetected.
21-
- If we require explicit user-opt in for implicit conversions, we can significantly improve type inference by propagating expected type information more widely in those parts of the program where there is no opt-in.
21+
- If we require explicit user opt-in for implicit conversions, we can significantly improve type inference by propagating expected type information more widely in those parts of the program where there is no opt-in.
2222

2323
There is one broad use case, however, where implicit conversions are very hard to replace. This is the case where an implicit conversion is used to adapt a method argument to its formal parameter type. An example from the standard library:
2424
```scala
@@ -132,5 +132,52 @@ type Modifier = into[ModifierClass]
132132
```
133133
The into-erasure for function parameters also works in aliased types. So a function defining parameters of `Modifier` type can use them internally as if they were from the underlying `ModifierClass`.
134134

135-
## Alternatives
135+
## Details: Conversion target types
136136

137+
The description so far said that conversions are allowed if the target type
138+
139+
A conversion target type is one of the following:
140+
141+
- a type of the form `into[T]`,
142+
- a reference `p.C` to a class or trait `C` that is declared with an `into` modifier,
143+
which can also be followed by type arguments,
144+
- a type alias of a conversion target type,
145+
- a match type that reduces to a conversion target type,
146+
- an annotated type `T @ann` where `T` is a conversion target type,
147+
- a refined type `T {...}` where `T` is a conversion target type,
148+
- a union `T | U` if two conversion target types `T` and `U`,
149+
- an intersection `T & U` if two conversion target types `T` and `U`,
150+
- an instance of a type parameter that is explicitly instantiated to a conversion target type.
151+
152+
153+
Inferred type parameters do not count as conversion target types. For instance, consider:
154+
155+
```scala
156+
trait Token
157+
class Keyword(str: String)
158+
given Conversion[String, Keyword] = KeyWord(_)
159+
160+
List[into[Keyword]]("if", "then", "else")
161+
```
162+
This type-checks since the target type of the list elements is the type parameter of the `List.apply` method which is explicitly instantiated to `into[Keyword]`. On the other hand, if we continue the example as follows we get an error:
163+
```scala
164+
val ifKW: into[Keyword] = "if"
165+
List(ifKW, "then", "else") // error
166+
```
167+
Here, the type variable of `List.apply` is not explicitly instantiated, but is inferred to have type `into[Keyword]`. This is not enough to allow
168+
implicit conversions on the second and third arguments.
169+
170+
Subclasses of `into` classes or traits do not count as conversion target types. For instance, consider:
171+
172+
```scala
173+
into trait T
174+
class C(x: Int) extends T
175+
given Conversion[Int, C] = C(_)
176+
177+
def f(x: T) = ()
178+
def g(x: C) = ()
179+
f("abc") // ok
180+
g("abc") // error
181+
```
182+
The call `f("abc")` type-checks since `f`'s parameter type `T` is `into`.
183+
But the call `g("abc")` does not type-check since `g`'s parameter type `C` is not `into`. It does not matter that `C` extends a trait `T` that is `into`.

tests/warn/into-inferred.check

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Feature Warning: tests/warn/into-inferred.scala:23:43 ---------------------------------------------------------------
2+
23 | val ys: List[into[Keyword]] = List(ifKW, "then", "else") // warn // warn
3+
| ^^^^^^
4+
| Use of implicit conversion given instance given_Conversion_String_Keyword in object Test should be enabled
5+
| by adding the import clause 'import scala.language.implicitConversions'
6+
| or by setting the compiler option -language:implicitConversions.
7+
| See the Scala docs for value scala.language.implicitConversions for a discussion
8+
| why the feature should be explicitly enabled.
9+
-- Feature Warning: tests/warn/into-inferred.scala:23:51 ---------------------------------------------------------------
10+
23 | val ys: List[into[Keyword]] = List(ifKW, "then", "else") // warn // warn
11+
| ^^^^^^
12+
| Use of implicit conversion given instance given_Conversion_String_Keyword in object Test should be enabled
13+
| by adding the import clause 'import scala.language.implicitConversions'
14+
| or by setting the compiler option -language:implicitConversions.
15+
| See the Scala docs for value scala.language.implicitConversions for a discussion
16+
| why the feature should be explicitly enabled.

tests/warn/into-inferred.scala

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//> using options -feature
2+
3+
import language.experimental.into
4+
import Conversion.{into, underlying}
5+
6+
trait Token
7+
class Keyword(str: String)
8+
case class Phrase(words: into[Keyword]*)
9+
10+
object Test:
11+
given Conversion[String, Keyword] = Keyword(_)
12+
13+
val xs = List[into[Keyword]]("if", "then", "else") // ok
14+
val _: List[Keyword] = xs.map(_.underlying)
15+
16+
val p = Phrase("if", "then", "else") // ok
17+
val ws = p.words
18+
val _: Seq[Keyword] = ws
19+
20+
val p2 = Phrase(xs*) // ok
21+
22+
val ifKW: into[Keyword] = "if"
23+
val ys: List[into[Keyword]] = List(ifKW, "then", "else") // warn // warn

0 commit comments

Comments
 (0)