Skip to content
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

Add symbol-based type shortener #42

Closed
wants to merge 20 commits into from
Closed

Conversation

jvican
Copy link

@jvican jvican commented Jan 23, 2017

This PR introduces a symbol-based type shortener to replace the string-based one. The benefits of this approach are:

  • Provides more consistent behaviour with the Scala compiler
  • Allows fine-grained control over what's in scope and what's not
  • Helps future customization (as the whole information about scopes and symbols is available)

I think that there are some small details to flesh out before we nail down the desired behaviour. I'm commenting on some lines to make it clearer. I ran this on cats and I received pretty diffs, but for some reasons I was not successful at running it in scalajs.

@jvican jvican self-assigned this Jan 23, 2017
@jvican jvican requested a review from olafurpg January 23, 2017 10:31
token.input.eq(from.input) &&
token.end <= to.end &&
token.start >= from.start
sealed abstract class Patch {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes were imported from previous @olafurpg's commits in other branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have bigger pending changes on imports. But I'm hoping to get this in first if that's possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary now? If not remove

case _ =>
}
/** The compiler sets different symbols for `PackageDef`s
* and term names pointing to that package. Get the
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason this is not aligned, sorry about that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what?! can you fix it manually?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword suggestion: returns canonical symbol of a package symbol for equality comparison.

It's typically good to start docstrings with "Returns ..."

* we need to check both names to guarantee whether a name is in scope.
*/
@inline
def lookupBothNames(name: String,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm extending this method to return both the term symbol and type names if both exist. This can happen if types with same names are defined in a trait and its companion object, for example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain that in docstring instead of here?

}

// Disambiguate overloaded symbols caused by name shadowing
if (symbol.isOverloaded) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to disambiguate the overloaded symbol with the owner. If there's not luck, we fallback to a string-based approach: get the symbol whose full name is the longest common subexpression of our tpe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention this in code comment?

val wholeScope = mixScopes(inScope, realRootScope)
val disambiguatingSyntax = ref.syntax
val (_, reversedSymbols) = {
names.iterator.foldLeft(wholeScope -> List.empty[g.Symbol]) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iterator here can be removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you have it?

// Return shortened type for names already in scope
val onlyNames = onlyPaths.map(_._1)
val removed = removePrefixes(refNoThis, onlyNames)
val shortenedAndRenamedType = removed.transform {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renaming logic can go into an independent method for easing readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is a good idea. I'm not religious about it, but I think the rule of 30 is nice to keep in mind https://github.com/databricks/scala-style-guide#rule_of_30

else paths.tail.map(_._1)
}

// Shortened names must be just one if no PDT
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the idiomatic way of failing in scalafix? Here we're failing fast since these cases should never happen. I wonder if this is ok or we should go for another solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these exceptional/unexpected cases? If so, then throw exception with clear error message about what assumption was broken. If not exceptional or unexpected, then encode in type signature of shortenType

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, I suspect this will eventually become a part of the scala.meta semantic api, once we have gained some experience with it in scalafix.

@jvican
Copy link
Author

jvican commented Jan 23, 2017

So far, imports inside blocks are not recognised (and we should):

val foo = {
  import scala.concurrent.mutable.List
  List.empty[Int]
}

It could be done by changing the ScopeTraverser to visit vals and defs and then changing the logic in the shortenType method to get the enclosing scope. I'm happy to take a look at it in another PR, since these cases are rare and should not block any progress on scalafix's front.

Copy link
Contributor

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work @jvican! A lot of comments, but mostly small things.

I am not as deep into the reflect api, so some parts I couldn't review thoroughly. Do you have anything to add @xeno-by?

}
}
/** Get the last sequential top level package from a source file. */
def getLastTopLevelPkg(potentialPkg: Stat): Stat = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an equivalent implementation?

  def getLastTopLevelPkg(potPkg: Stat): Stat = potPkg match {
    case Pkg(_, head +: Nil) => getLastTopLevelPkg(head)
    case Pkg(_, head +: _) => head
    case _ => potPkg
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what test is this method needed?

@@ -18,4 +17,7 @@ trait SemanticApi {

/** Returns the type annotation for given val/def. */
def typeSignature(defn: Defn): Option[Type]

/** */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Returns the same type with fully qualified names shortened and missing imports.

What do you think about returning a case class instead of tuple?

tpe should be named toShorten and t should be atLocation.

What happens on failure? This is method is used for aestetics reasons and can safely be skipped so it could either return an Option[(a, b)] if shortening failed or default to toShorten -> Nil. WDYT?

token.input.eq(from.input) &&
token.end <= to.end &&
token.start >= from.start
sealed abstract class Patch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have bigger pending changes on imports. But I'm hoping to get this in first if that's possible.

>>>
import scala.collection.immutable.{Map => MyMap}
class A {
implicit val x: MyMap[Int, String] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive! 😍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at the other two tests checking renames.

MyMap(1 -> "STRING")
}
<<< removed map
import scala.collection.immutable.{Map => _}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't testing anything, since Map is a val in Predef. Maybe use a different collection that's not in Predef?

else paths.tail.map(_._1)
}

// Shortened names must be just one if no PDT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, I suspect this will eventually become a part of the scala.meta semantic api, once we have gained some experience with it in scalafix.

}

// Shortened names must be just one if no PDT
assert(shortenedNames.size == 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include at least a clarifying message.

new SemanticApi {
override def shortenType(tpe: m.Type,
owner: m.Tree): (m.Type, Seq[m.Ref]) = {
logger.elem(tpe)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

val typeRefsAndRewrites = typeRefs.zip(typeRewrites)
val mappedRewrites = typeRefsAndRewrites.map {
case (typeRef, missingOrHit) =>
typeRef -> missingOrHit.fold(_._2, identity)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

._.2, identity is a sign that a case class might be a better encoding.

@@ -190,7 +190,7 @@ class ScalaJs
Command("examples/test:compile")
)
),
skip = true // GenJsCode is hard: import renames + dependent types
skip = false // GenJsCode is hard: import renames + dependent types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you run clean/compile after disabling scalafix? I think I disabled it for some reason while debugging.

@jvican
Copy link
Author

jvican commented Jan 23, 2017

Diff from cats:

diff --git a/.jvmopts b/.jvmopts
deleted file mode 100644
index 83ed6fb4..00000000
--- a/.jvmopts
+++ /dev/null
@@ -1,12 +0,0 @@
-# see https://weblogs.java.net/blog/kcpeppe/archive/2013/12/11/case-study-jvm-hotspot-flags
--Dfile.encoding=UTF8
--Xms1G
--Xmx6G
--XX:MaxPermSize=512M
--XX:ReservedCodeCacheSize=250M
--XX:+TieredCompilation
--XX:-UseGCOverheadLimit
-# effectively adds GC to Perm space
--XX:+CMSClassUnloadingEnabled
-# must be enabled for CMSClassUnloadingEnabled to work
--XX:+UseConcMarkSweepGC
diff --git a/js/src/test/scala/cats/tests/FutureTests.scala b/js/src/test/scala/cats/tests/FutureTests.scala
index e248189d..d81a4746 100644
--- a/js/src/test/scala/cats/tests/FutureTests.scala
+++ b/js/src/test/scala/cats/tests/FutureTests.scala
@@ -2,6 +2,7 @@ package cats
 package js
 package tests
 
+import scala.concurrent.ExecutionContextExecutor
 import cats.laws.discipline._
 import cats.js.instances.Await
 import cats.js.instances.future.futureComonad
@@ -17,7 +18,7 @@ import cats.laws.discipline.arbitrary._
 // https://issues.scala-lang.org/browse/SI-7934
 @deprecated("", "")
 class DeprecatedForwarder {
-  implicit def runNow = scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
+  implicit def runNow: ExecutionContextExecutor = scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
 }
 object DeprecatedForwarder extends DeprecatedForwarder
 import DeprecatedForwarder.runNow
diff --git a/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala b/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala
index 769970c6..addac01e 100644
--- a/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala
+++ b/kernel-laws/src/test/scala/cats/kernel/laws/LawTests.scala
@@ -228,7 +228,7 @@ class LawTests extends FunSuite with Discipline {
           .forall { case (x, y) => a.eqv(x, y) == b.eqv(x, y) }
     }
 
-    implicit val monoidOrderN = Order.whenEqualMonoid[N]
+    implicit val monoidOrderN: Monoid[Order[N]] with Band[Order[N]] = Order.whenEqualMonoid[N]
     laws[GroupLaws, Order[N]].check(_.monoid)
     laws[GroupLaws, Order[N]].check(_.band)
 
diff --git a/kernel/src/main/scala/cats/kernel/instances/char.scala b/kernel/src/main/scala/cats/kernel/instances/char.scala
index 4238fa84..456afbe4 100644
--- a/kernel/src/main/scala/cats/kernel/instances/char.scala
+++ b/kernel/src/main/scala/cats/kernel/instances/char.scala
@@ -4,7 +4,7 @@ package instances
 package object char extends CharInstances
 
 trait CharInstances {
-  implicit val catsKernelStdOrderForChar = new CharOrder
+  implicit val catsKernelStdOrderForChar: CharOrder = new CharOrder
 }
 
 class CharOrder extends Order[Char] {
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 10bb5da4..e8db97b3 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -11,3 +11,5 @@ addSbtPlugin("org.scala-js"         % "sbt-scalajs"           % "0.6.13")
 addSbtPlugin("com.github.tkawachi"  % "sbt-doctest"           % "0.4.1")
 addSbtPlugin("org.xerial.sbt"       % "sbt-sonatype"          % "1.1")
 addSbtPlugin("com.fortysevendeg"    % "sbt-microsites"        % "0.3.2")
+
+addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.2.2")
diff --git a/tests/src/test/scala/cats/tests/EvalTests.scala b/tests/src/test/scala/cats/tests/EvalTests.scala
index d7c41002..9a9af542 100644
--- a/tests/src/test/scala/cats/tests/EvalTests.scala
+++ b/tests/src/test/scala/cats/tests/EvalTests.scala
@@ -1,6 +1,11 @@
 package cats
 package tests
 
+import cats.Monoid
+import cats.Semigroup
+import cats.Order
+import cats.PartialOrder
+import cats.Eq
 import scala.math.min
 import cats.laws.ComonadLaws
 import cats.laws.discipline.{BimonadTests, CartesianTests, MonadTests, SerializableTests}
@@ -92,7 +97,7 @@ class EvalTests extends CatsSuite {
   }
 
   {
-    implicit val iso = CartesianTests.Isomorphisms.invariant[Eval]
+    implicit val iso: CartesianTests.Isomorphisms[Eval] = CartesianTests.Isomorphisms.invariant[Eval]
     checkAll("Eval[Int]", BimonadTests[Eval].bimonad[Int, Int, Int])
     checkAll("Eval[Int]", MonadTests[Eval].monad[Int, Int, Int])
   }
@@ -102,27 +107,27 @@ class EvalTests extends CatsSuite {
   checkAll("Eval[Int]", GroupLaws[Eval[Int]].group)
 
   {
-    implicit val A = ListWrapper.monoid[Int]
+    implicit val A: Monoid[ListWrapper[Int]] = ListWrapper.monoid[Int]
     checkAll("Eval[ListWrapper[Int]]", GroupLaws[Eval[ListWrapper[Int]]].monoid)
   }
 
   {
-    implicit val A = ListWrapper.semigroup[Int]
+    implicit val A: Semigroup[ListWrapper[Int]] = ListWrapper.semigroup[Int]
     checkAll("Eval[ListWrapper[Int]]", GroupLaws[Eval[ListWrapper[Int]]].semigroup)
   }
 
   {
-    implicit val A = ListWrapper.order[Int]
+    implicit val A: Order[ListWrapper[Int]] = ListWrapper.order[Int]
     checkAll("Eval[ListWrapper[Int]]", OrderLaws[Eval[ListWrapper[Int]]].order)
   }
 
   {
-    implicit val A = ListWrapper.partialOrder[Int]
+    implicit val A: PartialOrder[ListWrapper[Int]] = ListWrapper.partialOrder[Int]
     checkAll("Eval[ListWrapper[Int]]", OrderLaws[Eval[ListWrapper[Int]]].partialOrder)
   }
 
   {
-    implicit val A = ListWrapper.eqv[Int]
+    implicit val A: Eq[ListWrapper[Int]] = ListWrapper.eqv[Int]
     checkAll("Eval[ListWrapper[Int]]", OrderLaws[Eval[ListWrapper[Int]]].eqv)
   }
 
diff --git a/tests/src/test/scala/cats/tests/IdTests.scala b/tests/src/test/scala/cats/tests/IdTests.scala
index 0465dd34..a7b12751 100644
--- a/tests/src/test/scala/cats/tests/IdTests.scala
+++ b/tests/src/test/scala/cats/tests/IdTests.scala
@@ -1,10 +1,11 @@
 package cats
 package tests
 
+import cats.Id
 import cats.laws.discipline._
 
 class IdTests extends CatsSuite {
-  implicit val iso = CartesianTests.Isomorphisms.invariant[Id]
+  implicit val iso: CartesianTests.Isomorphisms[Id] = CartesianTests.Isomorphisms.invariant[Id]
 
   checkAll("Id[Int]", BimonadTests[Id].bimonad[Int, Int, Int])
   checkAll("Bimonad[Id]", SerializableTests.serializable(Bimonad[Id]))
diff --git a/tests/src/test/scala/cats/tests/NonEmptyListTests.scala b/tests/src/test/scala/cats/tests/NonEmptyListTests.scala
index c913a8f8..c59a86e5 100644
--- a/tests/src/test/scala/cats/tests/NonEmptyListTests.scala
+++ b/tests/src/test/scala/cats/tests/NonEmptyListTests.scala
@@ -1,6 +1,8 @@
 package cats
 package tests
 
+import cats.PartialOrder
+import cats.Order
 import cats.kernel.laws.{GroupLaws, OrderLaws}
 
 import cats.data.NonEmptyList
@@ -36,7 +38,7 @@ class NonEmptyListTests extends CatsSuite {
   checkAll("Eq[NonEmptyList[ListWrapper[Int]]]", SerializableTests.serializable(Eq[NonEmptyList[ListWrapper[Int]]]))
 
   {
-    implicit val A = ListWrapper.partialOrder[Int]
+    implicit val A: PartialOrder[ListWrapper[Int]] = ListWrapper.partialOrder[Int]
     checkAll("NonEmptyList[ListWrapper[Int]]", OrderLaws[NonEmptyList[ListWrapper[Int]]].partialOrder)
     checkAll("PartialOrder[NonEmptyList[ListWrapper[Int]]]", SerializableTests.serializable(PartialOrder[NonEmptyList[ListWrapper[Int]]]))
 
@@ -44,7 +46,7 @@ class NonEmptyListTests extends CatsSuite {
   }
 
   {
-    implicit val A = ListWrapper.order[Int]
+    implicit val A: Order[ListWrapper[Int]] = ListWrapper.order[Int]
     checkAll("NonEmptyList[ListWrapper[Int]]", OrderLaws[NonEmptyList[ListWrapper[Int]]].order)
     checkAll("Order[NonEmptyList[ListWrapper[Int]]]", SerializableTests.serializable(Order[NonEmptyList[ListWrapper[Int]]]))

traverser.traverse(unit.body)

def toMetaType(tp: g.Tree) =
config.dialect(tp.toString).parse[m.Type].get
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is causing the errors.

@@ -13,6 +15,14 @@ case object ExplicitImplicit extends Rewrite {
case m.Term.ApplyType(m.Term.Name("implicitly"), _) => true
case _ => false
}
@scala.annotation.tailrec
final def parents(tree: Tree,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

@@ -8,14 +8,37 @@ class A {
implicit val x: List[Int] = List(1)
}
<<< map
import scala.concurrent.Future
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

token.input.eq(from.input) &&
token.end <= to.end &&
token.start >= from.start
sealed abstract class Patch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary now? If not remove

@@ -44,14 +67,124 @@ class A {
class A {
class B { class C }
implicit val x = new B
implicit val y = new x.C
implicit val y: x.C = new x.C
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the input, remove?

Given the ambiguity of term selects in Scala meta, we have to try luck
with every name and look it up with both type and term names. The
previous traversal assumed that once we found a symbol, we took the
right path, but this is not true:

```scala
object A
class A {
  type B
}
```

Looking up `A.B` would fail if the algorithm start looking term names.
`object A` would be found, but `type B` wouldn't since it's not defined
in the companion object but the type.

The new algorithm allow us to backtrack once we find these lookup
failures (as we're browsing through the RootPackage members, we know
that we will find at least one solution). Therefore, we perform a full
DFS pre-order traversal in the binary tree that represents all the
possibilities.
Copy link
Author

@jvican jvican left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone through the remaining set of comments. Current version has more documentation, a simplified implementation product of several iterations, and higher test covering (with added tests).

I have one question:

  • Where should I define exceptions for scalafix? Do you have the infrastructure for error handling yet? Perhaps it's better to use sys.error until you get to it?

And some remarks on the current implementation:

  • The errors in cats cannot be fixed with -Yrangepos. The type in the implicit val is inferred and cannot be fetched from the sources. Therefore, implicit val types whose types depend on the Kind projector cannot be fixed in a principled way. I suggest that we think about a low-level fix to still print the string representation of the types (remember that being an inferred type, its original tree is also an empty TypeTree).
  • We're still not handling imports inside DefTrees. I'll have a look at that tomorrow.
  • Implicit vals that depend on type alias will get the dealiased type.
type Foo = String
implicit val foo = new Foo

will be rewritten as:

implicit val foo: String = new Foo

I'm still thinking of a solution to this (if there's one, it's not clear to me we can be consistent with type aliases without introducing misbehaviours--scalac only give us the expanded type).

@@ -8,14 +8,37 @@ class A {
implicit val x: List[Int] = List(1)
}
<<< map
import scala.concurrent.Future
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

>>>
import scala.collection.immutable.{Map => MyMap}
class A {
implicit val x: MyMap[Int, String] =
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at the other two tests checking renames.

}
>>>
class A {
class B { class C }
implicit val x: B = new B
implicit val y: x.C = new x.C
}
<<< NOWRAP higher kinded cats
package cats {
Copy link
Author

@jvican jvican Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a reproduction of an intricate scenario. Even though it has the names cats, I would leave it here. If you disagree, please point to concrete file you'd like to have it.

}
}
<<< higher kinded
import scala.concurrent.Future
Copy link
Author

@jvican jvican Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this line is needed. If you have a look at the shortened Future in the implicit val type result, you'll see that the import oracle correctly shortens the type because it finds it in the context.

trait C {
class B
}
implicit val x: D.c.B = new D.c.B
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃

Copy link
Contributor

@xeno-by xeno-by left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have time to look at the algorithm in details, but went through the rest of the code.

>>>
import scala.collection.immutable.{Map => MyMap}
class A {
implicit val x: MyMap[Int, String] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed :)

object slick {
case class Supplier(id: Int, name: String)
implicit val supplierGetter = (arg: (Int, String)) => Supplier(arg._1, arg._2)
}
>>>
object slick {
case class Supplier(id: Int, name: String)
implicit val supplierGetter: ((Int, String)) => slick.Supplier = (arg: (Int, String)) => Supplier(arg._1, arg._2)
implicit val supplierGetter: ((Int, String)) => Supplier = (arg: (Int, String)) => Supplier(arg._1, arg._2)
}
<<< NOWRAP package import
package scala.concurrent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some tricky examples that come to mind:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package a {
  class C
}

package b {
  class C
}

package c {
  object M {
    import a._
    import b._
    implicit val c = new b.C
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trait T {
  class C
}

package object a extends T {
}

object M {
  import a._
  implicit val c = new C
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package a {
  class C
}

package b {
  class C
}

package c {
  object M {
    import a._
    locally {
      import b._
      implicit val c = new C
    }
  }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the code snippets, will add to tests. Forgot about our beloved locally 😄.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome tests! Very nice 👍

/** The compiler sets different symbols for `PackageDef`s
* and term names pointing to that package. Get the
* underlying symbol of `moduleClass` for them to be equal. */
@inline
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why @inline here and below?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can be removed.

val mixedScope = sc.cloneScope
sc2.foreach(s => mixedScope.enterIfNew(s))
mixedScope
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably use a dedicated data structure to manipulate scopes. Things like enterIfNew may have hidden performance costs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is indeed a performance cost attached to it. I'll study a better data structure to manipulate scopes in the future, right now we're focusing more on correctness. I've also realised that I should not be using enterIfNew in some concrete parts of the code. Fixing tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything trivial, e.g. case class Scope(syms: List[Symbol]), would do. The implementation of Scope is quite intricate, so it'll be just an unnecessary complication.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should optimize for correctness and simplicity for now. We can worry about performance later.

// Handle packages to import from user-defined compiler flags
val packagesToImportFrom = {
if (g.settings.noimports) Nil
else if (unit.isJava) RootImports.javaImports
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can unit.isJava ever be true?

Copy link
Author

@jvican jvican Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this logic from nsc Context just in case it is sometimes true. I would expect it to be false almost always, but better safe than sorry?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scalafix-nsc guards against unit.isJava, so this will always be false. I don't mind keeping it though for consistency with the compiler impl.

traverser.traverse(unit.body)

def toMetaType(tp: g.Tree) = {
config.dialect(tp.toString).parse[m.Type].get
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no guarantee that parse is going to succeed here. Do you plan to make this more robust?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually the cause of a problem with cats. We have no general solution so far, tp may not have a tree representation (inferred) and it's not fetchable from sources. Any suggestion @xeno-by?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait for a Type converter that is coming to 2.x along with nirvana and friends.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the type converter will solve the problem either. Kind-projector synthesizes unique type names for each appearance of Type.Name(?). The synthesized types look like this

cats.laws.discipline.CartesianTests.Isomorphisms[[β$0$]cats.free.Free[Option,β$0$]]

I'm not 100% sure, but I think those should be pretty-printed as ?. Maybe we'll need to special case kind-projector since it's a very popular compiler plugin.

The best solution for now is to gracefully handle the parse error and not shorten that type signature. The type signature of shortenType indicates that it's always a safe operation, and indeed, you can always choose not to shorten the type.

Copy link
Contributor

@xeno-by xeno-by Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can specialcase kind-projector, yeah.

The [T]Foo[T] type doesn't have surface syntax in Scala, so it'll have to be emulated - either with kind-projector or with the { type λ[T] = Foo[T] }#λ trick. This can be done automatically by the type converter.

new SemanticApi {
override def shortenType(tpe: m.Type, owner: m.Tree): m.Type = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should owner be renamed to atLocation to be consistent with the API?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If owner is only used for its position, why not pass a position here in the first place?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and yes. We decided to pass owner just in case we could need it afterwards, but it's unnecessary IIRC.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature is tricky because the current impl only works for defn.{val,var,def}, so the owner should really be a Defn, if anything. With a position, it wouldn't be clear which position you should pass in e.g. the defn's name or the defn itself.

IIRC, the owner needs to map to some tree with an ownerchain.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the disambiguation between defn and defn's name be done automatically?

val bottomUpScope = interestingOwners.iterator
.map(_.info.members.filter(keepRelevantMembers))
.reduce(mixScopes _)
val globalScope = mixScopes(bottomUpScope, userImportsScope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this take import priority into account?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't yet, thanks for pointing out.

inScope: g.Scope,
enclosingTerm: g.Symbol): m.Ref = {

val refNoThis = stripThis(toShorten)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this strip things like C.this.foo or only some.pkg.this.foo?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both, actually. They will be converted to C.foo and some.pkg.foo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if C is an enclosing class? Then C.foo doesn't make sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, Foo.this.bar can be stripped to bar in all cases except the following?

trait Foo {
  def bar
  trait Kas {
    def bar
    Foo.this.bar
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, yes.

@olafurpg
Copy link
Contributor

olafurpg commented Jan 25, 2017

BTW. Maybe move the new test cases to a ShortenType.source. We've gone waay beyond basic.source :D

@xeno-by xeno-by mentioned this pull request Jan 30, 2017
2 tasks
@jvican
Copy link
Author

jvican commented Feb 13, 2017

I'll rebase this on top of the semantic API soon.

@olafurpg olafurpg mentioned this pull request Mar 6, 2017
@olafurpg
Copy link
Contributor

olafurpg commented Apr 3, 2017

With the introduction of scalahost and the scala.meta semantic API, I believe context aware scope would be a great feature to have in scala.meta instead of scalafix. Also, scalafix-nsc is being phased out in favor of the offline Mirror so I think that is the best way forward.

@olafurpg olafurpg closed this Apr 3, 2017
@jvican
Copy link
Author

jvican commented Apr 3, 2017

@xeno-by I'm planning to follow up on this next weekend. Will you accept a PR fully implementing a symbol-based type shortener with the new semantic API?

@olafurpg
Copy link
Contributor

olafurpg commented Apr 3, 2017

I think a good start would be to open a ticket in scalameta listing the details of this feature, what methods/signatures it would have etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants