diff --git a/build.sbt b/build.sbt index ad87e4f7f4a8..fb7336bc0f91 100644 --- a/build.sbt +++ b/build.sbt @@ -1349,7 +1349,8 @@ lazy val root: Project = (project in file(".")) .aggregate(library, reflect, compiler, interactive, repl, replFrontend, scaladoc, scalap, testkit, partest, junit, scalaDist).settings( Compile / sources := Seq.empty, - onLoadMessage := """|*** Welcome to the sbt build definition for Scala! *** + onLoadMessage := s"""|*** Welcome to the sbt build definition for Scala! *** + |version=${(Global / version).value} scalaVersion=${(Global / scalaVersion).value} |Check README.md for more information.""".stripMargin ) diff --git a/project/ScriptCommands.scala b/project/ScriptCommands.scala index 24cb727a0df3..156a40dbd72a 100644 --- a/project/ScriptCommands.scala +++ b/project/ScriptCommands.scala @@ -4,8 +4,10 @@ import java.nio.file.Paths import sbt._ import Keys._ +import sbt.complete.Parsers._ import BuildSettings.autoImport._ +import VersionUtil._ /** Custom commands for use by the Jenkins scripts. This keeps the surface area and call syntax small. */ object ScriptCommands { @@ -16,7 +18,8 @@ object ScriptCommands { setupPublishCore, setupValidateTest, setupBootstrapStarr, setupBootstrapLocker, setupBootstrapQuick, setupBootstrapPublish, - enableOptimizerCommand + enableOptimizerCommand, + restarr, restarrFull, ) /** Set up the environment for `validate/publish-core`. @@ -110,6 +113,34 @@ object ScriptCommands { def enableOptimizerCommand = setup("enableOptimizer")(_ => enableOptimizer) + /** For local dev: sets `scalaVersion` to the version in `/buildcharacter.properties` or the given arg. + * Running `reload` will re-read the build files, resetting `scalaVersion`. */ + def restarr = Command("restarr")(_ => (Space ~> StringBasic).?) { (state, s) => + val newVersion = s.getOrElse(readVersionFromPropsFile(state)) + val x = Project.extract(state) + val sv = x.get(Global / scalaVersion) + state.log.info(s"Re-STARR'ing: setting scalaVersion from $sv to $newVersion (`reload` to undo)") + x.appendWithSession(Global / scalaVersion := newVersion, state) // don't use version.value or it'll be a wrong, new value + } + + /** For local dev: publishes locally (without optimizing) & then sets the new `scalaVersion`. + * Also it generates `/buildcharacter.properties` which is the default used by `restarr`. */ + def restarrFull = Command.command("restarrFull") { state => + setupPublishCoreNonOpt.nameOption.get :: + generateBuildCharacterPropertiesFile.key.label :: + publishLocal.key.label :: + restarr.nameOption.get :: + state + } + + private def readVersionFromPropsFile(state: State): String = { + val props = readProps(file("buildcharacter.properties")) + val newVersion = props("maven.version.number") + val fullVersion = props("version.number") + state.log.info(s"Read STARR version from buildcharacter.properties: $newVersion (full version: $fullVersion)") + newVersion + } + private[this] def setup(name: String)(f: Seq[String] => Seq[Setting[_]]) = Command.args(name, name) { case (state, seq) => Project.extract(state).appendWithSession(f(seq), state) } diff --git a/project/VersionUtil.scala b/project/VersionUtil.scala index a3d3d0e46a90..6b4e659cc7a2 100644 --- a/project/VersionUtil.scala +++ b/project/VersionUtil.scala @@ -183,14 +183,17 @@ object VersionUtil { propFile } - /** The global versions.properties data */ - lazy val versionProps: Map[String, String] = { + private[build] def readProps(f: File): Map[String, String] = { val props = new Properties() - val in = new FileInputStream(file("versions.properties")) + val in = new FileInputStream(f) try props.load(in) finally in.close() - props.asScala.toMap.map { - case (k, v) => (k, sys.props.getOrElse(k, v)) // allow system properties to override versions.properties - } + props.asScala.toMap + } + + /** The global versions.properties data */ + lazy val versionProps: Map[String, String] = { + val versionProps = readProps(file("versions.properties")) + versionProps.map { case (k, v) => (k, sys.props.getOrElse(k, v)) } // allow sys props to override versions.properties } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index 28205aca4669..96197ee1710d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -676,15 +676,16 @@ abstract class BTypes { } def innerClassAttributeEntry: Either[NoClassBTypeInfo, Option[InnerClassEntry]] = info.map(i => i.nestedInfo.force map { - case NestedInfo(_, outerName, innerName, isStaticNestedClass) => + case NestedInfo(_, outerName, innerName, isStaticNestedClass, enteringTyperPrivate) => + // the static flag in the InnerClass table has a special meaning, see InnerClass comment + def adjustStatic(flags: Int): Int = ( flags & ~Opcodes.ACC_STATIC | + (if (isStaticNestedClass) Opcodes.ACC_STATIC else 0) + ) & BCodeHelpers.INNER_CLASSES_FLAGS InnerClassEntry( internalName, outerName.orNull, innerName.orNull, - // the static flag in the InnerClass table has a special meaning, see InnerClass comment - ( i.flags & ~Opcodes.ACC_STATIC | - (if (isStaticNestedClass) Opcodes.ACC_STATIC else 0) - ) & BCodeHelpers.INNER_CLASSES_FLAGS + flags = adjustStatic(if (enteringTyperPrivate) (i.flags & ~Opcodes.ACC_PUBLIC) | Opcodes.ACC_PRIVATE else i.flags) ) }) @@ -876,7 +877,8 @@ abstract class BTypes { final case class NestedInfo(enclosingClass: ClassBType, outerName: Option[String], innerName: Option[String], - isStaticNestedClass: Boolean) + isStaticNestedClass: Boolean, + enteringTyperPrivate: Boolean) /** * This class holds the data for an entry in the InnerClass table. See the InnerClass summary diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromClassfile.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromClassfile.scala index 4397533a5ac5..6b22a6ada8e6 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromClassfile.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromClassfile.scala @@ -130,7 +130,8 @@ abstract class BTypesFromClassfile { classBTypeFromParsedClassfile(classNode.outerClass) } val staticFlag = (innerEntry.access & Opcodes.ACC_STATIC) != 0 - NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) + NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag, + (flags & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE) } val inlineInfo = inlineInfoFromClassfile(classNode) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 597fd4f9fdf1..544c43fd386c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -503,7 +503,7 @@ abstract class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { else Some(s"${innerClassSym.rawname}${innerClassSym.moduleSuffix}") // moduleSuffix for module classes } - Some(NestedInfo(enclosingClass, outerName, innerName, isStaticNestedClass)) + Some(NestedInfo(enclosingClass, outerName, innerName, isStaticNestedClass, enteringTyper(innerClassSym.isPrivate))) } /** diff --git a/src/compiler/scala/tools/nsc/backend/jvm/PostProcessor.scala b/src/compiler/scala/tools/nsc/backend/jvm/PostProcessor.scala index 64977797366d..6eb6c9db51fb 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/PostProcessor.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/PostProcessor.scala @@ -133,7 +133,8 @@ abstract class PostProcessor extends PerRunInit { def setInnerClasses(classNode: ClassNode): Unit = { classNode.innerClasses.clear() - backendUtils.addInnerClasses(classNode, backendUtils.collectNestedClasses(classNode)) + val (declared, referred) = backendUtils.collectNestedClasses(classNode) + backendUtils.addInnerClasses(classNode, declared, referred) } def serializeClass(classNode: ClassNode): Array[Byte] = { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index 941ef2426aac..e42a047c683e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -413,11 +413,12 @@ abstract class BackendUtils extends PerRunInit { } /** * Visit the class node and collect all referenced nested classes. + * @return (declaredInnerClasses, referredInnerClasses) */ - def collectNestedClasses(classNode: ClassNode): List[ClassBType] = { + def collectNestedClasses(classNode: ClassNode): (List[ClassBType], List[ClassBType]) = { val c = new Collector c.visit(classNode) - c.innerClasses.toList + (c.declaredInnerClasses.toList, c.referredInnerClasses.toList) } /* @@ -432,11 +433,13 @@ abstract class BackendUtils extends PerRunInit { * * can-multi-thread */ - final def addInnerClasses(jclass: asm.ClassVisitor, refedInnerClasses: List[ClassBType]): Unit = { - val allNestedClasses = refedInnerClasses.flatMap(_.enclosingNestedClassesChain.get).distinct - + final def addInnerClasses(jclass: asm.tree.ClassNode, declaredInnerClasses: List[ClassBType], refedInnerClasses: List[ClassBType]): Unit = { // sorting ensures nested classes are listed after their enclosing class thus satisfying the Eclipse Java compiler - for (nestedClass <- allNestedClasses.sortBy(_.internalName.toString)) { + val allNestedClasses = new mutable.TreeSet[ClassBType]()(Ordering.by(_.internalName)) + allNestedClasses ++= declaredInnerClasses + refedInnerClasses.foreach(_.enclosingNestedClassesChain.get.foreach(allNestedClasses += _)) + + for (nestedClass <- allNestedClasses) { // Extract the innerClassEntry - we know it exists, enclosingNestedClassesChain only returns nested classes. val Some(e) = nestedClass.innerClassAttributeEntry.get jclass.visitInnerClass(e.name, e.outerName, e.innerName, e.flags) @@ -783,7 +786,15 @@ object BackendUtils { } abstract class NestedClassesCollector[T](nestedOnly: Boolean) extends GenericSignatureVisitor(nestedOnly) { - val innerClasses = mutable.Set.empty[T] + + val declaredInnerClasses = mutable.Set.empty[T] + val referredInnerClasses = mutable.Set.empty[T] + + def innerClasses: collection.Set[T] = declaredInnerClasses ++ referredInnerClasses + def clear(): Unit = { + declaredInnerClasses.clear() + referredInnerClasses.clear() + } def declaredNestedClasses(internalName: InternalName): List[T] @@ -791,7 +802,7 @@ object BackendUtils { def visit(classNode: ClassNode): Unit = { visitInternalName(classNode.name) - innerClasses ++= declaredNestedClasses(classNode.name) + declaredInnerClasses ++= declaredNestedClasses(classNode.name) visitInternalName(classNode.superName) classNode.interfaces.asScala foreach visitInternalName @@ -850,7 +861,8 @@ object BackendUtils { def visitInternalName(internalName: String, offset: Int, length: Int): Unit = if (internalName != null && containsChar(internalName, offset, length, '$')) { for (c <- getClassIfNested(internalName.substring(offset, length))) - innerClasses += c + if (!declaredInnerClasses.contains(c)) + referredInnerClasses += c } // either an internal/Name or [[Linternal/Name; -- there are certain references in classfiles diff --git a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala index 38b2125e32c6..01f422386529 100644 --- a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala +++ b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala @@ -383,100 +383,61 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { * ElementValueList ::= ElementValue {`,` ElementValue} */ def annotation(): Tree = { - def annArg(): Tree = { - def elementValue(): Tree = { - tryLiteral() match { - case Some(lit) => atPos(in.currentPos)(Literal(lit)) - case _ if in.token == AT => - in.nextToken() - annotation() - case _ if in.token == LBRACE => - atPos(in.pos) { - in.nextToken() - val ts = new ListBuffer[Tree] - if (in.token == RBRACE) - Apply(ArrayModule_overloadedApply) - else { - var bailout = false - elementValue() match { - case EmptyTree => bailout = true - case t => ts += t - } - while (in.token == COMMA && !bailout) { - in.nextToken() - if (in.token == RBRACE) { - // trailing comma - } else { - elementValue() match { - case EmptyTree => bailout = true - case t => ts += t - } - } - } - if (!bailout && in.token != RBRACE) { - bailout = true - } - if (bailout) { - var braceDepth = 1 - while (braceDepth > 0) { - in.nextToken() - in.token match { - case LBRACE => braceDepth += 1 - case RBRACE => braceDepth -= 1 - case _ => - } - } - EmptyTree - } else { - accept(RBRACE) - Apply(ArrayModule_overloadedApply, ts.toList: _*) - } - } - } - case _ if in.token == IDENTIFIER => - qualId(orClassLiteral = true) - case _ => - in.nextToken() - EmptyTree - } + object LiteralK { def unapply(token: Token) = tryLiteral() } + + def elementValue(): Tree = in.token match { + case LiteralK(k) => in.nextToken(); atPos(in.currentPos)(Literal(k)) + case IDENTIFIER => qualId(orClassLiteral = true) + case LBRACE => accept(LBRACE); elementArray() + case AT => accept(AT); annotation() + case _ => in.nextToken(); EmptyTree + } + + def elementArray(): Tree = atPos(in.pos) { + val ts = new ListBuffer[Tree] + while (in.token != RBRACE) { + ts += elementValue() + if (in.token == COMMA) in.nextToken() // done this way trailing commas are supported } + val ok = !ts.contains(EmptyTree) + in.token match { + case RBRACE if ok => accept(RBRACE); Apply(ArrayModule_overloadedApply, ts.toList: _*) + case _ => skipTo(RBRACE); EmptyTree + } + } - if (in.token == IDENTIFIER) { - qualId(orClassLiteral = true) match { - case name: Ident if in.token == EQUALS => - in.nextToken() - /* name = value */ - val value = elementValue() - if (value.isEmpty) EmptyTree else gen.mkNamedArg(name, value) - case rhs => - /* implicit `value` arg with constant value */ - gen.mkNamedArg(nme.value, rhs) + // 1) name = value + // 2) implicit `value` arg with constant value + // 3) implicit `value` arg + def annArg(): Tree = { + def mkNamedArg(name: Ident, value: Tree) = if (value.isEmpty) EmptyTree else gen.mkNamedArg(name, value) + in.token match { + case IDENTIFIER => qualId(orClassLiteral = true) match { + case name: Ident if in.token == EQUALS => accept(EQUALS); mkNamedArg(name, elementValue()) + case rhs => mkNamedArg(Ident(nme.value), rhs) } - } else { - /* implicit `value` arg */ - val value = elementValue() - if (value.isEmpty) EmptyTree else gen.mkNamedArg(nme.value, value) + case _ => mkNamedArg(Ident(nme.value), elementValue()) } } atPos(in.pos) { val id = convertToTypeId(qualId()) - if (in.token == LPAREN) { - val saved = new JavaTokenData {}.copyFrom(in) // prep to bail if non-literals/identifiers - accept(LPAREN) - val args = - if (in.token == RPAREN) Nil - else commaSeparated(atPos(in.pos)(annArg())) - if (in.token == RPAREN) { - accept(RPAREN) - New(id, args :: Nil) - } else { - in.copyFrom(saved) - skipAhead() - accept(RPAREN) - EmptyTree - } - } else New(id, ListOfNil) + in.token match { + case LPAREN => + // TODO: fix copyFrom+skipAhead; CharArrayReaderData missing + val saved = new JavaTokenData {}.copyFrom(in) // prep to bail if non-literals/identifiers + accept(LPAREN) + val args = in.token match { + case RPAREN => Nil + case _ => commaSeparated(atPos(in.pos)(annArg())) + } + val ok = !args.contains(EmptyTree) + in.token match { + case RPAREN if ok => accept(RPAREN); New(id, List(args)) + case _ => in.copyFrom(saved); skipAhead(); accept(RPAREN); EmptyTree + } + case _ => New(id, ListOfNil) + } } } @@ -720,6 +681,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { def constantTpe(const: Constant): Tree = TypeTree(ConstantType(const)) def forConst(const: Constant): Tree = { + in.nextToken() if (in.token != SEMI) tpt1 else { def isStringTyped = tpt1 match { @@ -1015,10 +977,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners { case _ => null } if (l == null) None - else { - in.nextToken() - Some(Constant(l)) - } + else Some(Constant(l)) } /** CompilationUnit ::= [package QualId semi] TopStatSeq diff --git a/src/compiler/scala/tools/nsc/transform/Erasure.scala b/src/compiler/scala/tools/nsc/transform/Erasure.scala index 6ed7a9717367..00afc503f5e8 100644 --- a/src/compiler/scala/tools/nsc/transform/Erasure.scala +++ b/src/compiler/scala/tools/nsc/transform/Erasure.scala @@ -1164,14 +1164,13 @@ abstract class Erasure extends InfoTransform } qual.tpe.typeSymbol match { - case UnitClass | NullClass => LIT(0) - case IntClass => qual - case s @ (ShortClass | ByteClass | CharClass) => numericConversion(qual, s) - case BooleanClass => If(qual, LIT(true.##), LIT(false.##)) - case LongClass => staticsCall(nme.longHash) - case FloatClass => staticsCall(nme.floatHash) - case DoubleClass => staticsCall(nme.doubleHash) - case _ => staticsCall(nme.anyHash) + case UnitClass | NullClass => LIT(0) + case IntClass | ShortClass | ByteClass | CharClass => qual + case BooleanClass => If(qual, LIT(true.##), LIT(false.##)) + case LongClass => staticsCall(nme.longHash) + case FloatClass => staticsCall(nme.floatHash) + case DoubleClass => staticsCall(nme.doubleHash) + case _ => staticsCall(nme.anyHash) } } else if (isPrimitiveValueClass(qual.tpe.typeSymbol)) { // Rewrite 5.getClass to ScalaRunTime.anyValClass(5) diff --git a/src/compiler/scala/tools/nsc/transform/async/LiveVariables.scala b/src/compiler/scala/tools/nsc/transform/async/LiveVariables.scala index 2623eb5e893d..b4d8de489fc8 100644 --- a/src/compiler/scala/tools/nsc/transform/async/LiveVariables.scala +++ b/src/compiler/scala/tools/nsc/transform/async/LiveVariables.scala @@ -30,7 +30,7 @@ trait LiveVariables extends ExprBuilder { def fieldsToNullOut(asyncStates: List[AsyncState], finalState: AsyncState, liftables: List[Tree]): mutable.LinkedHashMap[Int, (mutable.LinkedHashSet[Symbol], mutable.LinkedHashSet[Symbol])] = { - val liftedSyms = mutable.HashSet[Symbol]() + val liftedSyms = mutable.LinkedHashSet[Symbol]() // include only vars liftedSyms ++= liftables.iterator.collect { @@ -54,8 +54,8 @@ trait LiveVariables extends ExprBuilder { */ def fieldsUsedIn(as: AsyncState): (collection.Set[Symbol], collection.Set[Symbol]) = { class FindUseTraverser extends AsyncTraverser { - val usedBeforeAssignment = new mutable.HashSet[Symbol]() - val assignedFields = new mutable.HashSet[Symbol]() + val usedBeforeAssignment = new mutable.LinkedHashSet[Symbol]() + val assignedFields = new mutable.LinkedHashSet[Symbol]() private def capturing[A](body: => A): A = { val saved = capturing try { diff --git a/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala b/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala index 1b86cd32d83e..29087b617062 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala @@ -411,10 +411,9 @@ trait Logic extends Debugging { object gatherEqualities extends PropTraverser { override def apply(p: Prop) = p match { - case Eq(v, c) => - vars += v - v.registerEquality(c) - case _ => super.apply(p) + case Eq(v, NullConst) if !modelNull => vars += v // not modeling equality to null + case Eq(v, c) => vars += v; v.registerEquality(c) + case _ => super.apply(p) } } diff --git a/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala b/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala index 47a78a5d0ca4..8527e03fe492 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala @@ -188,9 +188,10 @@ trait TreeAndTypeAnalysis extends Debugging { // a type is "uncheckable" (for exhaustivity) if we don't statically know its subtypes (i.e., it's unsealed) // we consider tuple types with at least one component of a checkable type as a checkable type def uncheckableType(tp: Type): Boolean = { - val checkable = ( - (isTupleType(tp) && tupleComponents(tp).exists(tp => !uncheckableType(tp))) - || enumerateSubtypes(tp, grouped = false).nonEmpty) + val checkable = { + if (isTupleType(tp)) tupleComponents(tp).exists(tp => !uncheckableType(tp)) + else enumerateSubtypes(tp, grouped = false).nonEmpty + } // if (!checkable) debug.patmat("deemed uncheckable: "+ tp) !checkable } @@ -221,10 +222,6 @@ trait MatchApproximation extends TreeAndTypeAnalysis with ScalaLogic with MatchT override def toString = s"T${id}C($prop)" } - class TreeMakersToPropsIgnoreNullChecks(root: Symbol) extends TreeMakersToProps(root) { - override def uniqueNonNullProp(p: Tree): Prop = True - } - // returns (tree, tests), where `tree` will be used to refer to `root` in `tests` class TreeMakersToProps(val root: Symbol) { prepareNewAnalysis() // reset hash consing for Var and Const @@ -236,7 +233,6 @@ trait MatchApproximation extends TreeAndTypeAnalysis with ScalaLogic with MatchT def uniqueEqualityProp(testedPath: Tree, rhs: Tree): Prop = uniqueEqualityProps.getOrElseUpdate((testedPath, rhs), Eq(Var(testedPath), ValueConst(rhs))) - // overridden in TreeMakersToPropsIgnoreNullChecks def uniqueNonNullProp (testedPath: Tree): Prop = uniqueNonNullProps.getOrElseUpdate(testedPath, Not(Eq(Var(testedPath), NullConst))) @@ -513,7 +509,7 @@ trait MatchAnalysis extends MatchApproximation { val start = if (StatisticsStatics.areSomeColdStatsEnabled) statistics.startTimer(statistics.patmatAnaExhaust) else null var backoff = false - val approx = new TreeMakersToPropsIgnoreNullChecks(prevBinder) + val approx = new TreeMakersToProps(prevBinder) val symbolicCases = approx.approximateMatch(cases, approx.onUnknown { tm => approx.fullRewrite.applyOrElse[TreeMaker, Prop](tm, { case BodyTreeMaker(_, _) => True // irrelevant -- will be discarded by symbolCase later diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 97422f02b076..8f162eb90fec 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -3948,6 +3948,10 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper */ @tailrec def tree2ConstArg(tree: Tree, pt: Type): Option[ClassfileAnnotArg] = tree match { + case Apply(Select(New(_), nme.CONSTRUCTOR), _) if pt.typeSymbol == ArrayClass && unit.isJava => + // In Java, a single value may be passed for array annotation parameters + tree2ConstArg(Apply(Select(gen.mkAttributedRef(ArrayModule), nme.apply), List(tree)), pt) + case Apply(Select(New(_), nme.CONSTRUCTOR), _) if pt.typeSymbol == ArrayClass => reportAnnotationError(ArrayConstantsError(tree)); None @@ -3956,7 +3960,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper val annType = annInfo.atp if (!annType.typeSymbol.isSubClass(pt.typeSymbol)) - reportAnnotationError(AnnotationTypeMismatchError(tpt, annType, annType)) + reportAnnotationError(AnnotationTypeMismatchError(tpt, pt, annType)) else if (!annType.typeSymbol.isJavaDefined) reportAnnotationError(NestedAnnotationError(ann, annType)) diff --git a/src/partest/scala/tools/partest/nest/Runner.scala b/src/partest/scala/tools/partest/nest/Runner.scala index def13edc3ad2..db9db83f2a44 100644 --- a/src/partest/scala/tools/partest/nest/Runner.scala +++ b/src/partest/scala/tools/partest/nest/Runner.scala @@ -422,7 +422,7 @@ class Runner(val testInfo: TestInfo, val suiteRunner: AbstractRunner) { val bestDiff = if (!checkFile.canRead) diff else - gitRunner.flatMap(_ => withTempFile(outFile, fileBase, filteredCheck)(f => + gitRunner.flatMap(_ => withTempFile(outDir, fileBase, filteredCheck)(f => gitDiff(f, logFile))).getOrElse(diff) _transcript append bestDiff genFail("output differs") diff --git a/src/reflect/scala/reflect/internal/Symbols.scala b/src/reflect/scala/reflect/internal/Symbols.scala index dddaf91aa6b4..32c345b9f9d8 100644 --- a/src/reflect/scala/reflect/internal/Symbols.scala +++ b/src/reflect/scala/reflect/internal/Symbols.scala @@ -1867,6 +1867,7 @@ trait Symbols extends api.Symbols { self: SymbolTable => info match { case ci @ ClassInfoType(_, _, _) => setInfo(ci.copy(parents = ci.parents :+ SerializableTpe)) + invalidateCaches(ci.typeSymbol.typeOfThis, ci.typeSymbol :: Nil) case i => abort("Only ClassInfoTypes can be made serializable: "+ i) } diff --git a/src/reflect/scala/reflect/internal/Types.scala b/src/reflect/scala/reflect/internal/Types.scala index 64cfe7308cf2..22757675c6df 100644 --- a/src/reflect/scala/reflect/internal/Types.scala +++ b/src/reflect/scala/reflect/internal/Types.scala @@ -5267,11 +5267,15 @@ trait Types invalidateCaches(tp, updatedSyms) } - def invalidateCaches(t: Type, updatedSyms: List[Symbol]) = + def invalidateCaches(t: Type, updatedSyms: List[Symbol]): Unit = t match { - case st: SingleType if updatedSyms.contains(st.sym) => st.invalidateSingleTypeCaches() case tr: TypeRef if updatedSyms.contains(tr.sym) => tr.invalidateTypeRefCaches() case ct: CompoundType if ct.baseClasses.exists(updatedSyms.contains) => ct.invalidatedCompoundTypeCaches() + case st: SingleType => + if (updatedSyms.contains(st.sym)) st.invalidateSingleTypeCaches() + val underlying = st.underlying + if (underlying ne st) + invalidateCaches(underlying, updatedSyms) case _ => } diff --git a/src/scaladoc/scala/tools/nsc/ScalaDoc.scala b/src/scaladoc/scala/tools/nsc/ScalaDoc.scala index cf35c77c68f9..644d0b839ed2 100644 --- a/src/scaladoc/scala/tools/nsc/ScalaDoc.scala +++ b/src/scaladoc/scala/tools/nsc/ScalaDoc.scala @@ -14,6 +14,7 @@ package scala.tools.nsc import scala.tools.nsc.doc.DocFactory import scala.tools.nsc.reporters.ConsoleReporter +import scala.tools.nsc.settings.DefaultPathFactory import scala.reflect.internal.util.{FakePos, Position} /** The main class for scaladoc, a front-end for the Scala compiler @@ -25,7 +26,8 @@ class ScalaDoc { def process(args: Array[String]): Boolean = { var reporter: ScalaDocReporter = null val docSettings = new doc.Settings(msg => reporter.error(FakePos("scaladoc"), msg + "\n scaladoc -help gives more information"), - msg => reporter.echo(msg)) + msg => reporter.echo(msg), + DefaultPathFactory) reporter = new ScalaDocReporter(docSettings) val command = new ScalaDoc.Command(args.toList, docSettings) def hasFiles = command.files.nonEmpty || docSettings.uncompilableFiles.nonEmpty diff --git a/src/scaladoc/scala/tools/nsc/doc/Settings.scala b/src/scaladoc/scala/tools/nsc/doc/Settings.scala index 8d4264b3d0aa..e1372c11799e 100644 --- a/src/scaladoc/scala/tools/nsc/doc/Settings.scala +++ b/src/scaladoc/scala/tools/nsc/doc/Settings.scala @@ -21,6 +21,8 @@ import scala.tools.nsc.settings.{DefaultPathFactory, PathFactory} * @param error A function that prints a string to the appropriate error stream * @param printMsg A function that prints the string, without any extra boilerplate of error */ class Settings(error: String => Unit, val printMsg: String => Unit = println(_), pathFactory: PathFactory = DefaultPathFactory) extends scala.tools.nsc.Settings(error, pathFactory) { + // https://github.com/tkawachi/sbt-doctest depends on this constructor being available + def this(error: String => Unit, printMsg: String => Unit) = this(error, printMsg, DefaultPathFactory) // TODO 2.13 Remove private def removalIn213 = "This flag is scheduled for removal in 2.13. If you have a case where you need this flag then please report a bug." diff --git a/test/files/neg/t10019.check b/test/files/neg/t10019.check new file mode 100644 index 000000000000..3eb9db6bf2c3 --- /dev/null +++ b/test/files/neg/t10019.check @@ -0,0 +1,11 @@ +t10019.scala:5: warning: match may not be exhaustive. +It would fail on the following input: Foo(None) + def single(t: Foo): Nothing = t match { + ^ +t10019.scala:9: warning: match may not be exhaustive. +It would fail on the following input: (Foo(None), _) + def tuple(s: Foo, t: Foo): Nothing = (s, t) match { + ^ +error: No warnings can be incurred under -Werror. +2 warnings +1 error diff --git a/test/files/neg/t10019.scala b/test/files/neg/t10019.scala new file mode 100644 index 000000000000..9d9aac4e7ff3 --- /dev/null +++ b/test/files/neg/t10019.scala @@ -0,0 +1,12 @@ +// scalac: -Werror +object Bug { + sealed case class Foo(e: Option[Int]) + + def single(t: Foo): Nothing = t match { + case Foo(Some(_)) => ??? + } + + def tuple(s: Foo, t: Foo): Nothing = (s, t) match { + case (Foo(Some(_)), _) => ??? + } +} diff --git a/test/files/pos/case-object-add-serializable.scala b/test/files/pos/case-object-add-serializable.scala new file mode 100644 index 000000000000..7728649414a7 --- /dev/null +++ b/test/files/pos/case-object-add-serializable.scala @@ -0,0 +1,11 @@ +// scalac: -Xdev -Werror +// Was: "warning: !!! base trait Serializable not found in basetypes of object Person. This might indicate incorrect caching of TypeRef#parents." +// under -Xdev +class Test { + def apply = { + case class Person(name: String) + val x = Person("") + Person.getClass + x.name + } +} diff --git a/test/files/pos/t10373.scala b/test/files/pos/t10373.scala new file mode 100644 index 000000000000..fc285faef084 --- /dev/null +++ b/test/files/pos/t10373.scala @@ -0,0 +1,16 @@ +// scalac: -Werror +abstract class Foo { + def bar(): Unit = this match { + case Foo_1() => //do something + case Foo_2() => //do something + // Works fine + } + + def baz(that: Foo): Unit = (this, that) match { + case (Foo_1(), _) => //do something + case (Foo_2(), _) => //do something + // match may not be exhaustive + } +} +case class Foo_1() extends Foo +case class Foo_2() extends Foo diff --git a/test/files/pos/t12133/I.java b/test/files/pos/t12133/I.java new file mode 100644 index 000000000000..5d5305a80093 --- /dev/null +++ b/test/files/pos/t12133/I.java @@ -0,0 +1,4 @@ +package pkg; + +@UniqueConstraint(columnNames = {"account_id_ok", "name"}) +public class I {} diff --git a/test/files/pos/t12133/J.java b/test/files/pos/t12133/J.java new file mode 100644 index 000000000000..2c8d95f3badb --- /dev/null +++ b/test/files/pos/t12133/J.java @@ -0,0 +1,4 @@ +package pkg; + +@Table(name = "portal", uniqueConstraints = @UniqueConstraint(columnNames = {"account_id_fk", "name"})) +public class J {} diff --git a/test/files/pos/t12133/Table.java b/test/files/pos/t12133/Table.java new file mode 100644 index 000000000000..edb74c3e7c88 --- /dev/null +++ b/test/files/pos/t12133/Table.java @@ -0,0 +1,6 @@ +package pkg; + +public @interface Table { + String name(); + UniqueConstraint[] uniqueConstraints(); +} diff --git a/test/files/pos/t12133/UniqueConstraint.java b/test/files/pos/t12133/UniqueConstraint.java new file mode 100644 index 000000000000..82b82d98593c --- /dev/null +++ b/test/files/pos/t12133/UniqueConstraint.java @@ -0,0 +1,5 @@ +package pkg; + +public @interface UniqueConstraint { + String[] columnNames(); +} diff --git a/test/files/pos/t12133/test.scala b/test/files/pos/t12133/test.scala new file mode 100644 index 000000000000..68c7a2ee2b0a --- /dev/null +++ b/test/files/pos/t12133/test.scala @@ -0,0 +1,4 @@ +class Test { + new pkg.I + new pkg.J +} diff --git a/test/junit/scala/tools/nsc/DeterminismTest.scala b/test/junit/scala/tools/nsc/DeterminismTest.scala index dcdba15755c5..1053caf1f779 100644 --- a/test/junit/scala/tools/nsc/DeterminismTest.scala +++ b/test/junit/scala/tools/nsc/DeterminismTest.scala @@ -1,19 +1,13 @@ package scala.tools.nsc -import java.io.OutputStreamWriter -import java.nio.charset.Charset -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} +import org.junit.{ Ignore, Test } -import javax.tools.ToolProvider -import org.junit.Test - -import scala.jdk.CollectionConverters._ import scala.reflect.internal.util.{BatchSourceFile, SourceFile} -import scala.tools.nsc.reporters.StoreReporter -import FileUtils._ class DeterminismTest { + private val tester = new DeterminismTester + import tester.test + @Test def testLambdaLift(): Unit = { def code = List[SourceFile]( source("a.scala", @@ -300,66 +294,117 @@ class DeterminismTest { test(List(code)) } + @Test def testAsync(): Unit = { + def code = List[SourceFile]( + source("a.scala", + """ + | object A { + | import scala.tools.nsc.OptionAwait.{optionally, value} + | def test = optionally { + | if (value(Some(true))) { + | var x = "" + | if (value(Some(false))) { + | value(Some(x)) + value(Some(2)) + | } + | } + | } + | } + | + """.stripMargin) + ) + test(List(code)) + } + + @Test def testReferenceToInnerClassMadeNonPrivate(): Unit = { + def code = List[SourceFile]( + source("t.scala", + """ + | trait T { + | private class Inner + | class OtherInner { new Inner } // triggers makeNotPrivate of Inner + | private val v: Option[Inner] = None + | } + """.stripMargin), + source("c.scala","""class C extends T""") + ) + test(List(code)) + } + def source(name: String, code: String): SourceFile = new BatchSourceFile(name, code) - private def test(groups: List[List[SourceFile]]): Unit = { - val referenceOutput = Files.createTempDirectory("reference") +} - def compile(output: Path, files: List[SourceFile]): Unit = { - val g = new Global(new Settings) - g.settings.usejavacp.value = true - g.settings.classpath.value = output.toAbsolutePath.toString - g.settings.outputDirs.setSingleOutput(output.toString) - val storeReporter = new StoreReporter(g.settings) - g.reporter = storeReporter - import g._ - val r = new Run - // println("scalac " + files.mkString(" ")) - r.compileSources(files) - Predef.assert(!storeReporter.hasErrors, storeReporter.infos.mkString("\n")) - files.filter(_.file.name.endsWith(".java")) match { - case Nil => - case javaSources => - def tempFileFor(s: SourceFile): Path = { - val f = output.resolve(s.file.name) - Files.write(f, new String(s.content).getBytes(Charset.defaultCharset())) - } - val options = List("-d", output.toString) - val javac = ToolProvider.getSystemJavaCompiler - assert(javac != null, "No javac from getSystemJavaCompiler. If the java on your path isn't a JDK version, but $JAVA_HOME is, launch sbt with --java-home \"$JAVA_HOME\"") - val fileMan = javac.getStandardFileManager(null, null, null) - val javaFileObjects = fileMan.getJavaFileObjects(javaSources.map(s => tempFileFor(s).toAbsolutePath.toString): _*) - val task = javac.getTask(new OutputStreamWriter(System.out), fileMan, null, options.asJava, Nil.asJava, javaFileObjects) - val result = task.call() - Predef.assert(result) - } - } - for (group <- groups.init) { - compile(referenceOutput, group) - } - compile(referenceOutput, groups.last) - @annotation.unused - class CopyVisitor(src: Path, dest: Path) extends SimpleFileVisitor[Path] { - override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = { - Files.createDirectories(dest.resolve(src.relativize(dir))) - super.preVisitDirectory(dir, attrs) - } - override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { - Files.copy(file, dest.resolve(src.relativize(file))) - super.visitFile(file, attrs) +import scala.annotation.compileTimeOnly +import scala.language.experimental.macros +import scala.reflect.macros.blackbox + +object OptionAwait { + def optionally[T](body: T): Option[T] = macro impl + @compileTimeOnly("[async] `value` must be enclosed in `optionally`") + def value[T](option: Option[T]): T = ??? + def impl(c: blackbox.Context)(body: c.Tree): c.Tree = { + import c.universe._ + val awaitSym = typeOf[OptionAwait.type].decl(TermName("value")) + def mark(t: DefDef): Tree = c.internal.markForAsyncTransform(c.internal.enclosingOwner, t, awaitSym, Map.empty) + val name = TypeName("stateMachine$async") + q""" + final class $name extends _root_.scala.tools.nsc.OptionStateMachine { + ${mark(q"""override def apply(tr$$async: _root_.scala.Option[_root_.scala.AnyRef]) = ${body}""")} } - } - for (permutation <- permutationsWithSubsets(groups.last)) { - val recompileOutput = Files.createTempDirectory("recompileOutput") - copyRecursive(referenceOutput, recompileOutput) - compile(recompileOutput, permutation) - assertDirectorySame(referenceOutput, recompileOutput, permutation.toString) - deleteRecursive(recompileOutput) - } - deleteRecursive(referenceOutput) + new $name().start().asInstanceOf[${c.macroApplication.tpe}] + """ + } +} +trait AsyncStateMachine[F, R] { + /** Assign `i` to the state variable */ + protected def state_=(i: Int): Unit + /** Retrieve the current value of the state variable */ + protected def state: Int + /** Complete the state machine with the given failure. */ + protected def completeFailure(t: Throwable): Unit + /** Complete the state machine with the given value. */ + protected def completeSuccess(value: AnyRef): Unit + /** Register the state machine as a completion callback of the given future. */ + protected def onComplete(f: F): Unit + /** Extract the result of the given future if it is complete, or `null` if it is incomplete. */ + protected def getCompleted(f: F): R + /** + * Extract the success value of the given future. If the state machine detects a failure it may + * complete the async block and return `this` as a sentinel value to indicate that the caller + * (the state machine dispatch loop) should immediately exit. + */ + protected def tryGet(tr: R): AnyRef +} + + +abstract class OptionStateMachine extends AsyncStateMachine[Option[AnyRef], Option[AnyRef]] { + var result$async: Option[AnyRef] = _ + + // FSM translated method + def apply(tr$async: Option[AnyRef]): Unit + + // Required methods + private[this] var state$async: Int = 0 + protected def state: Int = state$async + protected def state_=(s: Int): Unit = state$async = s + protected def completeFailure(t: Throwable): Unit = throw t + protected def completeSuccess(value: AnyRef): Unit = result$async = Some(value) + protected def onComplete(f: Option[AnyRef]): Unit = ??? + protected def getCompleted(f: Option[AnyRef]): Option[AnyRef] = { + f + } + protected def tryGet(tr: Option[AnyRef]): AnyRef = tr match { + case Some(value) => + value.asInstanceOf[AnyRef] + case None => + result$async = None + this // sentinel value to indicate the dispatch loop should exit. + } + def start(): Option[AnyRef] = { + apply(None) + result$async } - def permutationsWithSubsets[A](as: List[A]): List[List[A]] = - as.permutations.toList.flatMap(_.inits.filter(_.nonEmpty)).distinct } + diff --git a/test/junit/scala/tools/nsc/DeterminismTester.scala b/test/junit/scala/tools/nsc/DeterminismTester.scala new file mode 100644 index 000000000000..97a1914f02a9 --- /dev/null +++ b/test/junit/scala/tools/nsc/DeterminismTester.scala @@ -0,0 +1,108 @@ +package scala.tools.nsc + +import java.io.OutputStreamWriter +import java.nio.charset.Charset +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.{FileVisitResult, Files, Path, Paths, SimpleFileVisitor} +import scala.collection.JavaConverters._ +import javax.tools.ToolProvider + +import scala.reflect.internal.util.SourceFile +import scala.reflect.io.AbstractFile +import scala.tools.nsc.FileUtils._ +import scala.tools.nsc.reporters.StoreReporter +import scala.reflect.internal.util.BatchSourceFile + +object DeterminismTester extends DeterminismTester { + def main(args: Array[String]): Unit = { + val (scalacOpts, sourceFilesPaths) = args.indexOf("--") match { + case -1 => (Nil, args.toList) + case i => + val tuple = args.toList.splitAt(i) + (tuple._1, tuple._2.drop(1)) + } + def isJavaOrScala(p: Path) = { + val name = p.getFileName.toString + name.endsWith(".java") || name.endsWith(".scala") + } + def expand(path: Path): Seq[Path] = { + if (Files.isDirectory(path)) + Files.walk(path).iterator().asScala.filter(isJavaOrScala).toList + else path :: Nil + } + val sourceFiles = sourceFilesPaths.map(Paths.get(_)).flatMap(expand).map(path => new BatchSourceFile(AbstractFile.getFile(path.toFile))) + test(scalacOpts, sourceFiles :: Nil) + } +} + +class DeterminismTester { + + def test(groups: List[List[SourceFile]]): Unit = test(Nil, groups) + def test(scalacOptions: List[String], groups: List[List[SourceFile]]): Unit = { + val referenceOutput = Files.createTempDirectory("reference") + + def compile(output: Path, files: List[SourceFile]): Unit = { + // println("compile: " + files) + val g = new Global(new Settings) + g.settings.usejavacp.value = true + g.settings.classpath.value = output.toAbsolutePath.toString + g.settings.outputDirs.setSingleOutput(output.toString) + g.settings.async.value = true + g.settings.processArguments(scalacOptions, true) + val storeReporter = new StoreReporter + g.reporter = storeReporter + import g._ + val r = new Run + // println("scalac " + files.mkString(" ")) + r.compileSources(files) + Predef.assert(!storeReporter.hasErrors, storeReporter.infos.mkString("\n")) + files.filter(_.file.name.endsWith(".java")) match { + case Nil => + case javaSources => + def tempFileFor(s: SourceFile): Path = { + val f = output.resolve(s.file.name) + Files.write(f, new String(s.content).getBytes(Charset.defaultCharset())) + } + val options = List("-d", output.toString) + val javac = ToolProvider.getSystemJavaCompiler + assert(javac != null, "No javac from getSystemJavaCompiler. If the java on your path isn't a JDK version, but $JAVA_HOME is, launch sbt with --java-home \"$JAVA_HOME\"") + val fileMan = javac.getStandardFileManager(null, null, null) + val javaFileObjects = fileMan.getJavaFileObjects(javaSources.map(s => tempFileFor(s).toAbsolutePath.toString): _*) + val task = javac.getTask(new OutputStreamWriter(System.out), fileMan, null, options.asJava, Nil.asJava, javaFileObjects) + val result = task.call() + Predef.assert(result) + } + } + + for (group <- groups.init) { + compile(referenceOutput, group) + } + compile(referenceOutput, groups.last) + + class CopyVisitor(src: Path, dest: Path) extends SimpleFileVisitor[Path] { + override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.createDirectories(dest.resolve(src.relativize(dir))) + super.preVisitDirectory(dir, attrs) + } + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.copy(file, dest.resolve(src.relativize(file))) + super.visitFile(file, attrs) + } + } + val permutations: List[List[SourceFile]] = if (groups.last.size > 32) { + groups.last.reverse :: groups.last.map(_ :: Nil) + } else permutationsWithSubsets(groups.last) + for (permutation <- permutations) { + val recompileOutput = Files.createTempDirectory("recompileOutput") + copyRecursive(referenceOutput, recompileOutput) + compile(recompileOutput, permutation) + assertDirectorySame(referenceOutput, recompileOutput, permutation.toString) + deleteRecursive(recompileOutput) + } + deleteRecursive(referenceOutput) + + } + def permutationsWithSubsets[A](as: List[A]): List[List[A]] = + as.permutations.toList.flatMap(_.inits.filter(_.nonEmpty)).distinct + +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala b/test/junit/scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala index 2e4dc7c7305f..c31ba455b043 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala @@ -19,12 +19,15 @@ class Collector extends NestedClassesCollector[String](nestedOnly = false) { @RunWith(classOf[JUnit4]) class NestedClassesCollectorTest { val c = new Collector { - override def visitInternalName(internalName: String, offset: Int, length: Int): Unit = - innerClasses += internalName.substring(offset, length) + override def visitInternalName(internalName: String, offset: Int, length: Int): Unit = { + val c = internalName.substring(offset, length) + if (!declaredInnerClasses.contains(c)) + referredInnerClasses += c + } } def inners: List[String] = { val res = c.innerClasses.toList.sorted - c.innerClasses.clear() + c.clear() res } diff --git a/test/scaladoc/run/t10673.scala b/test/scaladoc/run/t10673.scala index 39bc7dc75566..01790adf0719 100644 --- a/test/scaladoc/run/t10673.scala +++ b/test/scaladoc/run/t10673.scala @@ -32,6 +32,8 @@ object Test extends ScaladocModelTest { import access._ def showParents(e: MemberTemplateEntity): Unit = { e.parentTypes.foreach(_._2.refEntity.foreach { + case (_, (LinkToMember(mbr, tpl), _)) => println(s"found link for member $mbr to $tpl") + case (_, (LinkToTpl(tpl), _)) => println(s"found link $tpl") case (_, (LinkToExternalTpl(name, _, tpl), _)) => println(s"'$name' links to $tpl") case (_, (Tooltip(name), _)) => println(s"'$name' no link!") })