@@ -69,6 +69,7 @@ class JSCodeGen()(using genCtx: Context) {
6969 // Some state --------------------------------------------------------------
7070
7171 private val generatedClasses = mutable.ListBuffer .empty[js.ClassDef ]
72+ private val generatedStaticForwarderClasses = mutable.ListBuffer .empty[(Symbol , js.ClassDef )]
7273
7374 private val currentClassSym = new ScopedVar [Symbol ]
7475 private val currentMethodSym = new ScopedVar [Symbol ]
@@ -111,6 +112,7 @@ class JSCodeGen()(using genCtx: Context) {
111112 genCompilationUnit(ctx.compilationUnit)
112113 } finally {
113114 generatedClasses.clear()
115+ generatedStaticForwarderClasses.clear()
114116 }
115117 }
116118
@@ -179,6 +181,39 @@ class JSCodeGen()(using genCtx: Context) {
179181
180182 for (tree <- generatedClasses)
181183 genIRFile(cunit, tree)
184+
185+ if (generatedStaticForwarderClasses.nonEmpty) {
186+ /* #4148 Add generated static forwarder classes, except those that
187+ * would collide with regular classes on case insensitive file systems.
188+ */
189+
190+ /* I could not find any reference anywhere about what locale is used
191+ * by case insensitive file systems to compare case-insensitively.
192+ * In doubt, force the English locale, which is probably going to do
193+ * the right thing in virtually all cases (especially if users stick
194+ * to ASCII class names), and it has the merit of being deterministic,
195+ * as opposed to using the OS' default locale.
196+ * The JVM backend performs a similar test to emit a warning for
197+ * conflicting top-level classes. However, it uses `toLowerCase()`
198+ * without argument, which is not deterministic.
199+ */
200+ def caseInsensitiveNameOf (classDef : js.ClassDef ): String =
201+ classDef.name.name.nameString.toLowerCase(java.util.Locale .ENGLISH )
202+
203+ val generatedCaseInsensitiveNames =
204+ generatedClasses.map(caseInsensitiveNameOf).toSet
205+
206+ for ((site, classDef) <- generatedStaticForwarderClasses) {
207+ if (! generatedCaseInsensitiveNames.contains(caseInsensitiveNameOf(classDef))) {
208+ genIRFile(cunit, classDef)
209+ } else {
210+ report.warning(
211+ s " Not generating the static forwarders of ${classDef.name.name.nameString} " +
212+ " because its name differs only in case from the name of another class or trait in this compilation unit." ,
213+ site.srcPos)
214+ }
215+ }
216+ }
182217 }
183218
184219 private def genIRFile (cunit : CompilationUnit , tree : ir.Trees .ClassDef ): Unit = {
@@ -346,7 +381,7 @@ class JSCodeGen()(using genCtx: Context) {
346381 forwarders,
347382 Nil
348383 )(js.OptimizerHints .empty)
349- generatedClasses += forwardersClassDef
384+ generatedStaticForwarderClasses += sym -> forwardersClassDef
350385 }
351386 }
352387 allMemberDefsExceptStaticForwarders
@@ -523,14 +558,33 @@ class JSCodeGen()(using genCtx: Context) {
523558 * `def hashCode(): Int` and a `static def hashCode(Int): Int`. The JVM
524559 * back-end considers them as colliding because they have the same name,
525560 * but we must not.
561+ *
562+ * By default, we only emit forwarders for top-level objects, like the JVM
563+ * back-end. However, if requested via a compiler option, we enable them
564+ * for all static objects. This is important so we can implement static
565+ * methods of nested static classes of JDK APIs (see scala-js/#3950).
526566 */
527567
528568 /** Is the given Scala class, interface or module class a candidate for
529569 * static forwarders?
570+ *
571+ * - the flag `-XnoForwarders` is not set to true, and
572+ * - the symbol is static, and
573+ * - either of both of the following is true:
574+ * - the flag `-scalajsGenStaticForwardersForNonTopLevelObjects` is set to true, or
575+ * - the symbol was originally at the package level
576+ *
577+ * Other than the Scala.js-specific flag, and the fact that we also consider
578+ * interfaces, this performs the same tests as the JVM back-end.
530579 */
531580 def isCandidateForForwarders (sym : Symbol ): Boolean = {
532- // it must be a top level class
533- sym.isStatic
581+ ! ctx.settings.XnoForwarders .value && sym.isStatic && {
582+ ctx.settings.scalajsGenStaticForwardersForNonTopLevelObjects.value || {
583+ atPhase(flattenPhase) {
584+ toDenot(sym).owner.is(PackageClass )
585+ }
586+ }
587+ }
534588 }
535589
536590 /** Gen the static forwarders to the members of a class or interface for
0 commit comments