Skip to content
This repository was archived by the owner on Mar 27, 2023. It is now read-only.

Commit 28caa53

Browse files
authored
Merge pull request #23 from ShaneDelmore/add_advice_from_resources
Add advice from resources
2 parents 28c5f8e + 99872bd commit 28caa53

File tree

7 files changed

+100
-48
lines changed

7 files changed

+100
-48
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ Then create a file named .clippy.json in the root of your project directory and
6161
"version": "0.3.1",
6262
"advices": [
6363
{
64-
"id": 2,
6564
"error": {
6665
"type": "typeMismatch",
6766
"found": "scala\\.concurrent\\.Future\\[Int\\]",
@@ -78,6 +77,15 @@ Then create a file named .clippy.json in the root of your project directory and
7877
}
7978
````
8079

80+
# Library specific advice
81+
82+
If you have advice that is specific to a library or library version you can also bundle the advice with your library.
83+
If your users have Scala-Clippy installed they will see your advice if your library is inclued in their project.
84+
This can be helpful in the common case where users of your library need specific imports to be able to use your functionality.
85+
To bundle clippy advice with your library just put it in a file named clippy.json in your resources directory
86+
87+
For examples on how to write tests for advice to ensure it does not go out of date see [CompileTests.scala](./tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala)
88+
8189
# Alternative ways to use Clippy
8290

8391
You can also use Clippy directly as a compiler plugin. If you use SBT, add the following setting to your

model/src/main/scala/com/softwaremill/clippy/Advice.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ package com.softwaremill.clippy
22

33
import org.json4s.JsonAST._
44

5-
case class Advice(id: Long, compilationError: CompilationError[RegexT], advice: String, library: Library) {
5+
case class Advice(compilationError: CompilationError[RegexT], advice: String, library: Library) {
66
def errMatching: PartialFunction[CompilationError[ExactT], String] = {
77
case ce if compilationError.matches(ce) => advice
88
}
99

1010
def toJson: JValue = JObject(
11-
"id" -> JInt(id),
1211
"error" -> compilationError.toJson,
1312
"text" -> JString(advice),
1413
"library" -> library.toJson
@@ -19,12 +18,11 @@ object Advice {
1918
def fromJson(jvalue: JValue): Option[Advice] = {
2019
(for {
2120
JObject(fields) <- jvalue
22-
JField("id", JInt(id)) <- fields
2321
JField("error", errorJV) <- fields
2422
error <- CompilationError.fromJson(errorJV).toList
2523
JField("text", JString(text)) <- fields
2624
JField("library", libraryJV) <- fields
2725
library <- Library.fromJson(libraryJV).toList
28-
} yield Advice(id.longValue(), error, text, library)).headOption
26+
} yield Advice(error, text, library)).headOption
2927
}
3028
}

plugin/src/main/scala/com/softwaremill/clippy/AdviceLoader.scala

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,30 @@ import scala.concurrent.{ExecutionContext, Future}
1010
import scala.io.Source
1111
import scala.tools.nsc.Global
1212
import scala.util.{Failure, Success, Try}
13+
import scala.collection.JavaConversions._
1314

1415
class AdviceLoader(global: Global, url: String, localStoreDir: File, projectAdviceFile: Option[File])(implicit ec: ExecutionContext) {
1516
private val OneDayMillis = 1000L * 60 * 60 * 24
1617

1718
private val localStore = new File(localStoreDir, "clippy.json.gz")
1819

20+
private val resourcesAdvice: List[Advice] =
21+
getClass.getClassLoader
22+
.getResources("clippy.json")
23+
.toList
24+
.flatMap(loadAdviceFromUrL)
25+
26+
private def loadAdviceFromUrL(url: URL): List[Advice] =
27+
TryWith(url.openStream())(inputStreamToClippy(_).advices) match {
28+
case Success(advices) => advices
29+
case Failure(_) =>
30+
global.warning(s"Cannot load advice from ${url.getPath} : Ignoring.")
31+
Nil
32+
}
33+
1934
private val projectAdvice: List[Advice] =
20-
projectAdviceFile.flatMap { file =>
21-
Try(loadLocally(file))
22-
.map(bytes => inputStreamToClippy(decodeUtf8Bytes(bytes)).advices)
23-
.recover {
24-
case e: Exception =>
25-
global.warning(s"Cannot load advice from project store: $file. Ignoring.")
26-
throw e
27-
}.toOption
28-
}.getOrElse(Nil)
35+
projectAdviceFile.map(file =>
36+
loadAdviceFromUrL(file.toURI.toURL)).getOrElse(Nil)
2937

3038
def load(): Future[Clippy] = {
3139
val localClippy = if (!localStore.exists()) {
@@ -52,8 +60,9 @@ class AdviceLoader(global: Global, url: String, localStoreDir: File, projectAdvi
5260
}
5361
}
5462

55-
// Add in project specific advice
56-
localClippy.map(clippy => clippy.copy(advices = projectAdvice ++ clippy.advices))
63+
// Add in advice found in resources and project root
64+
localClippy.map(clippy =>
65+
clippy.copy(advices = (projectAdvice ++ resourcesAdvice ++ clippy.advices).distinct))
5766
}
5867

5968
private def fetchStoreParse(): Future[Clippy] =
@@ -99,8 +108,7 @@ class AdviceLoader(global: Global, url: String, localStoreDir: File, projectAdvi
99108
if (!localStoreDir.isDirectory && !localStoreDir.mkdir()) {
100109
throw new IOException(s"Cannot create directory $localStoreDir")
101110
}
102-
val os = new FileOutputStream(localStore)
103-
try os.write(bytes) finally os.close()
111+
TryWith(new FileOutputStream(localStore))(_.write(bytes)).get
104112
}
105113

106114
private def storeLocallyInBackground(bytes: Array[Byte]): Unit = {

plugin/src/main/scala/com/softwaremill/clippy/ClippyPlugin.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ class ClippyPlugin(val global: Global) extends Plugin {
2828
global.reporter = new DelegatingReporter(r, handleError)
2929

3030
def handleError(pos: Position, msg: String): String = {
31-
val totalMatchingFunction = advices.map(_.errMatching)
32-
.foldLeft(PartialFunction.empty[CompilationError[ExactT], String])(_.orElse(_)).lift
33-
val adviceText = CompilationErrorParser.parse(msg).flatMap(totalMatchingFunction).map("\n Clippy advises: " + _).getOrElse("")
34-
msg + adviceText
31+
val parsedMsg = CompilationErrorParser.parse(msg)
32+
val matchers = advices.map(_.errMatching.lift)
33+
val matches = matchers.flatMap(pf => parsedMsg.flatMap(pf))
34+
35+
matches.size match {
36+
case 0 =>
37+
msg
38+
case 1 =>
39+
matches.mkString(s"$msg\n Clippy advises: ", "", "")
40+
case _ =>
41+
matches.mkString(s"$msg\n Clippy advises you to try one of these solutions: \n ", "\n or\n ", "")
42+
}
3543
}
3644
}
3745

plugin/src/main/scala/com/softwaremill/clippy/Utils.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.softwaremill.clippy
22

33
import java.io.{ByteArrayOutputStream, InputStream}
4+
import java.io.Closeable
5+
import scala.util.control.NonFatal
6+
import scala.util.{Failure, Try}
7+
48

59
object Utils {
610
/**
@@ -41,4 +45,26 @@ object Utils {
4145
}
4246
finally is.close()
4347
}
48+
49+
object TryWith {
50+
def apply[C <: Closeable, R](resource: => C)(f: C => R): Try[R] =
51+
Try(resource).flatMap(resourceInstance => {
52+
try {
53+
val returnValue = f(resourceInstance)
54+
Try(resourceInstance.close()).map(_ => returnValue)
55+
}
56+
catch {
57+
case NonFatal(exceptionInFunction) =>
58+
try {
59+
resourceInstance.close()
60+
Failure(exceptionInFunction)
61+
}
62+
catch {
63+
case NonFatal(exceptionInClose) =>
64+
exceptionInFunction.addSuppressed(exceptionInClose)
65+
Failure(exceptionInFunction)
66+
}
67+
}
68+
})
69+
}
4470
}

tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.util.zip.GZIPOutputStream
66
import com.softwaremill.clippy._
77
import org.scalatest.{BeforeAndAfterAll, Matchers, FlatSpec}
88

9+
import scala.tools.reflect.ToolBox
910
import scala.tools.reflect.ToolBoxError
1011

1112
class CompileTests extends FlatSpec with Matchers with BeforeAndAfterAll {
@@ -26,31 +27,32 @@ class CompileTests extends FlatSpec with Matchers with BeforeAndAfterAll {
2627

2728
val advices = List(
2829
Advice(
29-
1L,
3030
TypeMismatchError(ExactT("slick.dbio.DBIOAction[*]"), None, ExactT("slick.lifted.Rep[Option[*]]"), None).asRegex,
3131
"Perhaps you forgot to call .result on your Rep[]? This will give you a DBIOAction that you can compose with other DBIOActions.",
3232
Library("com.typesafe.slick", "slick", "3.1.0")
3333
),
3434
Advice(
35-
2L,
3635
TypeMismatchError(ExactT("akka.http.scaladsl.server.StandardRoute"), None, ExactT("akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]"), None).asRegex,
3736
"did you forget to define an implicit akka.stream.ActorMaterializer? It allows routes to be converted into a flow. You can read more at http://doc.akka.io/docs/akka-stream-and-http-experimental/2.0/scala/http/routing-dsl/index.html",
3837
Library("com.typesafe.akka", "akka-http-experimental", "2.0.0")
3938
),
4039
Advice(
41-
3L,
4240
NotFoundError(ExactT("value wire")).asRegex,
4341
"you need to import com.softwaremill.macwire._",
4442
Library("com.softwaremill.macwire", "macros", "2.0.0")
4543
),
4644
Advice(
47-
4L,
45+
NotFoundError(ExactT("value wire")).asRegex,
46+
"If you need further help check out the macwire readme at https://github.com/adamw/macwire",
47+
Library("com.softwaremill.macwire", "macros", "2.0.0")
48+
),
49+
Advice(
4850
TypeArgumentsDoNotConformToOverloadedBoundsError(
49-
ExactT("*"), ExactT("value apply"), Set(
50-
ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"),
51-
ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]")
52-
)
53-
).asRegex,
51+
ExactT("*"), ExactT("value apply"), Set(
52+
ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"),
53+
ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]")
54+
)
55+
).asRegex,
5456
"incorrect class name passed to TableQuery",
5557
Library("com.typesafe.slick", "slick", "3.1.1")
5658
)
@@ -110,24 +112,26 @@ class CompileTests extends FlatSpec with Matchers with BeforeAndAfterAll {
110112
""".stripMargin
111113
)
112114

115+
val tb = {
116+
val cpp = sys.env("CLIPPY_PLUGIN_PATH")
117+
118+
import scala.reflect.runtime._
119+
val cm = universe.runtimeMirror(getClass.getClassLoader)
120+
121+
cm.mkToolBox(options = s"-Xplugin:$cpp -Xplugin-require:clippy")
122+
}
123+
124+
def parse(snippet: String) = tb.eval(tb.parse(snippet))
125+
113126
for ((name, s) <- snippets) {
114127
name should "compile with errors" in {
115-
val cpp = sys.env("CLIPPY_PLUGIN_PATH")
116-
117-
import scala.reflect.runtime._
118-
val cm = universe.runtimeMirror(getClass.getClassLoader)
119-
120-
import scala.tools.reflect.ToolBox
121-
val tb = cm.mkToolBox(options = s"-Xplugin:$cpp -Xplugin-require:clippy")
122-
123-
try {
124-
tb.eval(tb.parse(s))
125-
fail("Should report compile errors")
126-
}
127-
catch {
128-
case e: ToolBoxError =>
129-
e.message should include("Clippy advises")
130-
}
128+
(the[ToolBoxError] thrownBy parse(s)).message should include("Clippy advises")
131129
}
132130
}
131+
132+
"Clippy" should "return all matching advice" in {
133+
(the[ToolBoxError] thrownBy parse(snippets("macwire")))
134+
.message should include("Clippy advises you to try one of these")
135+
}
136+
133137
}

ui/app/dal/AdvicesRepository.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class AdvicesRepository(database: SqlDatabase, idGenerator: IdGenerator)(implici
5454
case class StoredAdvice(id: Long, errorTextRaw: String, patternRaw: String, compilationError: CompilationError[RegexT],
5555
advice: String, state: AdviceState, library: Library, contributor: Contributor, comment: Option[String]) {
5656

57-
def toAdvice = Advice(id, compilationError, advice, library)
57+
def toAdvice = Advice(compilationError, advice, library)
5858
def toAdviceListing = AdviceListing(id, compilationError, advice, library,
5959
ContributorListing(contributor.github, contributor.twitter))
6060
}

0 commit comments

Comments
 (0)