Skip to content

Commit ae8741e

Browse files
authored
Fix Patch.replaceSymbols bug that affects ScalaTest rule. (#1293)
Previously, the ScalaTest autofix rule didn't properly handle cases with renamed imports due to a bug in Scalafix. This commit fixes that bug so that the ScalaTest rule works correctly as expected.
1 parent 8122016 commit ae8741e

File tree

9 files changed

+129
-6
lines changed

9 files changed

+129
-6
lines changed

scalafix-core/src/main/scala/scalafix/internal/patch/ReplaceSymbolOps.scala

+42-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scalafix.internal.patch
33
import scala.meta._
44
import scala.meta.internal.trees._
55

6+
import scalafix.internal.util.SymbolOps
67
import scalafix.internal.util.SymbolOps.Root
78
import scalafix.internal.util.SymbolOps.SignatureName
89
import scalafix.patch.Patch
@@ -38,14 +39,19 @@ object ReplaceSymbolOps {
3839
(Symbol.Global(qual, Signature.Type(name)).syntax -> to) ::
3940
Nil
4041
}.toMap
41-
def loop(ref: Ref, sym: Symbol): (Patch, Symbol) = {
42+
def loop(ref: Ref, sym: Symbol, isImport: Boolean): (Patch, Symbol) = {
4243
(ref, sym) match {
4344
// same length
4445
case (a @ Name(_), Symbol.Global(Symbol.None, SignatureName(b))) =>
4546
ctx.replaceTree(a, b) -> Symbol.None
4647
// ref is shorter
47-
case (a @ Name(_), sym @ Symbol.Global(_, SignatureName(b))) =>
48-
ctx.replaceTree(a, b) -> sym
48+
case (a @ Name(_), sym @ Symbol.Global(owner, SignatureName(b))) =>
49+
if (isImport) {
50+
val qual = SymbolOps.toTermRef(sym)
51+
ctx.replaceTree(a, qual.syntax) -> Symbol.None
52+
} else {
53+
ctx.replaceTree(a, b) -> sym
54+
}
4955
// ref is longer
5056
case (
5157
Select(qual, Name(_)),
@@ -57,7 +63,7 @@ object ReplaceSymbolOps {
5763
Select(qual: Ref, a @ Name(_)),
5864
Symbol.Global(symQual, SignatureName(b))
5965
) =>
60-
val (patch, toImport) = loop(qual, symQual)
66+
val (patch, toImport) = loop(qual, symQual, isImport)
6167
(patch + ctx.replaceTree(a, b)) -> toImport
6268
}
6369
}
@@ -79,6 +85,13 @@ object ReplaceSymbolOps {
7985
result
8086
}
8187
}
88+
object Identifier {
89+
def unapply(tree: Tree): Option[(Name, Symbol)] = tree match {
90+
case n: Name => n.symbol.map(s => n -> s)
91+
case Init(n: Name, _, _) => n.symbol.map(s => n -> s)
92+
case _ => None
93+
}
94+
}
8295
val patches = ctx.tree.collect { case n @ Move(to) =>
8396
// was this written as `to = "blah"` instead of `to = _root_.blah`
8497
val isSelected = to match {
@@ -88,14 +101,37 @@ object ReplaceSymbolOps {
88101
n.parent match {
89102
case Some(i @ Importee.Name(_)) =>
90103
ctx.removeImportee(i)
104+
case Some(i @ Importee.Rename(name, rename)) =>
105+
i.parent match {
106+
case Some(Importer(ref, `i` :: Nil)) =>
107+
Patch.replaceTree(ref, SymbolOps.toTermRef(to.owner).syntax) +
108+
Patch.replaceTree(name, to.signature.name)
109+
case Some(Importer(ref, _)) =>
110+
Patch.removeImportee(i) +
111+
Patch.addGlobalImport(
112+
Importer(
113+
SymbolOps.toTermRef(to.owner),
114+
List(
115+
Importee.Rename(Term.Name(to.signature.name), rename)
116+
)
117+
)
118+
)
119+
case _ => Patch.empty
120+
}
91121
case Some(parent @ Select(_, `n`)) if isSelected =>
92-
val (patch, imp) = loop(parent, to)
122+
val (patch, imp) =
123+
loop(parent, to, isImport = n.parents.exists(_.is[Import]))
93124
ctx.addGlobalImport(imp) + patch
94-
case Some(_) =>
125+
case Some(Identifier(parent, Symbol.Global(_, sig)))
126+
if sig.name != parent.value =>
127+
Patch.empty // do nothing because it was a renamed symbol
128+
case Some(parent) =>
95129
val addImport =
96130
if (n.isDefinition) Patch.empty
97131
else ctx.addGlobalImport(to)
98132
addImport + ctx.replaceTree(n, to.signature.name)
133+
case _ =>
134+
Patch.empty
99135
}
100136
}
101137
patches.asPatch
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
ignore = true
3+
*/
4+
package org.scalatest_autofix
5+
6+
object Matchers extends Matchers
7+
class Matchers {
8+
def shouldBe(n: Int): Unit = ???
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
rule = ScalatestAutofixRule
3+
*/
4+
package tests.scalatest_autofix
5+
6+
import org.scalatest_autofix.Matchers._
7+
8+
object ScalatestAutofixRule {
9+
def foo(): Unit = shouldBe(1)
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
rule = ScalatestAutofixRule
3+
*/
4+
package tests.scalatest_autofix
5+
6+
import scala.collection.mutable
7+
8+
object ScalatestAutofixRule2 {
9+
object WithRename {
10+
import org.scalatest_autofix.{Matchers => ScalaTestMatchers}
11+
12+
class UsesRename extends ScalaTestMatchers
13+
class UsesOriginal extends org.scalatest_autofix.Matchers {
14+
val x = mutable.ListBuffer.empty[Int]
15+
}
16+
}
17+
object WithoutRename {
18+
import org.scalatest_autofix.Matchers
19+
class UsesRename extends Matchers
20+
class UsesOriginal extends org.scalatest_autofix.Matchers
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.scalatest_autofix.matchers.should
2+
3+
object Matchers extends Matchers
4+
class Matchers {
5+
def shouldBe(n: Int): Unit = ???
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package tests.scalatest_autofix
2+
3+
import org.scalatest_autofix.matchers.should.Matchers._
4+
5+
object ScalatestAutofixRule {
6+
def foo(): Unit = shouldBe(1)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package tests.scalatest_autofix
2+
3+
import scala.collection.mutable
4+
import org.scalatest_autofix.matchers
5+
import org.scalatest_autofix.matchers.should.Matchers
6+
7+
object ScalatestAutofixRule2 {
8+
object WithRename {
9+
import org.scalatest_autofix.matchers.should.{Matchers => ScalaTestMatchers}
10+
11+
class UsesRename extends ScalaTestMatchers
12+
class UsesOriginal extends matchers.should.Matchers {
13+
val x = mutable.ListBuffer.empty[Int]
14+
}
15+
}
16+
object WithoutRename {
17+
class UsesRename extends Matchers
18+
class UsesOriginal extends matchers.should.Matchers
19+
}
20+
}

scalafix-tests/unit/src/main/resources/META-INF/services/scalafix.v1.Rule

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ banana.rule.SemanticRuleV1
22
banana.rule.SyntacticRuleV1
33
banana.rule.CommentFileNonAtomic
44
banana.rule.CommentFileAtomic
5+
scalafix.test.ScalatestAutofixRule
56
scalafix.test.ExplicitSynthetic
67
scalafix.tests.cli.CrashingRule
78
scalafix.tests.cli.NoOpRule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package scalafix.test
2+
3+
import scalafix.v1.SemanticRule
4+
import scalafix.v1._
5+
6+
class ScalatestAutofixRule extends SemanticRule("ScalatestAutofixRule") {
7+
override def fix(implicit doc: SemanticDocument): Patch = {
8+
Patch.replaceSymbols(
9+
"org.scalatest_autofix.Matchers" -> "org.scalatest_autofix.matchers.should.Matchers"
10+
)
11+
}
12+
}

0 commit comments

Comments
 (0)