diff --git a/build.sbt b/build.sbt index c2d5b489..2395b8ff 100644 --- a/build.sbt +++ b/build.sbt @@ -113,6 +113,8 @@ lazy val tastyQuery = mimaBinaryIssueFilters ++= { import com.typesafe.tools.mima.core.* Seq( + // private, not an issue + ProblemFilters.exclude[MissingClassProblem]("tastyquery.TypeOps$TypeFold"), // Everything in tastyquery.reader is private[tastyquery] at most ProblemFilters.exclude[Problem]("tastyquery.reader.*"), ) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala index 678f9960..ef8995d9 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala @@ -469,6 +469,8 @@ object Symbols { if local != null then local else throw new IllegalStateException(s"$this was not assigned a declared type") + private lazy val isPrefixDependent: Boolean = TypeOps.isPrefixDependent(declaredType) + private[tastyquery] final def setParamSymss(paramSymss: List[ParamSymbolsClause]): this.type = if myParamSymss != null then throw IllegalStateException(s"reassignment of paramSymss to $this") myParamSymss = paramSymss @@ -607,7 +609,8 @@ object Symbols { TermRef(owner.staticOwnerPrefix, this) final def typeAsSeenFrom(prefix: Prefix)(using Context): TypeOrMethodic = - declaredType.asSeenFrom(prefix, owner) + if isPrefixDependent then declaredType.asSeenFrom(prefix, owner) + else declaredType // fast path private def isConstructor: Boolean = owner.isClass && isMethod && name == nme.Constructor @@ -731,9 +734,12 @@ object Symbols { def declaredBounds: TypeBounds + private lazy val isPrefixDependent = TypeOps.isPrefixDependent(declaredBounds) + final def boundsAsSeenFrom(prefix: Prefix)(using Context): TypeBounds = def default: TypeBounds = - declaredBounds.mapBounds(_.asSeenFrom(prefix, owner)) + if isPrefixDependent then declaredBounds.mapBounds(_.asSeenFrom(prefix, owner)) + else declaredBounds // fast path, but also important to cut infinite recursions this match case sym: ClassTypeParamSymbol => diff --git a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala index 9b57b0e4..cf4cf6b3 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala @@ -6,6 +6,15 @@ import tastyquery.Types.* import tastyquery.TypeMaps.* private[tastyquery] object TypeOps: + /** Is the given type dependent at all on the prefix it will be seen from? + * + * In other words, does it contain any `ThisType`? + * + * If not, there is no need to map it through `asSeenFrom`. + */ + def isPrefixDependent(tp: TypeMappable): Boolean = + existsPart(tp)(_.isInstanceOf[ThisType]) + def asSeenFrom(tp: TypeOrMethodic, pre: Prefix, cls: Symbol)(using Context): tp.ThisTypeMappableType = pre match case NoPrefix | _: PackageRef => tp @@ -83,6 +92,69 @@ private[tastyquery] object TypeOps: } } + // Folds over types + + def existsPart(tp: TypeMappable)(p: TypeMappable => Boolean): Boolean = + new TypeFold[Boolean] { + override def apply(z: Boolean, tp: TypeMappable): Boolean = + z || p(tp) || super.apply(z, tp) + }.apply(false, tp) + end existsPart + + private class TypeFold[A] extends ((A, TypeMappable) => A): + def apply(z: A, tp: TypeMappable): A = tp match + case NoPrefix | _: PackageRef => + z + + case _: NothingType | _: AnyKindType | _: ThisType | _: ConstantType | _: ParamRef | _: RecThis => + z + case tp: NamedType => + apply(z, tp.prefix) + case tp: SuperType => + apply(z, tp.thistpe) + case tp: AppliedType => + tp.args.foldLeft(apply(z, tp.tycon))(this) + case tp: ByNameType => + apply(z, tp.resultType) + case tp: RepeatedType => + apply(z, tp.elemType) + case tp: LambdaType => + apply(tp.paramInfos.foldLeft(z)(this), tp.resultType) + case tp: AnnotatedType => + apply(z, tp.typ) + case tp: TypeRefinement => + apply(apply(z, tp.parent), tp.refinedBounds) + case tp: TermRefinement => + apply(apply(z, tp.parent), tp.refinedType) + case tp: RecType => + apply(z, tp.parent) + case tp: MatchType => + val z1 = apply(apply(z, tp.bound), tp.scrutinee) + tp.cases.foldLeft(z1)(apply(_, _)) + case tp: SkolemType => + apply(z, tp.tpe) + case tp: OrType => + apply(apply(z, tp.first), tp.second) + case tp: AndType => + apply(apply(z, tp.first), tp.second) + + case tp: WildcardTypeArg => + apply(z, tp.bounds) + + case AbstractTypeBounds(low, high) => + apply(apply(z, low), high) + case TypeAlias(alias) => + apply(z, alias) + + case tp: CustomTransientGroundType => + throw IllegalArgumentException(s"Unexpected custom transient type: $tp") + end apply + + def apply(z: A, caze: MatchTypeCase): A = + val z1 = caze.paramTypeBounds.foldLeft(z)(this) + apply(apply(z1, caze.pattern), caze.result) + end TypeFold + // Tests around `matches` /** The implementation for `tp1.matches(tp2)`. */ diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala index 05dd6da2..b3f44ff4 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala @@ -3635,4 +3635,16 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { assert(clue(c.declaredType).isApplied(_.isRef(JCollectionClass), List(_.isBounded(_.isNothing, _.isRef(e))))) } } + + testWithContext("recursive-match-types-bounds-as-seen-from-issue-401") { + val RecursiveMatchTypeSym = + ctx.findStaticType("simple_trees.RecursiveMatchType$package.RecursiveMatchType").asInstanceOf[TypeMemberSymbol] + val staticRef = RecursiveMatchTypeSym.staticRef + + /* Here, we only make sure that calls below terminate. + * More elaborate resolution is tested through `WholeClasspathSuite` over `RecursiveMatchTypeTest`. + */ + RecursiveMatchTypeSym.boundsAsSeenFrom(staticRef.prefix) + staticRef.underlying + } } diff --git a/test-sources/src/main/scala/simple_trees/RecursiveMatchType.scala b/test-sources/src/main/scala/simple_trees/RecursiveMatchType.scala new file mode 100644 index 00000000..78c02b67 --- /dev/null +++ b/test-sources/src/main/scala/simple_trees/RecursiveMatchType.scala @@ -0,0 +1,18 @@ +package simple_trees + +type RecursiveMatchType[A <: Tuple] <: Tuple = A match + case hd *: tl => hd *: RecursiveMatchType[tl] + case EmptyTuple => EmptyTuple + +object RecursiveMatchType: + inline def rec[A <: Tuple](a: A): RecursiveMatchType[A] = + inline a match + case b: (hd *: tl) => b.head *: rec(b.tail) + case _: EmptyTuple => EmptyTuple +end RecursiveMatchType + +// must be in a separate TASTy file to trigger issue #401 +object RecursiveMatchTypeTest: + inline def rec[A <: Tuple](x: A): RecursiveMatchType[A] = + RecursiveMatchType.rec(x) +end RecursiveMatchTypeTest