diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index 1db3f58e3b..23164de999 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala @@ -667,50 +667,40 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean, assert(isScalaClass || isInterface, s"Cannot call lookupMethod($methodName) on non Scala class $this") - @tailrec - def tryLookupInherited(ancestorInfo: ClassInfo): Option[MethodInfo] = { - ancestorInfo.publicMethodInfos.get(methodName) match { - case Some(m) if !m.isAbstract && (!m.nonExistent || ancestorInfo == this) => - Some(m) - case _ => - ancestorInfo.superClass match { - case Some(superClass) => tryLookupInherited(superClass) - case None => None + publicMethodInfos.get(methodName) match { + case Some(m) if !m.isAbstract => Some(m) + + case _ => + val candidate = superClass + .flatMap(_.tryLookupMethod(methodName)) + .filterNot(_.nonExistent) + + if (allowAddingSyntheticMethods) { + def maybeDefaultTarget = getDefaultTarget(methodName) + + def needsDefaultOverride(method: MethodInfo): Boolean = { + /* The .get is OK, since we only get here if: + * - This class doesn't implement the method directly. + * - The superClass has found a default target. + * In this case, we always find at least one target. + */ + method.isDefaultBridge && method.defaultBridgeTarget != maybeDefaultTarget.get.owner.className } - } - } - val existing = - if (isScalaClass) tryLookupInherited(this) - else publicMethodInfos.get(methodName).filter(!_.isAbstract) - if (!allowAddingSyntheticMethods) { - existing - } else if (existing.exists(m => !m.isDefaultBridge || m.owner == this)) { - /* If we found a non-bridge, it must be the right target. - * If we found a bridge directly in this class/interface, it must also - * be the right target. - */ - existing - } else { - // Try and find the target of a possible default bridge - findDefaultTarget(methodName).fold { - assert(existing.isEmpty) - existing - } { defaultTarget => - if (existing.exists(_.defaultBridgeTarget == defaultTarget.owner.className)) { - /* If we found an existing bridge targeting the right method, we - * can reuse it. - * We also get here with None when there is no target whatsoever. - */ - existing + candidate + .filterNot(needsDefaultOverride(_)) + .orElse(maybeDefaultTarget.map(createDefaultBridge(_))) } else { - // Otherwise, create a new default bridge - Some(createDefaultBridge(defaultTarget)) + candidate } - } } } + private val defaultTargets = mutable.Map.empty[MethodName, Option[MethodInfo]] + + private def getDefaultTarget(methodName: MethodName): Option[MethodInfo] = + defaultTargets.getOrElseUpdate(methodName, findDefaultTarget(methodName)) + /** Resolves an inherited default method. * * This lookup is specified by the JVM resolution rules for default diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala new file mode 100644 index 0000000000..b6870d16e5 --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala @@ -0,0 +1,72 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.ir.ClassKind +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.junit.async._ + +import org.scalajs.linker.interface.StandardConfig +import org.scalajs.linker.standard._ + +import org.scalajs.linker.testutils.TestIRBuilder._ +import org.scalajs.linker.testutils.LinkingUtils._ + +class BaseLinkerTest { + import scala.concurrent.ExecutionContext.Implicits.global + + @Test + def noUnnecessaryDefaultBridges(): AsyncResult = await { + val fooName = m("foo", Nil, IntRef) + val classDefs = Seq( + classDef( + "Intf", + kind = ClassKind.Interface, + methods = List( + MethodDef(EMF, fooName, NON, Nil, IntType, Some(int(1)))(EOH, UNV)) + ), + classDef( + "Base", + kind = ClassKind.Class, + superClass = Some(ObjectClass), + interfaces = List("Intf"), + methods = List(trivialCtor("Base")) + ), + classDef( + "Sub", + kind = ClassKind.Class, + superClass = Some("Base"), + methods = List(trivialCtor("Sub")) + ), + mainTestClassDef( + consoleLog(Apply(EAF, New("Sub", NoArgConstructorName, Nil), fooName, Nil)(IntType)) + ) + ) + + val config = StandardConfig().withOptimizer(false) + + for (moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers, config = config)) yield { + val clazz = findClass(moduleSet, "Sub").get + assertFalse(clazz.methods.exists(_.name.name == fooName)) + } + } + + private def findClass(moduleSet: ModuleSet, name: ClassName): Option[LinkedClass] = + moduleSet.modules.flatMap(_.classDefs).find(_.className == name) +}