Skip to content

repl - new heuristic for import shadowing #14951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions compiler/src/dotty/tools/repl/CollectTopLevelImports.scala

This file was deleted.

1 change: 1 addition & 0 deletions compiler/src/dotty/tools/repl/ReplCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import dotty.tools.dotc.util.Spans._
import dotty.tools.dotc.util.{ParsedComment, Property, SourceFile}
import dotty.tools.dotc.{CompilationUnit, Compiler, Run}
import dotty.tools.repl.results._
import dotty.tools.repl.transform._

import scala.collection.mutable
import scala.util.chaining.given
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import dotty.tools.dotc.util.{SourceFile, SourcePosition}
import dotty.tools.dotc.{CompilationUnit, Driver}
import dotty.tools.dotc.config.CompilerCommand
import dotty.tools.io._
import dotty.tools.repl.transform._
import dotty.tools.runner.ScalaClassLoader.*
import org.jline.reader._

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package dotty.tools.repl.transform

import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.core.Contexts._
import dotty.tools.dotc.core.Names.Name
import dotty.tools.dotc.core.Phases.Phase
import dotty.tools.dotc.core.StdNames.nme


/** This phase collects and transforms top-level Import trees to handle definition shadowing.
*
* This is used by repl to handle new run contexts and allowing
* definitions to be shadowed by imports in the same run.
*
* Import transformation is necessary for excluding its members when they are shadowed in the same run.
* This is done by finding all members defined after the Import clause calculating
* their intersection with available members from selectors including renaming.
*
* This step is necessary for proper new run initialization since we need to import the previous run
* into Context. It is accomplished in the following order:
* 1. Previous wrapper object for a given run
* 2. Previous imports for a given run
*
* This phase uses typed trees thus after the Typer phase.
*/
class CollectTopLevelImports extends Phase {
import tpd._

def phaseName: String = "collectTopLevelImports"

private var gatheredImports: List[Import] = _

def imports: List[Import] = gatheredImports

def run(using Context): Unit =
val PackageDef(_, _ :: TypeDef(_, rhs: Template) :: _) = ctx.compilationUnit.tpdTree: @unchecked
gatheredImports = transformTopLevelImports(rhs.body)

/** Transforms top-level imports to exclude intersecting members declared after the Import clause.
* To properly handle imports such as: `import A.f; def f = 3` consequently making sure that original selectors are
* filtered to eliminate potential duplications that would result in compilation error.
*
* Transformed imports of which selectors were all shadowed will be ignored in the future runs.
*/
private def transformTopLevelImports(trees: List[Tree])(using Context): List[Import] =
val definitions = collectTopLevelMemberDefs(trees)

trees.collect {
case tree @ Import(expr, selectors) =>
val definitionsAfterImport = definitions.filter(_._2 > tree.endPos.end).map(_._1)

val importedNames: List[Name] = (if selectors.exists(_.isWildcard) then
val allImportTypeMembers = expr.tpe.allMembers.map(_.name)
val nonWildcardSelectors = selectors.filter(_.isWildcard)
val renamedMembers = nonWildcardSelectors.map(_.imported.name)
nonWildcardSelectors.map(_.rename) ++ allImportTypeMembers.filterNot(renamedMembers.contains)
else
selectors.map(_.rename)
)

val shadowedMembers = importedNames.intersect(definitionsAfterImport)
val adjustedSelectors = shadowedMembers.map(collidingMember => {
untpd.ImportSelector(untpd.Ident(collidingMember), untpd.Ident(nme.WILDCARD))
})

val remainingSelectors = selectors.filterNot(importSelector => {
shadowedMembers.contains(importSelector.rename)
})

if remainingSelectors.isEmpty then
None
else
Some(Import(expr, adjustedSelectors ++ remainingSelectors))
}.flatten

private def collectTopLevelMemberDefs(trees: List[Tree])(using Context): List[(Name, Int)] =
trees.collect {
case tree: ValDef => tree.name -> tree.endPos.end
case tree: DefDef => tree.name -> tree.endPos.end
case tree: TypeDef => tree.name -> tree.endPos.end
}

}

72 changes: 71 additions & 1 deletion compiler/test-resources/repl/import-shadowing
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
scala> object A { def f = 1 }
scala> object A { def f = 1; def t = 2 }
// defined object A

scala> object B { def f = 2 }
Expand All @@ -25,3 +25,73 @@ def f: Int

scala> val x3 = f
val x3: Int = 1

scala> import A._; def f = 5
def f: Int

scala> val x4 = f
val x4: Int = 5

scala> def f = 6; println(f); import A._; println(f); // import shadowing should only work on toplevel definitions in the following runs
6
6
def f: Int

scala> import A._; println(f); def f = 7; println(f) // import shadowing should only work on toplevel definitions in the following runs
7
7
def f: Int

scala> def f = 8; import A.f
def f: Int

scala> val x5 = f
val x5: Int = 1

scala> import A.f; def f = 9
def f: Int

scala> val x6 = f
val x6: Int = 9

scala> import A.{f => ff}; def f = 10
def f: Int

scala> val x7 = f
val x7: Int = 10

scala> val x8 = ff
val x8: Int = 1

scala> def f = 10; import A.{f => ff}
def f: Int

scala> val x9 = f
val x9: Int = 10

scala> val x10 = ff
val x10: Int = 1

scala> import A.{t, _}; def f = 11
def f: Int

scala> val x11 = f
val x11: Int = 11

scala> def f = 12; import A.{t, _}
def f: Int

scala> val x12 = f
val x12: Int = 1

scala> def f = 13; import A.{t => f}
def f: Int

scala> val x13 = f
val x13: Int = 2

scala> import A.{t => f}; def f = 14
def f: Int

scala> val x14 = f
val x14: Int = 14