diff --git a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala index d1138c64..d0dc841c 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala @@ -273,6 +273,32 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS lazy val String_+ : TermSymbol = StringClass.findNonOverloadedDecl(nme.m_+) + private[tastyquery] def createEnumMagicMethods(cls: ClassSymbol): Unit = + createSpecialMethod(cls, nme.Constructor, PolyType(List(typeName("E")), List(NothingAnyBounds), UnitType)) + end createEnumMagicMethods + + /** Creates the members that are patched from stdLibPatches on Predef. + * + * dotc does that generically, but it does not really work for us, as we + * cannot read other files while loading one file. + */ + private[tastyquery] def createPredefMagicMethods(cls: ClassSymbol): Unit = + val nnMethodType = PolyType(List(typeName("T")))( + _ => List(NothingAnyBounds), + pt => MethodType(List(termName("x")))(_ => List(pt.paramRefs(0)), mt => AndType(mt.paramRefs(0), pt.paramRefs(0))) + ) + createSpecialMethod(cls, termName("nn"), nnMethodType, Inline | Final | Extension) + + val anyRefOrNull = OrType(AnyRefType, NullType) + val eqNeMethodType = MethodType( + List(termName("x")), + List(anyRefOrNull), + MethodType(List(termName("y")), List(anyRefOrNull), BooleanType) + ) + createSpecialMethod(cls, termName("eq"), eqNeMethodType, Inline | Final | Extension) + createSpecialMethod(cls, termName("ne"), eqNeMethodType, Inline | Final | Extension) + end createPredefMagicMethods + private def createSpecialPolyClass( name: TypeName, paramFlags: FlagSet, @@ -394,6 +420,7 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS lazy val ProductClass = scalaPackage.requiredClass("Product") lazy val ErasedNothingClass = scalaRuntimePackage.requiredClass("Nothing$") + lazy val ErasedBoxedUnitClass = scalaRuntimePackage.requiredClass("BoxedUnit") private[tastyquery] lazy val targetNameAnnotClass = scalaAnnotationPackage.optionalClass("targetName") diff --git a/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala b/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala index 9ccc35ec..8357683c 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala @@ -19,8 +19,11 @@ private[tastyquery] object Erasure: erase(tpe, SourceLanguage.Scala3) def erase(tpe: Type, language: SourceLanguage)(using Context): ErasedTypeRef = + erase(tpe, language, keepUnit = false) + + def erase(tpe: Type, language: SourceLanguage, keepUnit: Boolean)(using Context): ErasedTypeRef = given SourceLanguage = language - finishErase(preErase(tpe)) + finishErase(preErase(tpe, keepUnit)) end erase /** First pass of erasure, where some special types are preserved as is. @@ -28,12 +31,12 @@ private[tastyquery] object Erasure: * In particular, `Any` is preserved as `Any`, instead of becoming * `java.lang.Object`. */ - private def preErase(tpe: Type)(using Context, SourceLanguage): ErasedTypeRef = + private def preErase(tpe: Type, keepUnit: Boolean)(using Context, SourceLanguage): ErasedTypeRef = def hasArrayErasure(cls: ClassSymbol): Boolean = cls == defn.ArrayClass || (cls == defn.RepeatedParamClass && summon[SourceLanguage] == SourceLanguage.Java) def arrayOfBounds(bounds: TypeBounds): ErasedTypeRef = - preErase(bounds.high) match + preErase(bounds.high, keepUnit = false) match case ClassRef(cls) if cls == defn.AnyClass || cls == defn.AnyValClass => ClassRef(defn.ObjectClass) case typeRef => @@ -50,7 +53,8 @@ private[tastyquery] object Erasure: case _ => arrayOf(tpe.translucentSuperType) case TypeRef.OfClass(cls) => - ClassRef(cls).arrayOf() + if cls == defn.UnitClass then ClassRef(defn.ErasedBoxedUnitClass).arrayOf() + else ClassRef(cls).arrayOf() case tpe: TypeRef => tpe.optSymbol match case Some(sym: TypeMemberSymbol) => @@ -61,7 +65,7 @@ private[tastyquery] object Erasure: case _ => arrayOfBounds(tpe.bounds) case tpe: TypeParamRef => arrayOfBounds(tpe.bounds) - case tpe: Type => preErase(tpe).arrayOf() + case tpe: Type => preErase(tpe, keepUnit = false).arrayOf() case tpe: WildcardTypeArg => arrayOfBounds(tpe.bounds) end arrayOf @@ -74,40 +78,44 @@ private[tastyquery] object Erasure: arrayOf(targ) else ClassRef(cls) case _ => - preErase(tpe.translucentSuperType) + preErase(tpe.translucentSuperType, keepUnit) case TypeRef.OfClass(cls) => - ClassRef(cls) + if !keepUnit && cls == defn.UnitClass then ClassRef(defn.ErasedBoxedUnitClass) + else ClassRef(cls) case tpe: TypeRef => tpe.optSymbol match case Some(sym: TypeMemberSymbol) => sym.typeDef match case TypeMemberDefinition.OpaqueTypeAlias(_, alias) => - preErase(alias) + preErase(alias, keepUnit) case _ => - preErase(tpe.underlying) + preErase(tpe.underlying, keepUnit) case _ => - preErase(tpe.underlying) + preErase(tpe.underlying, keepUnit) case tpe: SingletonType => - preErase(tpe.underlying) + preErase(tpe.underlying, keepUnit) case tpe: TypeParamRef => - preErase(tpe.bounds.high) + preErase(tpe.bounds.high, keepUnit) case tpe: MatchType => tpe.reduced match - case Some(reduced) => preErase(reduced) - case None => preErase(tpe.bound) + case Some(reduced) => preErase(reduced, keepUnit) + case None => preErase(tpe.bound, keepUnit) case tpe: OrType => - erasedLub(preErase(tpe.first), preErase(tpe.second)) + erasedLub(preErase(tpe.first, keepUnit = false), preErase(tpe.second, keepUnit = false)) case tpe: AndType => summon[SourceLanguage] match - case SourceLanguage.Java => preErase(tpe.first) - case SourceLanguage.Scala2 => preErase(Scala2Erasure.eraseAndType(tpe)) - case SourceLanguage.Scala3 => erasedGlb(preErase(tpe.first), preErase(tpe.second)) + case SourceLanguage.Java => + preErase(tpe.first, keepUnit = false) + case SourceLanguage.Scala2 => + preErase(Scala2Erasure.eraseAndType(tpe), keepUnit = false) + case SourceLanguage.Scala3 => + erasedGlb(preErase(tpe.first, keepUnit = false), preErase(tpe.second, keepUnit = false)) case tpe: AnnotatedType => - preErase(tpe.typ) + preErase(tpe.typ, keepUnit) case tpe: RefinedType => - preErase(tpe.parent) + preErase(tpe.parent, keepUnit) case tpe: RecType => - preErase(tpe.parent) + preErase(tpe.parent, keepUnit) case _: ByNameType => defn.Function0Class.erasure case _: NothingType => @@ -168,7 +176,8 @@ private[tastyquery] object Erasure: end erasedLub private def erasedClassRefLub(cls1: ClassSymbol, cls2: ClassSymbol)(using Context): ClassSymbol = - if cls1 == defn.ErasedNothingClass then cls2 + if cls1 == cls2 then cls1 + else if cls1 == defn.ErasedNothingClass then cls2 else if cls2 == defn.ErasedNothingClass then cls1 else if cls1 == defn.NullClass then if cls2.isSubClass(defn.ObjectClass) then cls2 @@ -176,6 +185,7 @@ private[tastyquery] object Erasure: else if cls2 == defn.NullClass then if cls1.isSubClass(defn.ObjectClass) then cls1 else defn.AnyClass + else if cls1 == defn.ErasedBoxedUnitClass || cls2 == defn.ErasedBoxedUnitClass then defn.ObjectClass else /** takeWhile+1 */ def takeUpTo[T](l: List[T])(f: T => Boolean): List[T] = diff --git a/tasty-query/shared/src/main/scala/tastyquery/Names.scala b/tasty-query/shared/src/main/scala/tastyquery/Names.scala index c45ae402..e6cedeaa 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Names.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Names.scala @@ -121,6 +121,7 @@ object Names { val Tuple: TypeName = typeName("Tuple") val NonEmptyTuple: TypeName = typeName("NonEmptyTuple") val TupleCons: TypeName = typeName("*:") + val Enum: TypeName = typeName("Enum") @deprecated("you probably meant the term name `nme.EmptyTuple` instead", since = "0.8.3") val EmptyTuple: TypeName = typeName("EmptyTuple") @@ -133,11 +134,14 @@ object Names { val scala2PackageObjectClass: TypeName = termName("package").withObjectSuffix.toTypeName private[tastyquery] val runtimeNothing: TypeName = typeName("Nothing$") + private[tastyquery] val runtimeBoxedUnit: TypeName = typeName("BoxedUnit") private[tastyquery] val internalRepeatedAnnot: TypeName = typeName("Repeated") private[tastyquery] val scala2LocalChild: TypeName = typeName("") private[tastyquery] val scala2ByName: TypeName = typeName("") + + private[tastyquery] val PredefModule: TypeName = moduleClassName("Predef") } /** Create a type name from a string */ diff --git a/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala b/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala index 6243196b..b3c8b8f4 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Signatures.scala @@ -40,8 +40,9 @@ object Signatures: case info: PolyType => rec(info.resultType, acc ::: ParamSig.TypeLen(info.paramTypeBounds.length) :: Nil) case tpe: Type => - val retType = optCtorReturn.map(_.localRef).getOrElse(tpe) - Signature(acc, ErasedTypeRef.erase(retType, language).toSigFullName) + val retType = optCtorReturn.map(_.appliedRefInsideThis).getOrElse(tpe) + val erasedRetType = ErasedTypeRef.erase(retType, language, keepUnit = true) + Signature(acc, erasedRetType.toSigFullName) } rec(info, Nil) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala index 218e4ed1..786a3943 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala @@ -190,14 +190,14 @@ private[tastyquery] object Subtyping: private def level3(tp1: Type, tp2: Type)(using Context): Boolean = tp2 match case TypeRef.OfClass(cls2) => - if cls2.typeParams.isEmpty then + val tparams2 = cls2.typeParams + if tparams2.isEmpty then if tp1.isLambdaSub then false // should be tp1.hasHigherKind, but the scalalib does not like that else if cls2 == defn.AnyClass then true else if cls2 == defn.SingletonClass && isSingleton(tp1) then true else level3WithBaseType(tp1, tp2, cls2) - else - // TODO Try eta-expansion if tp1.isLambdaSub && !tp1.isAnyKind - level4(tp1, tp2) + else if tp1.isLambdaSub then isSubType(tp1, etaExpand(tp2, tparams2)) + else level4(tp1, tp2) case tp2: TypeRef => isSubType(tp1, tp2.bounds.low) diff --git a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala index d462848a..cf5a484f 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Symbols.scala @@ -283,8 +283,8 @@ object Symbols { final def isPublic: Boolean = !flags.isAnyOf(Private | Protected | Local) && privateWithin.isEmpty - private[Symbols] final def isPrivateThis: Boolean = - flags.isAllOf(Private | Local) + private[Symbols] final def isPrivateParamAccessor: Boolean = + flags.isAllOf(Private | Local | ParamAccessor) /** The declared visibility of this symbol. */ final def visibility: Visibility = @@ -1156,9 +1156,9 @@ object Symbols { if owner == defn.scalaPackage then // The classes with special erasures that are loaded from Scala 2 pickles or .tasty files name match - case tpnme.AnyVal | tpnme.TupleCons => defn.ObjectClass.erasure - case tpnme.Tuple | tpnme.NonEmptyTuple => defn.ProductClass.erasure - case _ => ErasedTypeRef.ClassRef(this) + case tpnme.AnyVal => defn.ObjectClass.erasure + case tpnme.Tuple | tpnme.NonEmptyTuple | tpnme.TupleCons => defn.ProductClass.erasure + case _ => ErasedTypeRef.ClassRef(this) else ErasedTypeRef.ClassRef(this) end computeErasure @@ -1495,7 +1495,7 @@ object Symbols { case _ => false getDecl(name) match - case some @ Some(sym) if !sym.isPrivateThis || isOwnThis => + case some @ Some(sym) if !sym.isPrivateParamAccessor || isOwnThis => some case _ => if name == nme.Constructor then None @@ -1628,6 +1628,21 @@ object Symbols { case tpe => throw InvalidProgramStructureException(s"Unexpected type $tpe for $annot") end extractSealedChildFromChildAnnot + + private[tastyquery] def makePolyConstructorType(selfReferencingCtorType: TypeOrMethodic): TypeOrMethodic = + val typeParams = this.typeParams + if typeParams.isEmpty then selfReferencingCtorType + else + /* Make a PolyType with the same type parameters as the class, and + * substitute references to them of the form `C.this.T` by the + * corresponding `paramRefs` of the `PolyType`. + */ + PolyType(typeParams.map(_.name))( + pt => + typeParams.map(p => Substituters.substLocalThisClassTypeParams(p.declaredBounds, typeParams, pt.paramRefs)), + pt => Substituters.substLocalThisClassTypeParams(selfReferencingCtorType, typeParams, pt.paramRefs) + ) + end makePolyConstructorType } object ClassSymbol: diff --git a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala index 21212b84..645ba9d7 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala @@ -145,4 +145,60 @@ private[tastyquery] object TypeOps: loop(tp1.paramTypes, tp2.paramTypes) end matchingMethodParams + + // baseClasses + + /** The set of classes inherited by this type, in linearization order. */ + def baseClasses(tp: Type)(using Context): List[ClassSymbol] = + tp match + case TypeRef.OfClass(cls) => + cls.linearization + + case tp: TypeProxy => + baseClasses(tp.superType) + + case tp: AndType => + // Base classes are the merge of the operand base classes. + val baseClasses1 = baseClasses(tp.first) + val baseClasses1Set = baseClasses1.toSet + + def recur(baseClasses2: List[ClassSymbol]): List[ClassSymbol] = baseClasses2 match + case baseClass2 :: baseClasses2Rest => + if baseClasses1Set.contains(baseClass2) then + // Don't add baseClass2 since it's already there; shortcut altogether if it is not a trait + if baseClass2.isTrait then recur(baseClasses2Rest) + else baseClasses1 // common class, therefore the rest is the same in both sequences + else + // Include baseClass2 and continue + baseClass2 :: recur(baseClasses2Rest) + case Nil => + baseClasses1 + end recur + + recur(baseClasses(tp.second)) + + case tp: OrType => + // Base classes are the intersection of the operand base classes. + // scala.Null is ignored on both sides + val baseClasses1 = baseClasses(tp.first).filter(_ != defn.NullClass) + val baseClasses1Set = baseClasses1.toSet + + def recur(baseClasses2: List[ClassSymbol]): List[ClassSymbol] = baseClasses2 match + case baseClass2 :: baseClasses2Rest => + if baseClasses1Set.contains(baseClass2) then + // Include baseClass2 which is common; shortcut altogether if it is not a trait + if baseClass2.isTrait then baseClass2 :: recur(baseClasses2Rest) + else baseClasses2 // common class, therefore the rest is the same in both sequences + else + // Exclude baseClass2 and continue + recur(baseClasses2Rest) + case Nil => + Nil + end recur + + recur(baseClasses(tp.second).filter(_ != defn.NullClass)) + + case _: AnyKindType | _: NothingType | _: TypeLambda | _: CustomTransientGroundType => + Nil + end baseClasses end TypeOps diff --git a/tasty-query/shared/src/main/scala/tastyquery/Types.scala b/tasty-query/shared/src/main/scala/tastyquery/Types.scala index e08c9487..46fd79c6 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/Types.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/Types.scala @@ -163,6 +163,9 @@ object Types { def erase(tpe: Type, language: SourceLanguage)(using Context): ErasedTypeRef = Erasure.erase(tpe, language) + def erase(tpe: Type, language: SourceLanguage, keepUnit: Boolean)(using Context): ErasedTypeRef = + Erasure.erase(tpe, language, keepUnit) + extension (typeRef: ArrayTypeRef) def elemType: ErasedTypeRef = if typeRef.dimensions == 1 then typeRef.base @@ -2339,11 +2342,39 @@ object Types { val myJoin = this.myJoin if (myJoin != null) then myJoin else - val computedJoin = defn.ObjectType // TypeOps.orDominator(this) + val computedJoin = computeJoin() this.myJoin = computedJoin computedJoin } + private def computeJoin()(using Context): Type = + /** The minimal set of classes in `classes` which derive all other classes in `classes` */ + def dominators(classes: List[ClassSymbol], acc: List[ClassSymbol]): List[ClassSymbol] = classes match + case cls :: rest => + if acc.exists(_.isSubClass(cls)) then dominators(rest, acc) + else dominators(rest, cls :: acc) + case Nil => + acc + end dominators + + val prunedNulls = tryPruneNulls(this) + + val commonBaseClasses = TypeOps.baseClasses(prunedNulls) + val doms = dominators(commonBaseClasses, Nil) + doms.flatMap(cls => prunedNulls.baseType(cls)).reduceLeft(AndType.make(_, _)) + end computeJoin + + private def tryPruneNulls(tp: Type)(using Context): Type = tp match + case tp: OrType => + val first = tryPruneNulls(tp.first) + val second = tryPruneNulls(tp.second) + if first.isSubType(defn.NullType) && defn.NullType.isSubType(second) then second + else if second.isSubType(defn.NullType) && defn.NullType.isSubType(first) then first + else tp.derivedOrType(first, second) + case _ => + tp + end tryPruneNulls + private[tastyquery] def resolveMember(name: Name, pre: Type)(using Context): ResolveMemberResult = join.resolveMember(name, pre) diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala index d235ddab..3e444cc4 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala @@ -70,6 +70,12 @@ private[reader] final class ReaderContext(underlying: Context): def createStringMagicMethods(cls: ClassSymbol): Unit = underlying.defn.createStringMagicMethods(cls) + + def createEnumMagicMethods(cls: ClassSymbol): Unit = + underlying.defn.createEnumMagicMethods(cls) + + def createPredefMagicMethods(cls: ClassSymbol): Unit = + underlying.defn.createPredefMagicMethods(cls) end ReaderContext private[reader] object ReaderContext: diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/ClassfileParser.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/ClassfileParser.scala index 312fd4a5..da07e7a3 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/ClassfileParser.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/ClassfileParser.scala @@ -237,13 +237,15 @@ private[reader] object ClassfileParser { if cls.owner == rctx.javaLangPackage then if cls.name == tpnme.Object then rctx.createObjectMagicMethods(cls) else if cls.name == tpnme.String then rctx.createStringMagicMethods(cls) + else if cls.name == tpnme.Enum then rctx.createEnumMagicMethods(cls) for (sym, javaFlags, sigOrDesc) <- loadMembers() do val parsedType = sigOrDesc match case SigOrDesc.Desc(desc) => Descriptors.parseDescriptor(sym, desc) case SigOrDesc.Sig(sig) => JavaSignatures.parseSignature(sym, sig, allRegisteredSymbols) val adaptedType = - if sym.isMethod && javaFlags.isVarargsIfMethod then patchForVarargs(sym, parsedType) + if sym.isMethod && sym.name == nme.Constructor then cls.makePolyConstructorType(parsedType) + else if sym.isMethod && javaFlags.isVarargsIfMethod then patchForVarargs(sym, parsedType) else parsedType sym.withDeclaredType(adaptedType) diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/JavaSignatures.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/JavaSignatures.scala index d3ff7409..784e0e26 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/JavaSignatures.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/classfiles/JavaSignatures.scala @@ -200,12 +200,13 @@ private[classfiles] object JavaSignatures: val tname = identifier().toTypeName val classBound = expect(':') - referenceTypeSignature(env) match - case Some(tpe) => tpe - case _ => rctx.FromJavaObjectType + referenceTypeSignature(env).toList val interfaceBounds = readWhile(':', referenceType(env)) if env.withAddedParam(tname) then emptyTypeBounds // shortcut as we will throw away the bounds - else RealTypeBounds(rctx.NothingType, interfaceBounds.foldLeft(classBound)(AndType(_, _))) + else + val allBounds = classBound ::: interfaceBounds + val bound = if allBounds.isEmpty then rctx.AnyType else allBounds.reduceLeft(AndType(_, _)) + RealTypeBounds(rctx.NothingType, bound) def typeParamsRest(env: JavaSignature): List[TypeBounds] = readUntil('>', typeParameter(env)) diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala index fe365021..bcd05440 100644 --- a/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala +++ b/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala @@ -302,6 +302,7 @@ private[pickles] class PickleReader { val givenSelfType = if atEnd then None else Some(readTrueTypeRef()) cls.withParentsDirect(parentTypes) cls.withGivenSelfType(givenSelfType) + if cls.owner == rctx.scalaPackage && tname == tpnme.PredefModule then rctx.createPredefMagicMethods(cls) cls case VALsym => /* Discard symbols that should not be seen from a Scala 3 point of view: @@ -361,19 +362,7 @@ private[pickles] class PickleReader { rctx.UnitType val tpe1 = resultToUnit(tpe) - - val typeParams = cls.typeParams - if typeParams.isEmpty then tpe1 - else - /* Make a PolyType with the same type parameters as the class, and - * substitute references to them of the form `C.this.T` by the - * corresponding `paramRefs` of the `PolyType`. - */ - PolyType(typeParams.map(_.name))( - pt => - typeParams.map(p => Substituters.substLocalThisClassTypeParams(p.declaredBounds, typeParams, pt.paramRefs)), - pt => Substituters.substLocalThisClassTypeParams(tpe1, typeParams, pt.paramRefs) - ) + cls.makePolyConstructorType(tpe1) end patchConstructorType def readChildren()(using ReaderContext, PklStream, Entries, Index): Unit = diff --git a/tasty-query/shared/src/test/scala/tastyquery/SignatureSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/SignatureSuite.scala index 9f0431df..6e65dd9d 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/SignatureSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/SignatureSuite.scala @@ -91,6 +91,19 @@ class SignatureSuite extends UnrestrictedUnpicklingSuite: assertSigned(getFirstEntry, "():java.util.TreeMap.Entry") } + testWithContext("Java bounded generic") { + val FilesModuleClass = ctx.findTopLevelModuleClass("java.nio.file.Files") + + val readAttributes = FilesModuleClass + .findAllOverloadedDecls(termName("readAttributes")) + .find(_.declaredType.isInstanceOf[PolyType]) + .get + assertSigned( + readAttributes, + "(1,java.nio.file.Path,java.lang.Class,java.nio.file.LinkOption[]):java.nio.file.attribute.BasicFileAttributes" + ) + } + testWithContext("RichInt") { val RichInt = ctx.findTopLevelClass("scala.runtime.RichInt") @@ -157,6 +170,28 @@ class SignatureSuite extends UnrestrictedUnpicklingSuite: assertSigned(withArrayExactAnyVal, "(java.lang.Object[]):scala.Unit") } + testWithContext("unit-erasure") { + val UnitErasureClass = ctx.findTopLevelClass("simple_trees.UnitErasure") + + val unitVal = UnitErasureClass.findNonOverloadedDecl(termName("unitVal")) + assertNotSigned(unitVal, "():scala.Unit") + + val unitVar = UnitErasureClass.findNonOverloadedDecl(termName("unitVar")) + assertNotSigned(unitVar, "():scala.Unit") + + val unitVarSetter = UnitErasureClass.findNonOverloadedDecl(termName("unitVar_=")) + assertSigned(unitVarSetter, "(scala.runtime.BoxedUnit):scala.Unit") + + val unitParamelessDef = UnitErasureClass.findNonOverloadedDecl(termName("unitParamelessDef")) + assertNotSigned(unitParamelessDef, "():scala.Unit") + + val unitResult = UnitErasureClass.findNonOverloadedDecl(termName("unitResult")) + assertSigned(unitResult, "():scala.Unit") + + val unitParam = UnitErasureClass.findNonOverloadedDecl(termName("unitParam")) + assertSigned(unitParam, "(scala.runtime.BoxedUnit):java.lang.Object") + } + testWithContext("type-member") { val TypeMember = ctx.findTopLevelClass("simple_trees.TypeMember") @@ -295,6 +330,18 @@ class SignatureSuite extends UnrestrictedUnpicklingSuite: val arrayOfUnion = UnionType.findNonOverloadedDecl(name"arrayOfUnion") assertSigned(arrayOfUnion, "(java.lang.Object[]):java.lang.Object[]") + + val unitOrNull = UnionType.findNonOverloadedDecl(termName("unitOrNull")) + assertSigned(unitOrNull, "(scala.runtime.BoxedUnit):scala.runtime.BoxedUnit") + + val intOrNull = UnionType.findNonOverloadedDecl(termName("intOrNull")) + assertSigned(intOrNull, "(java.lang.Object):java.lang.Object") + + val optionOrNull = UnionType.findNonOverloadedDecl(termName("optionOrNull")) + assertSigned(optionOrNull, "(scala.Option):scala.Option") + + val optionOrUnit = UnionType.findNonOverloadedDecl(termName("optionOrUnit")) + assertSigned(optionOrUnit, "(java.lang.Object):java.lang.Object") } testWithContext("refined types") { @@ -352,10 +399,18 @@ class SignatureSuite extends UnrestrictedUnpicklingSuite: assertSigned(takeNonEmptyTupleSig, "(scala.Product):scala.Unit") val takeStarColonSig = TuplesClass.findNonOverloadedDecl(termName("takeStarColon")) - assertSigned(takeStarColonSig, "(java.lang.Object):scala.Unit") + assertSigned(takeStarColonSig, "(scala.Product):scala.Unit") val takeEmptyTupleSig = TuplesClass.findNonOverloadedDecl(termName("takeEmptyTuple")) assertSigned(takeEmptyTupleSig, "(scala.Tuple$package.EmptyTuple):scala.Unit") + + val TupleClass = ctx.findTopLevelClass("scala.Tuple") + + val colonStar = TupleClass.findNonOverloadedDecl(termName(":*")) + assertSigned(colonStar, "(2,java.lang.Object):scala.Product") + + val starColon = TupleClass.findNonOverloadedDecl(termName("*:")) + assertSigned(starColon, "(2,java.lang.Object):scala.Product") } testWithContext("local object") { diff --git a/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala index 515f5e2f..e71bbd5a 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/SubtypingSuite.scala @@ -1119,6 +1119,21 @@ class SubtypingSuite extends UnrestrictedUnpicklingSuite: ) } + testWithContext("class-type-constructors") { + val GenIterableClass = ctx.findTopLevelClass("scala.collection.Iterable") + val ImmutableIterableClass = ctx.findTopLevelClass("scala.collection.immutable.Iterable") + val MutableIterableClass = ctx.findTopLevelClass("scala.collection.mutable.Iterable") + + // No withRef's because these are not proper types + + assertEquiv(GenIterableClass.staticRef, GenIterableClass.newStaticRef) + + assertStrictSubtype(ImmutableIterableClass.staticRef, GenIterableClass.staticRef) + assertStrictSubtype(MutableIterableClass.staticRef, GenIterableClass.staticRef) + + assertNeitherSubtype(ImmutableIterableClass.staticRef, MutableIterableClass.staticRef) + } + testWithContext("annotated-types") { import scala.annotation.unchecked.uncheckedVariance as uV diff --git a/tasty-query/shared/src/test/scala/tastyquery/SymbolSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/SymbolSuite.scala index a0d2c75b..36e3c7b2 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/SymbolSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/SymbolSuite.scala @@ -17,7 +17,7 @@ class SymbolSuite extends RestrictedUnpicklingSuite { /** Needed for correct resolving of ctor signatures */ val fundamentalClasses: Seq[String] = - Seq("java.lang.Object", "scala.Unit", "scala.AnyVal", "scala.annotation.targetName") + Seq("java.lang.Object", "scala.Unit", "scala.AnyVal", "scala.annotation.targetName", "scala.runtime.BoxedUnit") def testWithContext(name: String, rootSymbolPath: String, extraRootSymbolPaths: String*)(using munit.Location)( body: Context ?=> Unit diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala index d2811348..37e9f8e7 100644 --- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala +++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala @@ -4,14 +4,15 @@ import scala.collection.mutable import tastyquery.Constants.* import tastyquery.Contexts.* +import tastyquery.Exceptions.* import tastyquery.Modifiers.* import tastyquery.Names.* import tastyquery.Symbols.* +import tastyquery.Traversers.TreeTraverser import tastyquery.Trees.* import tastyquery.Types.* import TestUtils.* -import tastyquery.Traversers.TreeTraverser class TypeSuite extends UnrestrictedUnpicklingSuite { extension [T](elems: List[T]) @@ -61,6 +62,17 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { false end isIntersectionOf + def isUnionOf(tpes: (Type => Boolean)*)(using Context): Boolean = + tpe match + case tpe: Type => + def parts(tpe: Type): List[Type] = tpe match + case tpe: OrType => parts(tpe.first) ::: parts(tpe.second) + case _ => tpe :: Nil + parts(tpe).isListOf(tpes*) + case _ => + false + end isUnionOf + def isApplied(cls: Type => Boolean, argRefs: Seq[TypeOrWildcard => Boolean])(using Context): Boolean = tpe match case tpe: TermType => @@ -815,7 +827,7 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { val tpe = refInterface.declaredType.asInstanceOf[TypeLambdaType] val List(tparamRefA) = tpe.paramRefs: @unchecked assert( - tparamRefA.bounds.high.isIntersectionOf(_.isFromJavaObject, _.isRef(JavaInterface1), _.isRef(JavaInterface2)), + tparamRefA.bounds.high.isIntersectionOf(_.isRef(JavaInterface1), _.isRef(JavaInterface2)), clues(tparamRefA.bounds) ) } @@ -3012,4 +3024,97 @@ class TypeSuite extends UnrestrictedUnpicklingSuite { val fooSym = SingletonClassTypeClass.findDecl(termName("foo")) assert(clue(fooSym.declaredType).isTypeRefOf(defn.SingletonClass)) } + + testWithContext("join") { + val UnionTypeJoinClass = ctx.findTopLevelModuleClass("simple_trees.UnionTypeJoin") + + val aClass = UnionTypeJoinClass.findDecl(typeName("A")).asClass + val bClass = UnionTypeJoinClass.findDecl(typeName("B")).asClass + val cClass = UnionTypeJoinClass.findDecl(typeName("C")).asClass + val dClass = UnionTypeJoinClass.findDecl(typeName("D")).asClass + val eClass = UnionTypeJoinClass.findDecl(typeName("E")).asClass + + // Spec says that join(A | B) = C[A | B] & D + val orType = OrType(aClass.staticRef, bClass.staticRef) + assert( + clue(orType.join).isIntersectionOf( + _.isApplied(_.isRef(cClass), List(_.isUnionOf(_.isRef(aClass), _.isRef(bClass)))), + _.isRef(dClass) + ) + ) + } + + testWithContext("all-symbol-resolutions") { + /* Test that we can resolve the `.symbol` of all the `Ident`s and `Select`s + * within the test-sources. + */ + + var successCount = 0 + val errorsBuilder = List.newBuilder[String] + + def testSelect(tree: TermReferenceTree): Unit = + try + tree.symbol + tree.referenceType match + case tpe: NamedType if tpe.prefix != NoPrefix => + successCount += 1 + case _ => + // too "easy"; don't count that as a success + () + catch + case ex: MemberNotFoundException => + val displayPos = + if tree.pos.hasLineColumnInformation then s":${tree.pos.pointLine}:${tree.pos.pointColumn}" + else "" + val prefixDetails = ex.prefix match + case prefix: SingletonType => s" (${prefix.showBasic}: ${prefix.superType.showBasic})" + case prefix: Type => s" (${prefix.showBasic})" + case _ => "" + + errorsBuilder += s"${tree.pos.sourceFile.path}$displayPos: ${ex.getMessage()}$prefixDetails" + end testSelect + + def walkTree(tree: Tree): Unit = + new Traversers.TreeTraverser { + override def traverse(tree: Tree): Unit = tree match + case tree: TermReferenceTree => + super.traverse(tree) + testSelect(tree) + case _ => + super.traverse(tree) + }.traverse(tree) + end walkTree + + def walk(pkg: PackageSymbol): Unit = + for sym <- pkg.declarations do + sym match + case sym: PackageSymbol => walk(sym) + case sym: ClassSymbol => sym.tree.foreach(walkTree(_)) + case _ => () + end walk + + val testSourcesPackages = List( + "companions", + "crosspackagetasty", + "empty_class", + "imported_files", + "imports", + "inheritance", + "javacompat", + "javadefined", + "mixjavascala", + "scala2compat", + "simple_trees", + "subtyping", + "synthetics" + ) + for testSourcesPackage <- testSourcesPackages do walk(ctx.findPackage(testSourcesPackage)) + + val errors = errorsBuilder.result() + if errors.nonEmpty then + fail(errors.mkString("Could not resolved the `symbol` of some trees in the test-sources:\n", "\n", "\n")) + + // As of this writing, there were 1201 successes + assert(clue(successCount) > 1000) + } } diff --git a/test-sources/src/main/scala/simple_trees/UnionType.scala b/test-sources/src/main/scala/simple_trees/UnionType.scala index 3c99e800..897f4780 100644 --- a/test-sources/src/main/scala/simple_trees/UnionType.scala +++ b/test-sources/src/main/scala/simple_trees/UnionType.scala @@ -6,4 +6,21 @@ class UnionType { def classesOrType(x: List[Int] | Vector[String]): Seq[Int | String] = x def arrayOfUnion(x: Array[AnyRef | Null]): Array[AnyRef | Null] = x + + def unitOrNull(x: Unit | Null): Unit | Null = x + + def intOrNull(x: Int | Null): Int | Null = x + + def optionOrNull(x: Option[Int] | Null): Option[Int] | Null = x + + def optionOrUnit(x: Option[Int] | Unit): Option[Int] | Unit = x + + def calls(): Unit = + argWithOrType(5) + classesOrType(Nil) + arrayOfUnion(Array()) + unitOrNull(()) + intOrNull(5) + optionOrNull(None) + optionOrUnit(()) } diff --git a/test-sources/src/main/scala/simple_trees/UnionTypeJoin.scala b/test-sources/src/main/scala/simple_trees/UnionTypeJoin.scala new file mode 100644 index 00000000..67a33665 --- /dev/null +++ b/test-sources/src/main/scala/simple_trees/UnionTypeJoin.scala @@ -0,0 +1,9 @@ +package simple_trees + +object UnionTypeJoin: + trait C[+T] + trait D + trait E + class A extends C[A] with D + class B extends C[B] with D with E +end UnionTypeJoin diff --git a/test-sources/src/main/scala/simple_trees/UnitErasure.scala b/test-sources/src/main/scala/simple_trees/UnitErasure.scala new file mode 100644 index 00000000..2d8836a1 --- /dev/null +++ b/test-sources/src/main/scala/simple_trees/UnitErasure.scala @@ -0,0 +1,22 @@ +package simple_trees + +class UnitErasure: + val unitVal: Unit = () + + var unitVar: Unit = () + + def unitParamelessDef: Unit = () + + def unitResult(): Unit = () + + def unitParam(x: Unit): Any = x + + def calls(): Unit = + val _ = unitVal + val _ = unitVar + unitVar = () + unitParamelessDef + unitResult() + unitParam(()) + end calls +end UnitErasure