Skip to content
Permalink
Browse files

[scalac-plugin] Match component specs

  • Loading branch information
cchantep
cchantep committed Feb 23, 2014
1 parent b4d565d commit b5994bd1773f63e7c04eccc2b24b6378c10b8f4d
@@ -63,19 +63,17 @@ trait ScalaCompiler {
javacOptions in Test ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"),
compile in Test <<= (compile in Test).dependsOn(clean in Test).dependsOn(
packageBin in (plugin, Compile)/* make sure plugin.jar is available */),
scalacOptions <++= (version in ThisBuild).
scalacOptions in Test <++= (version in ThisBuild).
zip(baseDirectory in (plugin, Compile)).
zip(name in (plugin, Compile)) map { d =>
val ((v, b), n) = d
val j = b / "target" / "scala-2.10" / "%s_2.10-%s.jar".format(n, v)

Seq("-feature", "-deprecation",
Seq("-feature", "-deprecation", "-P:acolyte:debug",
"-Xplugin:%s".format(j.getAbsolutePath))
},
resolvers += "Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
autoCompilerPlugins := true,
addCompilerPlugin("org.eu.acolyte" %% "scala-compiler-plugin" % "1.0.14"),
libraryDependencies ++= Seq("org.specs2" %% "specs2" % "2.3.2"),
libraryDependencies ++= Seq("org.specs2" %% "specs2" % "2.3.2" % "test"),
publishTo := None).dependsOn(plugin)

}
@@ -1,4 +1,4 @@
<plugin>
<name>rich-match</name>
<classname>acolyte.MatchPlugin</classname>
<name>acolyte</name>
<classname>acolyte.AcolytePlugin</classname>
</plugin>
@@ -4,13 +4,24 @@ import scala.tools.nsc.Global
import scala.tools.nsc.plugins.{ Plugin, PluginComponent }
import scala.tools.nsc.transform.Transform

class MatchPlugin(val global: Global) extends Plugin {
val name = "rich-match"
val description = "Rich or refactored pattern matching"
class AcolytePlugin(val global: Global) extends Plugin {
val name = "acolyte"
val description = "Syntax extensions: rich pattern matching"
val components = List[PluginComponent](MatchComponent)

var debug: Boolean with NotNull = false

override def processOptions(options: List[String], error: String Unit) {
for (o options) {
if (o == "debug") debug = true
}
}

override val optionsHelp: Option[String] = Some(
" -P:acolyte:debug Enable debug")

private object MatchComponent extends PluginComponent with Transform {
val global: MatchPlugin.this.global.type = MatchPlugin.this.global
val global: AcolytePlugin.this.global.type = AcolytePlugin.this.global
override val runsRightAfter = Some("parser")
override val runsAfter = runsRightAfter.toList
override val runsBefore = List[String]("typer")
@@ -21,7 +32,7 @@ class MatchPlugin(val global: Global) extends Plugin {
object MatchTransformer extends global.Transformer {
import scala.collection.mutable.ListBuffer
import global.{
abort,
//abort,
reporter,
Block,
CaseDef,
@@ -32,8 +43,15 @@ class MatchPlugin(val global: Global) extends Plugin {
}

override def transform(tree: Tree): Tree = tree match {
case m @ Match(_, _) refactorMatch(m)
case _ super.transform(tree)
case m @ Match(_, _) {
val richMatch = refactorMatch(m)

if (debug) reporter.info(m.pos,
s"Rich Match refactored: ${global show richMatch}", true)

richMatch
}
case _ super.transform(tree)
}

val tildeTerm = global.newTermName("$tilde")
@@ -52,9 +70,9 @@ class MatchPlugin(val global: Global) extends Plugin {

val vds = ListBuffer[ValDef]()
val cds = cs.map {
case ocd @ CaseDef(Apply(Ident(it), xt), g, by) if (
case ocd @ CaseDef(Apply(Ident(it), x), g, by) if (
it == tildeTerm)
(xt.headOption, xt.tail) match {
(x.headOption, x.tail) match {
case (Some(xt @ Apply(ex, xa)), Apply(_, ua) :: Nil)
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
ua, g, by)
@@ -73,19 +91,17 @@ class MatchPlugin(val global: Global) extends Plugin {
vds += vd
cd

/*
case (Some(xt @ Apply(ex, xa)),
Literal(Constant(())) :: Nil) ⇒
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
List(Literal(Constant(()))), g, by)
vds += vd
cd
*/
case (Some(xt @ Apply(ex, xa)), Nil)
// no binding
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
Nil, g, by)
vds += vd
cd

case _
reporter.error(ocd.pos, "Invalid ~ pattern")
abort("Invalid ~ pattern")

//abort("Invalid ~ pattern"
ocd
}

case cd cd
@@ -96,7 +112,8 @@ class MatchPlugin(val global: Global) extends Plugin {
}
case _
reporter.error(orig.pos, "Invalid Match")
abort("Invalid Match")
//abort("Invalid Match")
orig
}

@inline private def caseDef[T](i: Int, cp: Position, xp: Position, ex: Tree, xa: List[Tree], ua: List[Tree], g: Tree, b: Tree): (ValDef, CaseDef) = {
@@ -124,30 +141,13 @@ class MatchPlugin(val global: Global) extends Plugin {

val vdp = xp.withPoint(0).withSource(new BatchSourceFile(file, vdc), 0)

/*
[error] /path/to/file.scala#refactored-match-M:1: Compilation error.
[error] Error details.
[error] val Xtr1 = B() // generated from ln L, col C
*/

// CaseDef
/*
val pat = ua match {
case Literal(Constant(())) :: Nil ⇒ Apply(Ident(xn), Nil)
case _ ⇒ Apply(Ident(xn), ua)
}
*/
val pat = Apply(Ident(xn), ua)
val cd = CaseDef(pat, g, b)
val cdc =
s"${show(cd)} // generated from ln ${cp.line}, col ${cp.column - 5}"
val cdp = cp.withPoint(0).withSource(new BatchSourceFile(file, cdc), 0)

/*
[error] /path/to/file.scala#refactored-match-M:1: value Xtr0 is not a case class constructor, nor does it have an unapply/unapplySeq method
[error] case Xtr1((a @ _)) => Nil // generated from ln L, col C
*/

(atPos(vdp)(vd), atPos(cdp)(cd))
}
}
@@ -0,0 +1,50 @@
# Acolyte Scala compiler plugin

## Match component

Scala pattern matching: case class | extractor
Extract created with arg -> Stable identifier (ex regexp)
-> ~(..., ...)

```scala
case class Regex(e: String) {
lazy val re = e.r
def unapplySeq(target: Any): Option[List[String]] = re.unapplySeq(target)
}
```

```scala
str match {
case ~(Regex("^a.*")) 1
case ~(Regex("# ([A-Z]+).*"), a) 2
case ~(Regex("([0-9]+);([a-z]+)"), (a, b)) 3
case _ 4
}
```

## Plugin options

-P:acolyte:debug

## Compilation errors

```
[error] /path/to/file.scala#refactored-match-M:1: Compilation error.
[error] Error details.
[error] val Xtr1 = B() // generated from ln L, col C
```

```
[error] /path/to/file.scala#refactored-match-M:1: value Xtr0 is not a case class constructor, nor does it have an unapply/unapplySeq method
[error] case Xtr1((a @ _)) => Nil // generated from ln L, col C
```

## SBT usage

```scala
autoCompilerPlugins := true
addCompilerPlugin("org.eu.acolyte" %% "scala-compiler-plugin" % "1.0.14")
scalacOptions += "-P:acolyte:debug"
```
@@ -0,0 +1,87 @@
package acolyte

object MatchComponentSpec
extends org.specs2.mutable.Specification with MatchTest {

"Match component" title

"Basic Pattern matching" should {
"match extractor: Integer(n)" in {
patternMatching("456") aka "matching" mustEqual List("num-456")
}

"not match" in {
patternMatching("@") aka "matching" mustEqual Nil
}
}

"Extract with unapply" should {
"rich match without binding: ~(IntRange(5, 10))" in {
patternMatching("7") aka "matching" mustEqual List("5-to-10")
}

"rich match without binding: ~(IntRange(10, 20), i)" in {
patternMatching("12") aka "matching" mustEqual List("range:12")
}
}

"Extractor with unapplySeq" should {
"rich match without binding: ~(Regex(re))" in {
patternMatching("abc") aka "matching" mustEqual List("no-binding")
}

"rich match with one binding: ~(Regex(re), a)" in {
patternMatching("# BCD.") aka "matching" mustEqual List("BCD")
}

"rich match with several bindings: ~(Regex(re), (a, b))" in {
patternMatching("123;xyz") aka "matching" mustEqual List("xyz", "123")
}
}
}

sealed trait MatchTest {
def patternMatching(s: String): List[String] = s match {
case ~(IntRange(5, 10)) List("5-to-10")
case ~(IntRange(10, 20), i) List(s"range:$i")
case Integer(n) List(s"num-$n")
case ~(Regex("^a.*")) List("no-binding")
case ~(Regex("# ([A-Z]+).*"), a) List(a)
case ~(Regex("([0-9]+);([a-z]+)"), (a, b)) List(b, a)
case x Nil
}
}

/**
* Extractor factory based on regular expression.
*
* {{{
* val res: Boolean = rmatch {
* ~("abc") match {
* case ~(Regex("(.+)b([a-z]+)$"), (x, y)) => true // x == "a", y == "b"
* case _ => false
* }
* }
* }}}
*/
sealed case class Regex(e: String) {
lazy val re = e.r

/** See [[scala.util.matching.Regex.unapplySeq]]. */
def unapplySeq(target: Any): Option[List[String]] = re.unapplySeq(target)
}

/** Integer extractor */
object Integer {
val re = "^[0-9]+$".r
def unapply(v: String): Option[String] =
re.unapplySeq(v).fold[Option[String]](None)(_ Some(v))
}

/** Integer range extractor */
sealed case class IntRange(min: Int, max: Int) {
def unapply(v: String): Option[Int] = Integer.unapply(v) flatMap { s
val i = s.toInt
if (i < min || i > max) None else Some(i)
}
}

This file was deleted.

0 comments on commit b5994bd

Please sign in to comment.
You can’t perform that action at this time.