Skip to content
Draft
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
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,8 @@ object SymDenotations {
def opaqueAlias(using Context): Type = {
def recur(tp: Type): Type = tp.stripAnnots match {
case RefinedType(parent, rname, TypeAlias(alias)) =>
if rname == name then alias.stripLazyRef else recur(parent)
if rname == name then alias.stripLazyRef
else recur(parent)
case _ =>
NoType
}
Expand Down
38 changes: 36 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2748,7 +2748,10 @@ object Types extends TypeUtils {
if reduced.exists then reduced
else prefix.stripTypeVar match
case pre: (AppliedType | TypeRef)
if prefix.dealias.typeSymbol.isClass && this.symbol.isAliasType => dealias
// Don't dealias opaque types - they should maintain their opacity
// when accessed from outside their defining scope. See issue #24051.
if prefix.dealias.typeSymbol.isClass && this.symbol.isAliasType && !this.symbol.isOpaqueAlias =>
dealias
case _ => this

/** Guard against cycles that can arise if given `op`
Expand Down Expand Up @@ -2840,7 +2843,9 @@ object Types extends TypeUtils {
val reduced =
if isType && currentValidSymbol.isAllOf(ClassTypeParam) then argForParam(prefix)
else prefix.lookupRefined(name)
if reduced.exists then return reduced
// Don't expose opaque aliases through lookupRefined - they should
// maintain their opacity when accessed from outside. See issue #24051.
if reduced.exists && !currentValidSymbol.isOpaqueAlias then return reduced
if Config.splitProjections && isType then
prefix match
case prefix: AndType =>
Expand Down Expand Up @@ -3036,6 +3041,35 @@ object Types extends TypeUtils {

override def underlying(using Context): Type = info

/** For opaque types accessed via ThisType prefix from outside their defining scope,
* we should not expose the underlying alias through superType. This prevents
* types like OpaqueType[String] from being treated as Unit outside the scope.
* See issue #24051.
*/
override def superType(using Context): Type = info match {
case TypeAlias(aliased) =>
// Check if this is an opaque type with a ThisType prefix that grants
// transparent access but we're outside the opaque scope
if symbol.isOpaqueAlias then
prefix match {
case pre: Types.ThisType if pre.cls.seesOpaques =>
// Only return the alias if we're inside the opaque scope
val opaqueOwner = symbol.maybeOwner
if opaqueOwner.exists && ctx.owner.ownersIterator.contains(opaqueOwner) then
aliased
else
// Outside the scope - return the upper bound (Any for unbounded opaques)
symbol.info match {
case TypeBounds(_, hi) => hi
case _ => defn.AnyType
}
case _ => aliased
}
else aliased
case TypeBounds(_, hi) => hi
case st => st
}

override def translucentSuperType(using Context) = info match {
case TypeAlias(aliased) => aliased
case TypeBounds(_, hi) =>
Expand Down
14 changes: 12 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,10 @@ class Namer { typer: Typer =>
val forwarder =
if mbr.isType then
val forwarderName = checkNoConflict(alias.toTypeName, span)
var target = pathType.select(sym)
// Use TypeRef directly instead of pathType.select(sym) to avoid
// reduceProjection which would expose the underlying type of opaque aliases
// through the self-type refinement. See issue #24051.
var target: Type = TypeRef(pathType, sym)
if target.typeParams.nonEmpty then
target = target.etaExpand
newSymbol(
Expand Down Expand Up @@ -2231,7 +2234,14 @@ class Namer { typer: Typer =>

// Replace aliases to Unit by Unit itself. If we leave the alias in
// it would be erased to BoxedUnit.
def dealiasIfUnit(tp: Type) = if (tp.isRef(defn.UnitClass)) defn.UnitType else tp
// However, we must not dealias opaque types to Unit - they should preserve
// their opacity. See issue #24051.
def dealiasIfUnit(tp: Type) =
def involvesOpaqueAlias(tp: Type): Boolean = tp match
case tp: AppliedType => involvesOpaqueAlias(tp.tycon)
case tp: TypeRef => tp.symbol.isOpaqueAlias
case _ => false
if !involvesOpaqueAlias(tp) && tp.isRef(defn.UnitClass) then defn.UnitType else tp

def cookedRhsType = dealiasIfUnit(rhsType)
def lhsType = fullyDefinedType(cookedRhsType, "right-hand side", mdef.srcPos)
Expand Down
17 changes: 17 additions & 0 deletions tests/neg/i24051.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package example {
package types {
opaque type OpaqueType[A] = Unit
object OpaqueType {
def apply[A]: OpaqueType[A] = ()
}
}

object exports {
export example.types.OpaqueType
}

import exports.*

def test[A](a: A)(using ev: A =:= Unit) = a
val proof = test(OpaqueType[String]) // error - Should fail: OpaqueType[String] should not equal Unit
}
Loading