From 1eebbe60235ae428d9439b38a815ea2703c5b421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 3 Apr 2023 16:41:11 +0200 Subject: [PATCH 1/3] Do not drop package objects when computing signature names. It seems the comment in dotc is lying: it does not drop package objects. --- .../shared/src/main/scala/tastyquery/Symbols.scala | 6 +++--- .../src/test/scala/tastyquery/TypeSuite.scala | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala index 235b24ad..42571830 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala @@ -729,6 +729,8 @@ object Symbols { * > Drops package objects. Represents each term in the owner chain by a simple `_$`. * * The code actually represents each *non-class* in the owner chain by a simple `_$`. + * Moreover, there does not seem to be any code that actually drops package objects, + * and evidence suggests that it does not. */ private[tastyquery] final def signatureName: FullyQualifiedName = def computeErasedName(owner: Symbol, name: TypeName): FullyQualifiedName = owner match @@ -736,9 +738,7 @@ object Symbols { owner.fullName.select(name) case owner: ClassSymbol => - // Drop package objects - if owner.name.isPackageObjectClassName && owner.owner.isPackage then owner.owner.fullName.select(name) - else owner.signatureName.mapLast(_.toTermName).select(name) + owner.signatureName.mapLast(_.toTermName).select(name) case owner: TermOrTypeSymbol => // Replace non-class non-package owners by simple `_$` diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala index 6f8574e0..26b618e8 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala @@ -2022,6 +2022,19 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { assert(clue(app.tpe).isRef(anonClassSym)) } + testWithContext("toplevel-module-class-with-opaque-type-alias-companion-signature-name") { + val TopLevelOpaqueTypeAliasModule = + ctx.findStaticTerm("crosspackagetasty.TopLevelOpaqueTypeAlias$package.TopLevelOpaqueTypeAlias") + val TopLevelOpaqueTypeAliasModuleClass = + ctx.findStaticModuleClass("crosspackagetasty.TopLevelOpaqueTypeAlias$package.TopLevelOpaqueTypeAlias") + + val moduleValRhs = TopLevelOpaqueTypeAliasModule.tree.get.asInstanceOf[ValDef].rhs.get + val Apply(Select(New(_), ctorSignedName: SignedName), Nil) = moduleValRhs: @unchecked + + assert(clue(TopLevelOpaqueTypeAliasModuleClass.signatureName) == clue(ctorSignedName.sig.resSig)) + assert(clue(moduleValRhs.tpe).isRef(TopLevelOpaqueTypeAliasModuleClass)) + } + testWithContext("annotations") { val AnnotationsClass = ctx.findTopLevelClass("simple_trees.Annotations") val inlineClass = ctx.findTopLevelClass("scala.inline") From 7961962d5e263ca0b6429363868d899abf5847b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 3 Apr 2023 17:41:59 +0200 Subject: [PATCH 2/3] Handle AndType in ClassTypeParamSymbol.argForParam. --- .../src/main/scala/tastyquery/Symbols.scala | 20 +++++++++ .../src/test/scala/tastyquery/TypeSuite.scala | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala index 42571830..dbb56df0 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala @@ -563,6 +563,25 @@ object Symbols { idx += 1 } None + + case Some(base: AndType) => + (argForParam(base.first), argForParam(base.second)) match + case (None, tp2) => + tp2 + case (tp1, None) => + tp1 + case (Some(tp1), Some(tp2)) => + val variance = this.paramVariance.sign + val result: Type = + if tp1.isInstanceOf[WildcardTypeBounds] || tp2.isInstanceOf[WildcardTypeBounds] || variance == 0 then + // TODO? Compute based on bounds, instead of returning the original reference + TypeRef(pre, this) + else if variance > 0 then tp1 & tp2 + else tp1 | tp2 + end result + Some(result) + end match + /*case base: AndOrType => var tp1 = argForParam(base.tp1) var tp2 = argForParam(base.tp2) @@ -573,6 +592,7 @@ object Symbols { tp2 = tp2.bounds } if (base.isAnd == variance >= 0) tp1 & tp2 else tp1 | tp2*/ + case _ => /*if (pre.termSymbol.isPackage) argForParam(pre.select(nme.PACKAGE)) else*/ diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala index 26b618e8..de32d6ea 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala @@ -303,6 +303,48 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { assert(wildcardPatternCount == 2, clue(wildcardPatternCount)) } + testWithContext("match-bind-with-type-capture") { + val ListClass = ctx.findTopLevelClass("scala.collection.immutable.List") + val MatchTypeClass = ctx.findTopLevelClass("simple_trees.MatchType") + + val castMatchResultWithBindSym = MatchTypeClass.findNonOverloadedDecl(termName("castMatchResultWithBind")) + val castMatchResultWithBindDef = castMatchResultWithBindSym.tree.get.asInstanceOf[DefDef] + + /* type param [X] + * param x: X + * + * x match + * case is: List[t] => is.head + * + * `is` gets typed as `X & List[t]`. + * `is.head` must resolve to having type `t`. + */ + + val List(Right(List(typeXDef)), _) = castMatchResultWithBindDef.paramLists: @unchecked + val typeXSym = typeXDef.symbol + + val tTypeCaptureSym = findTree(castMatchResultWithBindDef) { case TypeTreeBind(TypeName(SimpleName("t")), _, sym) => + sym + } + + val bind = findTree(castMatchResultWithBindDef) { case bind @ Bind(SimpleName("is"), _, _) => + bind + } + val isSym = bind.symbol + + assert( + clue(isSym.declaredType) + .isIntersectionOf(_.isRef(typeXSym), _.isApplied(_.isRef(ListClass), List(_.isRef(tTypeCaptureSym)))) + ) + + val (typed, expr, qualifier) = findTree(castMatchResultWithBindDef) { + case typed @ Typed(expr @ Select(qualifier, SimpleName("head")), _) => (typed, expr, qualifier) + } + assert(clue(qualifier.tpe).isRef(isSym)) + assert(clue(clue(expr.tpe).widen).isRef(tTypeCaptureSym)) + assert(typed.tpe.isRef(tTypeCaptureSym)) + } + testWithContext("return") { val ReturnPathClass = ctx.findTopLevelClass("simple_trees.Return") From 363a88302d4958f9aed50af46596dab7a43c5531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 4 Apr 2023 14:45:57 +0200 Subject: [PATCH 3/3] Handle subtyping and member selection of TermRefinement with MethodicType. When a `TermRefinement` has a `MethodType` or a `PolyType`, we cannot use `Type.resolveMember(refinedName)`. `resolveMember` needs the correctly *signed* name of such terms. We cannot compute the fully correct signed name because the result type of the target member might be a subtype (with a different erasure) of the requested `refinedType`. In order to handle this situation, we add a new method `Type.resolveMatchingMember`, which matches the simple name and the parameters of the signature, but not its result. We compensate by adding a customizable predicate on the type. The implementation of `resolveMatchingMember` is unfortunately a bit duplicated from `resolveMember`. We can get into `resolveMatchingMember` from two places: in subtyping when the right-hand-side is a `TermRefinement`, or from `TermRefinement.resolveMember` itself. Because of subtyping, we must be able to resolve term members that do not have any accompanying `TermSymbol`. So `ResolvedMemberResult.TermMember` can now be constructed with an empty list of `TermSymbol`s. However, if such a result reaches a `TermRef` (possibily via `lookupMember`), we consider it as `NotFound` instead. --- build.sbt | 5 + .../main/scala/tastyquery/Signatures.scala | 3 + .../src/main/scala/tastyquery/Subtyping.scala | 63 +++++-- .../src/main/scala/tastyquery/Symbols.scala | 27 +++ .../src/main/scala/tastyquery/TypeOps.scala | 13 +- .../src/main/scala/tastyquery/Types.scala | 170 ++++++++++++++++-- .../scala/tastyquery/SubtypingSuite.scala | 77 ++++++++ .../src/test/scala/tastyquery/TypeSuite.scala | 20 +++ .../main/scala/subtyping/paths/Paths.scala | 6 + 9 files changed, 346 insertions(+), 38 deletions(-) diff --git a/build.sbt b/build.sbt index f9ce695b..476f3747 100644 --- a/build.sbt +++ b/build.sbt @@ -117,6 +117,11 @@ lazy val tastyQuery = ProblemFilters.exclude[DirectMissingMethodProblem]("tastyquery.Symbols#ClassSymbol.createRefinedClassSymbol"), ProblemFilters.exclude[FinalClassProblem]("tastyquery.TypeOps$AsSeenFromMap"), ProblemFilters.exclude[IncompatibleMethTypeProblem]("tastyquery.TypeOps#AsSeenFromMap.this"), + ProblemFilters.exclude[DirectMissingMethodProblem]("tastyquery.*.makeResolveMemberResult"), + + // private[tastyquery] and implemented in the entire "open boundary" of Type, so this is fine + ProblemFilters.exclude[DirectMissingMethodProblem]("tastyquery.Types#Type.resolveMatchingMember"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("tastyquery.Types#Type.resolveMatchingMember"), ) }, ) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala b/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala index 3dc94e9d..e9b228d0 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala @@ -21,6 +21,9 @@ object Signatures: case ParamSig.Term(typ) => typ.toString case ParamSig.TypeLen(len) => len.toString }.mkString("(", ",", ")") + ":" + resSig.toString + + private[tastyquery] def paramsCorrespond(that: Signature): Boolean = + this.paramsSig == that.paramsSig end Signature object Signature { diff --git a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala index f66efffe..20045e67 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala @@ -178,6 +178,22 @@ private[tastyquery] object Subtyping: val etaExpandSuccess = tparams1.nonEmpty && isSubtype(etaExpand(tp1, tparams1), tp2) etaExpandSuccess || level4(tp1, tp2) + case tp2: MethodType => + tp1 match + case tp1: MethodType => + TypeOps.matchingMethodParams(tp1, tp2) + && isSubtype(tp1.resultType, Substituters.substBinders(tp2.resultType, tp2, tp1)) + case _ => + false + + case tp2: PolyType => + tp1 match + case tp1: PolyType => + TypeOps.matchingPolyParams(tp1, tp2) + && isSubtype(tp1.resultType, Substituters.substBinders(tp2.resultType, tp2, tp1)) + case _ => + false + case tp2: RefinedType => (isSubtype(tp1, tp2.parent) && hasMatchingRefinedMember(tp1, tp2)) || level4(tp1, tp2) @@ -297,24 +313,35 @@ private[tastyquery] object Subtyping: TypeLambda.fromParamInfos(tparams)(tl => tp.appliedTo(tl.paramRefs)) private def hasMatchingRefinedMember(tp1: Type, tp2: RefinedType)(using Context): Boolean = - tp1.resolveMember(tp2.refinedName, tp1) match - case ResolveMemberResult.NotFound => - false - - case ResolveMemberResult.TermMember(symbols, tpe) => - tp2 match - case tp2: TermRefinement => isSubtype(tpe, tp2.refinedType) - case _: TypeRefinement => throw AssertionError(s"found term member for $tp2 in $tp1") - - case ResolveMemberResult.ClassMember(cls) => - tp2 match - case tp2: TypeRefinement => tp2.refinedBounds.contains(tp1.select(cls)) - case _: TermRefinement => throw AssertionError(s"found type member for $tp2 in $tp1") - - case ResolveMemberResult.TypeMember(symbols, bounds) => - tp2 match - case tp2: TypeRefinement => tp2.refinedBounds.contains(bounds) - case _: TermRefinement => throw AssertionError(s"found type member for $tp2 in $tp1") + tp2 match + case tp2: TypeRefinement => + tp1.resolveMember(tp2.refinedName, tp1) match + case ResolveMemberResult.NotFound => + false + case ResolveMemberResult.ClassMember(cls) => + tp2.refinedBounds.contains(tp1.select(cls)) + case ResolveMemberResult.TypeMember(symbols, bounds) => + tp2.refinedBounds.contains(bounds) + case ResolveMemberResult.TermMember(symbols, tpe) => + throw AssertionError(s"found term member for $tp2 in $tp1") + + case tp2: TermRefinement => + if !tp2.isMethodic then + tp1.resolveMember(tp2.refinedName, tp1) match + case ResolveMemberResult.NotFound => + false + case ResolveMemberResult.TermMember(_, tpe) => + tpe.isSubtype(tp2.refinedType) + case _: ResolveMemberResult.ClassMember | _: ResolveMemberResult.TypeMember => + throw AssertionError(s"found type member for $tp2 in $tp1") + else + tp1.resolveMatchingMember(tp2.signedName, tp1, _.isSubtype(tp2.refinedType)) match + case ResolveMemberResult.NotFound => + false + case _: ResolveMemberResult.TermMember => + true + case _: ResolveMemberResult.ClassMember | _: ResolveMemberResult.TypeMember => + throw AssertionError(s"found type member for $tp2 in $tp1") end hasMatchingRefinedMember private def level4(tp1: Type, tp2: Type)(using Context): Boolean = tp1 match diff --git a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala index dbb56df0..5d263160 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala @@ -1150,6 +1150,33 @@ object Symbols { ResolveMemberResult.NotFound end resolveMember + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + def lookup(lin: List[ClassSymbol]): ResolveMemberResult = lin match + case parentCls :: linRest => + var overloadsRest = parentCls.getAllOverloadedDecls(name.underlying) + while overloadsRest.nonEmpty do + val decl = overloadsRest.head + val matches = + !decl.isAnyOf(Private | Protected | Local) + && decl.needsSignature + && name.sig.paramsCorrespond(decl.signature) + if matches then + val tpe = decl.declaredTypeAsSeenFrom(pre) + if typePredicate(tpe) then return ResolveMemberResult.TermMember(decl :: Nil, tpe) + end if + overloadsRest = overloadsRest.tail + end while + lookup(linRest) + + case Nil => + ResolveMemberResult.NotFound + end lookup + + lookup(linearization) + end resolveMatchingMember + private var myTypeRef: TypeRef | Null = null private[tastyquery] final def typeRef(using Context): TypeRef = diff --git a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala index 11afdd6e..b04e1fa1 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala @@ -100,7 +100,7 @@ private[tastyquery] object TypeOps: case tp1: PolyType => tp2.widen match case tp2: PolyType => - tp1.paramNames.lengthCompare(tp2.paramNames) == 0 + matchingPolyParams(tp1, tp2) && matchesType(tp1.resultType, Substituters.substBinders(tp2.resultType, tp2, tp1)) case _ => false @@ -115,11 +115,20 @@ private[tastyquery] object TypeOps: true end matchesType + /** Do the parameter types of `tp1` and `tp2` match in a way that allows `tp1` to override `tp2`? + * + * This is the case if they're pairwise `>:>`. + */ + def matchingPolyParams(tp1: PolyType, tp2: PolyType)(using Context): Boolean = + // TODO Actually test `>:>`. + tp1.paramNames.lengthCompare(tp2.paramNames) == 0 + end matchingPolyParams + /** Do the parameter types of `tp1` and `tp2` match in a way that allows `tp1` to override `tp2`? * * This is the case if they're pairwise `=:=`. */ - private def matchingMethodParams(tp1: MethodType, tp2: MethodType)(using Context): Boolean = + def matchingMethodParams(tp1: MethodType, tp2: MethodType)(using Context): Boolean = def loop(formals1: List[Type], formals2: List[Type]): Boolean = formals1 match case formal1 :: rest1 => formals2 match diff --git a/tasty-query/shared/src/main/scala/tastyquery/Types.scala b/tasty-query/shared/src/main/scala/tastyquery/Types.scala index 35100a0f..e8ce2b27 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Types.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Types.scala @@ -207,7 +207,8 @@ object Types { case ResolveMemberResult.NotFound => None case resolved: ResolveMemberResult.TermMember => - Some(TermRef.fromResolved(this, resolved)) + if resolved.symbols.isEmpty then None + else Some(TermRef.fromResolved(this, resolved)) case resolved: ResolveMemberResult.ClassMember => Some(TypeRef.fromResolved(this, resolved)) case resolved: ResolveMemberResult.TypeMember => @@ -367,6 +368,17 @@ object Types { /** Find the member of this type with the given `name` when prefix `pre`. */ private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult + /** Finds the term member with the given signed name, disregarding the result type, + * and whose type satisfies the given predicate. + * + * This method must follow the "paths" followed by `resolveMember`. + * It is used when dealing with methodic refinements in + * `Subtyping.hasMatchingRefinedMember`and `TermRefinement.resolveMember`. + */ + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult + private[Types] def lookupRefined(name: Name)(using Context): Option[Type] = None // TODO @@ -509,6 +521,11 @@ object Types { private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = underlying.resolveMember(name, pre) + + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + underlying.resolveMatchingMember(name, pre, typePredicate) } /** Non-proxy types */ @@ -542,6 +559,11 @@ object Types { abstract class CustomTransientGroundType extends GroundType: private[tastyquery] final def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = throw AssertionError(s"Trying to findMember($name, $pre) on $this") + + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + throw AssertionError(s"Trying to call resolveMatchingMember($name, $pre) on $this") end CustomTransientGroundType // ----- Marker traits ------------------------------------------------ @@ -736,7 +758,7 @@ object Types { prefix match case prefix: Type => prefix.resolveMember(name, prefix) match - case ResolveMemberResult.TermMember(symbols, tpe) => + case ResolveMemberResult.TermMember(symbols, tpe) if symbols.nonEmpty => storeResolved(symbols.head, tpe) case _ => throw MemberNotFoundException(prefix, name) @@ -766,6 +788,17 @@ object Types { mt.resultType.resolveMember(name, pre) case tp => tp.resolveMember(name, pre) + end resolveMember + + private[tastyquery] override def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + underlying match + case mt: MethodType if mt.paramInfos.isEmpty /*&& symbol.is(StableRealizable)*/ => + mt.resultType.resolveMatchingMember(name, pre, typePredicate) + case tp => + tp.resolveMatchingMember(name, pre, typePredicate) + end resolveMatchingMember protected final def normalizedDerivedSelectImpl(prefix: Type)(using Context): TermRef = designator match @@ -845,6 +878,16 @@ object Types { ResolveMemberResult.NotFound end resolveMember + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + /* Until proven otherwise, we assume that PackageRef's cannot participate + * as valid subtypes of `TermRefinement`s, and that therefore we can + * always return `NotFound` here. + */ + ResolveMemberResult.NotFound + end resolveMatchingMember + override def toString(): String = s"PackageRef($fullyQualifiedName)" } @@ -967,6 +1010,16 @@ object Types { case _ => underlying.resolveMember(name, pre) + private[tastyquery] override def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + optSymbol match + case Some(sym: ClassSymbol) => + sym.resolveMatchingMember(name, pre, typePredicate) + case _ => + underlying.resolveMatchingMember(name, pre, typePredicate) + end resolveMatchingMember + protected final def normalizedDerivedSelectImpl(prefix: Type)(using Context): Type = val result1 = optSymbol match case Some(sym: ClassTypeParamSymbol) => sym.argForParam(prefix) @@ -1107,7 +1160,18 @@ object Types { case tycon @ TypeRef.OfClass(_) => tycon.resolveMember(name, pre) case _ => - ??? + superType.resolveMember(name, pre) + end resolveMember + + private[tastyquery] override def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + tycon match + case tycon @ TypeRef.OfClass(_) => + tycon.resolveMatchingMember(name, pre, typePredicate) + case _ => + superType.resolveMatchingMember(name, pre, typePredicate) + end resolveMatchingMember private[tastyquery] final def derivedAppliedType(tycon: Type, args: List[Type]): AppliedType = if ((tycon eq this.tycon) && (args eq this.args)) this @@ -1272,6 +1336,11 @@ object Types { private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = throw new AssertionError(s"Cannot find member in $this") + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + throw new AssertionError(s"Cannot find member in $this") + override def toString: String = if !initialized then s"MethodType($paramNames)(...)" else s"MethodType($paramNames)($paramTypes, $resultType)" @@ -1347,6 +1416,11 @@ object Types { private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = throw new AssertionError(s"Cannot find member in $this") + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + throw new AssertionError(s"Cannot find member in $this") + override def toString: String = if !initialized then s"PolyType($paramNames)(...)" else s"PolyType($paramNames)($myBounds, $myRes)" @@ -1499,17 +1573,6 @@ object Types { val refinedName: Name override final def underlying(using Context): Type = parent - - private[tastyquery] override def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = - val parentMember = parent.resolveMember(name, pre) - - if name != refinedName then parentMember - else - val myResult = makeResolveMemberResult(pre) - ResolveMemberResult.merge(parentMember, myResult) - end resolveMember - - protected def makeResolveMemberResult(pre: Type)(using Context): ResolveMemberResult end RefinedType /** A type refinement `parent { type refinedName <:> refinedBounds }`. @@ -1519,8 +1582,14 @@ object Types { */ final class TypeRefinement(val parent: Type, val refinedName: TypeName, val refinedBounds: TypeBounds) extends RefinedType: - protected def makeResolveMemberResult(pre: Type)(using Context): ResolveMemberResult = - ResolveMemberResult.TypeMember(Nil, refinedBounds) + private[tastyquery] override def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = + val parentMember = parent.resolveMember(name, pre) + + if name != refinedName then parentMember + else + val myResult = ResolveMemberResult.TypeMember(Nil, refinedBounds) + ResolveMemberResult.merge(parentMember, myResult) + end resolveMember private[tastyquery] final def derivedTypeRefinement( parent: Type, @@ -1540,8 +1609,59 @@ object Types { * @param refinedType The refined type for the given term member */ final class TermRefinement(val parent: Type, val refinedName: TermName, val refinedType: Type) extends RefinedType: - protected def makeResolveMemberResult(pre: Type)(using Context): ResolveMemberResult = - ResolveMemberResult.TermMember(Nil, refinedType) + // Cache fields + private[tastyquery] val isMethodic = refinedType.isInstanceOf[MethodicType] + private var mySignedName: SignedName | Null = null + + private[tastyquery] def signedName(using Context): SignedName = + val local = mySignedName + if local != null then local + else + val sig = Signature.fromType(refinedType, SourceLanguage.Scala3, optCtorReturn = None) + val computed = SignedName(refinedName, sig) + mySignedName = computed + computed + end signedName + + private[tastyquery] override def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = + if !isMethodic then + val parentMember = parent.resolveMember(name, pre) + if name != refinedName then parentMember + else + parentMember match + case ResolveMemberResult.TermMember(symbols, tpe) => + ResolveMemberResult.TermMember(symbols, tpe & refinedType) + case _ => + ResolveMemberResult.TermMember(Nil, refinedType) + else + name match + case SignedName(simpleName, sig, _) if simpleName == refinedName && sig.paramsCorrespond(signedName.sig) => + val parentMember = parent.resolveMatchingMember(signedName, pre, refinedType.isSubtype(_)) + parentMember match + case ResolveMemberResult.TermMember(symbols, _) => + // We can disregard the type coming from parent because we selected for `refinedType.isSubtype(_)` + ResolveMemberResult.TermMember(symbols, refinedType) + case _ => + ResolveMemberResult.TermMember(Nil, refinedType) + case _ => + parent.resolveMember(name, pre) + end resolveMember + + private[tastyquery] override def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + if isMethodic && name.underlying == refinedName && name.sig.paramsCorrespond(signedName.sig) then + if !typePredicate(refinedType) then ResolveMemberResult.NotFound + else + val parentMember = parent.resolveMatchingMember(signedName, pre, refinedType.isSubtype(_)) + parentMember match + case ResolveMemberResult.TermMember(symbols, _) => + // We can disregard the type coming from parent because we selected for `refinedType.isSubtype(_)` + ResolveMemberResult.TermMember(symbols, refinedType) + case _ => + ResolveMemberResult.TermMember(Nil, refinedType) + else parent.resolveMatchingMember(name, pre, typePredicate) + end resolveMatchingMember private[tastyquery] final def derivedTermRefinement(parent: Type, refinedName: TermName, refinedType: Type): Type = if ((parent eq this.parent) && (refinedName eq this.refinedName) && (refinedType eq this.refinedType)) this @@ -1760,6 +1880,11 @@ object Types { private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = join.resolveMember(name, pre) + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + join.resolveMatchingMember(name, pre, typePredicate) + private[tastyquery] def derivedOrType(first: Type, second: Type): Type = if (first eq this.first) && (second eq this.second) then this else OrType.make(first, second) @@ -1777,6 +1902,15 @@ object Types { private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = ResolveMemberResult.merge(first.resolveMember(name, pre), second.resolveMember(name, pre)) + private[tastyquery] def resolveMatchingMember(name: SignedName, pre: Type, typePredicate: Type => Boolean)( + using Context + ): ResolveMemberResult = + ResolveMemberResult.merge( + first.resolveMatchingMember(name, pre, typePredicate), + second.resolveMatchingMember(name, pre, typePredicate) + ) + end resolveMatchingMember + private[tastyquery] def derivedAndType(first: Type, second: Type): Type = if ((first eq this.first) && (second eq this.second)) this else AndType.make(first, second) diff --git a/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala index 371d5dcb..1aca41f0 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala @@ -767,6 +767,43 @@ class SubtypingSuite extends UnrestrictedUnpicklingSuite: assertNeitherSubtype(ConcreteSimplePathsChildClass.appliedRef, refineTerm("abstractTerm", listOf(defn.BooleanType))) .withRef[ConcreteSimplePathsChild, SimplePaths { def abstractTerm: List[Boolean] }] + val refinedSomeMethodType = + refineTerm("someMethod", MethodType(List(termName("x")), List(defn.IntType), defn.IntType)) + assertEquiv( + refineTerm("someMethod", MethodType(List(termName("x")), List(defn.IntType), defn.IntType)), + refinedSomeMethodType + ) + .withRef[SimplePaths { def someMethod(x: Int): Int }, SimplePaths { def someMethod(x: Int): Int }] + assertStrictSubtype(ConcreteSimplePathsChildClass.appliedRef, refinedSomeMethodType) + .withRef[ConcreteSimplePathsChild, SimplePaths { def someMethod(x: Int): Int }] + assertNeitherSubtype( + ConcreteSimplePathsChildClass.appliedRef, + refineTerm("someMethod", MethodType(List(termName("x")), List(defn.IntType), defn.BooleanType)) + ) + .withRef[ConcreteSimplePathsChild, SimplePaths { def someMethod(x: Int): Boolean }] + + val refinedSomePolyMethodType = + refineTerm( + "somePolyMethod", + PolyType(List(typeName("X")))( + tl => List(defn.NothingAnyBounds), + tl => MethodType(List(termName("x")), List(tl.paramRefs(0)), tl.paramRefs(0)) + ) + ) + assertEquiv( + refineTerm( + "somePolyMethod", + PolyType(List(typeName("Y")))( + tl => List(defn.NothingAnyBounds), + tl => MethodType(List(termName("x")), List(tl.paramRefs(0)), tl.paramRefs(0)) + ) + ), + refinedSomePolyMethodType + ) + .withRef[SimplePaths { def somePolyMethod[X](x: X): X }, SimplePaths { def somePolyMethod[Y](x: Y): Y }] + assertStrictSubtype(ConcreteSimplePathsChildClass.appliedRef, refinedSomePolyMethodType) + .withRef[ConcreteSimplePathsChild, SimplePaths { def somePolyMethod[Y](x: Y): Y }] + // term refinement - does not exist in class assertEquiv(refineTerm("otherTerm", defn.StringType), refineTerm("otherTerm", defn.StringType)) @@ -782,6 +819,46 @@ class SubtypingSuite extends UnrestrictedUnpicklingSuite: .withRef[ConcreteSimplePathsChild, SimplePaths { def otherTerm: List[Any] }] assertNeitherSubtype(ConcreteSimplePathsChildClass.appliedRef, refineTerm("otherTerm", listOf(defn.BooleanType))) .withRef[ConcreteSimplePathsChild, SimplePaths { def otherTerm: List[Boolean] }] + + val refinedSomeOtherMethodType = + refineTerm("someOtherMethod", MethodType(List(termName("x")), List(defn.IntType), defn.IntType)) + assertEquiv( + refineTerm("someOtherMethod", MethodType(List(termName("x")), List(defn.IntType), defn.IntType)), + refinedSomeOtherMethodType + ) + .withRef[SimplePaths { def someOtherMethod(x: Int): Int }, SimplePaths { def someOtherMethod(x: Int): Int }] + assertNeitherSubtype(ConcreteSimplePathsChildClass.appliedRef, refinedSomeOtherMethodType) + .withRef[ConcreteSimplePathsChild, SimplePaths { def someOtherMethod(x: Int): Int }] + assertNeitherSubtype( + ConcreteSimplePathsChildClass.appliedRef, + refineTerm("someOtherMethod", MethodType(List(termName("x")), List(defn.IntType), defn.BooleanType)) + ) + .withRef[ConcreteSimplePathsChild, SimplePaths { def someOtherMethod(x: Int): Boolean }] + + /* No `withRef` tests for a `someOtherPolyMethod` because dotc says: + * + * > Polymorphic refinement method someOtherPolyMethod without matching + * > type in parent class SimplePaths is no longer allowed + */ + val refinedSomeOtherPolyMethodType = + refineTerm( + "someOtherPolyMethod", + PolyType(List(typeName("X")))( + tl => List(defn.NothingAnyBounds), + tl => MethodType(List(termName("x")), List(tl.paramRefs(0)), tl.paramRefs(0)) + ) + ) + assertEquiv( + refineTerm( + "someOtherPolyMethod", + PolyType(List(typeName("Y")))( + tl => List(defn.NothingAnyBounds), + tl => MethodType(List(termName("x")), List(tl.paramRefs(0)), tl.paramRefs(0)) + ) + ), + refinedSomeOtherPolyMethodType + ) + assertNeitherSubtype(ConcreteSimplePathsChildClass.appliedRef, refinedSomeOtherPolyMethodType) } testWithContext("intersection-types") { diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala index de32d6ea..191d2878 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala @@ -2049,6 +2049,26 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { end for } + testWithContext("findMember-refined-method-signature") { + val RefinedTypeTreeClass = ctx.findTopLevelClass("simple_trees.RefinedTypeTree") + + val fooSym = RefinedTypeTreeClass.findNonOverloadedDecl(termName("foo")) + val fooBody = fooSym.tree.get.asInstanceOf[DefDef].rhs.get + val Apply(fun @ Select(qualifier, signedName @ SignedName(SimpleName("m"), _, _)), Nil) = fooBody: @unchecked + + assert(clue(qualifier.tpe).isInstanceOf[TermRef]) + assert(clue(qualifier.tpe.asInstanceOf[TermRef].underlying).isInstanceOf[TermRefinement]) + + val optMember = qualifier.tpe.lookupMember(signedName) + assert(optMember.isDefined) + + val AClass = RefinedTypeTreeClass.findMember(typeName("A")).asClass + val AmSym = AClass.findNonOverloadedDecl(termName("m")) + + assert(clue(optMember.get.symbol) == AmSym) + assert(clue(fooBody.tpe).isRef(defn.IntClass)) + } + testWithContext("scala-enum-anon-class-signature-name") { val ScalaEnumClass = ctx.findTopLevelClass("simple_trees.ScalaEnum") val ScalaEnumModuleClass = ctx.findTopLevelModuleClass("simple_trees.ScalaEnum") diff --git a/test-sources/src/main/scala/subtyping/paths/Paths.scala b/test-sources/src/main/scala/subtyping/paths/Paths.scala index 3f4eb519..01bd173b 100644 --- a/test-sources/src/main/scala/subtyping/paths/Paths.scala +++ b/test-sources/src/main/scala/subtyping/paths/Paths.scala @@ -38,6 +38,9 @@ open class SimplePaths: type AliasOfAbstractTypeWithBounds = AbstractTypeWithBounds def abstractTerm: AnyRef = ??? + + def someMethod(x: Int): Any = ??? + def somePolyMethod[X](x: X): Any = ??? end SimplePaths // With members of the same shape as SimplePaths @@ -54,6 +57,9 @@ class ConcreteSimplePathsChild extends SimplePaths: override def abstractTerm: List[Int] = ??? + override def someMethod(x: Int): Int = ??? + override def somePolyMethod[X](x: X): X = ??? + class InnerClassMono extends A class InnerClassPoly[T] extends C end ConcreteSimplePathsChild