├── .gitignore ├── COPYING ├── README.md ├── build.sbt ├── misc ├── LICENSE-paulp └── LICENSE-scalac.md ├── project ├── BuildCommon.scala ├── build.properties └── plugins.sbt ├── src ├── main │ ├── resources │ │ └── scalac-plugin.xml │ └── scala │ │ └── com │ │ └── fortysevendeg │ │ └── commas │ │ └── TrailingCommaPlugin.scala └── test │ └── scala │ ├── arguments.scala │ └── importSelectors.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | bower_components 9 | node_modules 10 | .idea 11 | .tmp 12 | 13 | *.iml 14 | /out 15 | .idea_modules 16 | .classpath 17 | .project 18 | /RUNNING_PID 19 | .settings 20 | .sass-cache 21 | scalajvm/upload/* 22 | 23 | # temp files 24 | .~* 25 | *~ 26 | *.orig 27 | 28 | # eclipse 29 | .scala_dependencies 30 | .buildpath 31 | .cache 32 | .target 33 | bin/ 34 | .ensime 35 | .ensime_cache 36 | 37 | # OSX 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 47 Degrees and Andy Scott. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commas 2 | 3 | A Scala compiler plugin that adds support for trailing commas. 4 | 5 | ### Usage 6 | 7 | Currently only a snapshot is available. 8 | 9 | resolvers += Opts.resolver.sonatypeSnapshots 10 | addCompilerPlugin("fail.sauce" %% "commas" % "0.1.1-SNAPSHOT") 11 | 12 | Now you can leave trailing commas in your import selectors and 13 | argument lists. 14 | 15 | ``` scala 16 | 17 | import scala.collection.immutable.{ Seq, Set, List, } 18 | 19 | val foo = List( 20 | "A", 21 | "B", 22 | "C", 23 | ) 24 | ``` 25 | 26 | ### Why? 27 | 28 | This is currently a proof of concept, for fun. 29 | 30 | ### How? 31 | 32 | The plugin hijacks the parser phase and replaces it with 33 | a customized parser that allows trailing commas in some 34 | scenarios. The hijacking is done with a mixture of reflection 35 | and carefully overridden fields. 36 | Paul Phillips' [fork of Scala][policy] provided the basis for 37 | the parser changes. 38 | 39 | [policy]: https://github.com/paulp/policy/commit/ead099046c6d2ad2544e494d6cfd091ff7fa33ec 40 | 41 | ### License 42 | 43 | See [COPYING](COPYING) and also: 44 | - [Scalac's license](misc/LICENSE-scalac.md) for "overridden" code from the Scala compiler 45 | - [Paul's policy license](misc/LICENSE-paulp) for the original adjustment to the Scala parser 46 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * commas, copyright 2016 Andy Scott 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | val commas = (project in file(".")) 19 | 20 | name := "commas" 21 | oscalaVersion := "2.11.7" 22 | 23 | homepage := 24 | Some(url("https://github.com/47deg/scala-commas")) 25 | 26 | libraryDependencies <++= scalaVersion(scalaVersion => Seq( 27 | "org.scala-lang" % "scala-compiler" % scalaVersion 28 | )) 29 | 30 | scalacOptions in console in Compile += "-Xplugin:" + (packageBin in Compile).value 31 | scalacOptions in Test += "-Xplugin:" + (packageBin in Compile).value 32 | 33 | 34 | publishMavenStyle := true 35 | publishArtifact in Test := false 36 | pomIncludeRepository := Function.const(false) 37 | 38 | publishTo := Some(if (isSnapshot.value) Opts.resolver.sonatypeSnapshots else Opts.resolver.sonatypeStaging) 39 | 40 | pomExtra := ( 41 | 42 | git@github.com:47deg/scala-commas.git 43 | scm:git:git@github.com:47deg/scala-commas.git 44 | 45 | 46 | 47 | andyscott 48 | Andy Scott 49 | http://github.com/andyscott/ 50 | 51 | 52 | ) 53 | -------------------------------------------------------------------------------- /misc/LICENSE-paulp: -------------------------------------------------------------------------------- 1 | Copyright 2014 Paul Phillips 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | https://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | -------------------------------------------------------------------------------- /misc/LICENSE-scalac.md: -------------------------------------------------------------------------------- 1 | Scala is licensed under the [BSD 3-Clause License](http://opensource.org/licenses/BSD-3-Clause). 2 | 3 | ## Scala License 4 | 5 | Copyright (c) 2002-2016 EPFL 6 | 7 | Copyright (c) 2011-2016 Lightbend, Inc. 8 | 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without modification, 12 | are permitted provided that the following conditions are met: 13 | 14 | * Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | * Neither the name of the EPFL nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 27 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 28 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | # Other Licenses 36 | 37 | This software includes projects with the following licenses, 38 | which are also included in the `licenses/` directory: 39 | 40 | ### [Apache License](http://www.apache.org/licenses/LICENSE-2.0.html) 41 | This license is used by the following third-party libraries: 42 | 43 | * jansi 44 | 45 | ### [BSD License](http://www.opensource.org/licenses/bsd-license.php) 46 | This license is used by the following third-party libraries: 47 | 48 | * jline 49 | 50 | ### [BSD 3-Clause License](http://opensource.org/licenses/BSD-3-Clause) 51 | This license is used by the following third-party libraries: 52 | 53 | * asm 54 | 55 | ### [MIT License](http://www.opensource.org/licenses/MIT) 56 | This license is used by the following third-party libraries: 57 | 58 | * jquery 59 | * jquery-ui 60 | * jquery-layout 61 | * sizzle 62 | * tools tooltip 63 | 64 | ### Public Domain 65 | The following libraries are freely available in the public domain: 66 | 67 | * forkjoin 68 | 69 | -------------------------------------------------------------------------------- /project/BuildCommon.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | 4 | import scalariform.formatter.preferences._ 5 | import com.typesafe.sbt.SbtScalariform 6 | import com.typesafe.sbt.SbtScalariform.ScalariformKeys 7 | import de.heikoseeberger.sbtheader.AutomateHeaderPlugin 8 | import de.heikoseeberger.sbtheader.HeaderPattern 9 | import de.heikoseeberger.sbtheader.HeaderPlugin 10 | import de.heikoseeberger.sbtheader.HeaderKey.headers 11 | 12 | import scala.{ Console => C } 13 | 14 | object BuildCommon extends AutoPlugin { 15 | 16 | override def requires = plugins.JvmPlugin && SbtScalariform && HeaderPlugin 17 | override def trigger = allRequirements 18 | 19 | override def projectSettings = 20 | baseSettings ++ 21 | formatSettings ++ 22 | miscSettings 23 | AutomateHeaderPlugin.projectSettings 24 | 25 | private[this] def baseSettings = Seq( 26 | organization := "com.fortysevendeg", 27 | scalacOptions ++= Seq( 28 | "-deprecation", "-feature", "-unchecked", "-encoding", "utf8"), 29 | scalacOptions ++= Seq( 30 | "-language:implicitConversions", 31 | "-language:higherKinds"), 32 | javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options"), 33 | headers <<= (name, version) { (name, version) => Map( 34 | "scala" -> ( 35 | HeaderPattern.cStyleBlockComment, 36 | s"""|/* 37 | | * $name, Copyright 2016 47 Degrees and Andy Scott 38 | | */ 39 | | 40 | |""".stripMargin) 41 | )} 42 | ) 43 | 44 | private[this] def formatSettings = SbtScalariform.scalariformSettings ++ Seq( 45 | ScalariformKeys.preferences := ScalariformKeys.preferences.value 46 | .setPreference(SpacesAroundMultiImports, true) 47 | .setPreference(PreserveSpaceBeforeArguments, true) 48 | .setPreference(DanglingCloseParenthesis, Preserve) 49 | .setPreference(AlignArguments, true) 50 | .setPreference(AlignSingleLineCaseStatements, true) 51 | //.setPreference(DoubleIndentMethodDeclaration, true) 52 | .setPreference(MultilineScaladocCommentsStartOnFirstLine, true) 53 | .setPreference(PlaceScaladocAsterisksBeneathSecondAsterisk, true) 54 | .setPreference(RewriteArrowSymbols, true) 55 | ) 56 | 57 | private[this] def miscSettings = Seq( 58 | shellPrompt := { s => 59 | s"${C.BLUE}❤️${Project.extract(s).currentProject.id}!! ${C.RESET} " } 60 | ) 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.5.1") 2 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") 3 | 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 5 | -------------------------------------------------------------------------------- /src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | trailing-commas 3 | com.fortysevendeg.commas.TrailingCommaPlugin 4 | 5 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/commas/TrailingCommaPlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * commas, Copyright 2016 Andy Scott 3 | */ 4 | 5 | package com.fortysevendeg.commas 6 | 7 | import scala.tools.nsc.{ Global ⇒ NscGlobal, SubComponent, Phase } 8 | import scala.tools.nsc.plugins.{ Plugin ⇒ NscPlugin } 9 | import scala.tools.nsc.ast.parser.{ SyntaxAnalyzer ⇒ NscSyntaxAnalyzer } 10 | 11 | /** Trailing comma plugin. */ 12 | class TrailingCommaPlugin(val global: NscGlobal) 13 | extends NscPlugin with PhaseJacking { 14 | 15 | override val name = "trailing-commas" 16 | override val description = "adds support for trailing commas" 17 | override val components = Nil 18 | 19 | { 20 | val newSyntaxAnalyzer = new TrailingCommaSyntaxAnalyzer(global) 21 | 22 | // reflection is safe... right? 23 | hijackField("syntaxAnalyzer", newSyntaxAnalyzer) 24 | hijackPhase("parser", newSyntaxAnalyzer) 25 | 26 | if (global.syntaxAnalyzer != newSyntaxAnalyzer) 27 | sys.error("failed to hijack parser") 28 | } 29 | } 30 | 31 | /** Our customized syntax analyzer phase */ 32 | private[commas] class TrailingCommaSyntaxAnalyzer(val global: NscGlobal) 33 | extends NscSyntaxAnalyzer { 34 | 35 | override val runsAfter = List[String]() 36 | override val runsRightAfter = None 37 | 38 | import global._ 39 | 40 | // It would be great to override newUnitParser, except you can't. So 41 | // instead we override newPhase and copy all the transitive code until 42 | // newUnitParser is called. 43 | def newUnitParser(unit: CompilationUnit) = new UnitParser(unit) with TCParser 44 | 45 | // ripped/copied, see above 46 | override def newPhase(prev: Phase): StdPhase = new ParserPhase(prev) 47 | 48 | // ripped/copied, see above 49 | private[this] def initialUnitBody(unit: CompilationUnit): Tree = { 50 | if (unit.isJava) new JavaUnitParser(unit).parse() 51 | else if (currentRun.parsing.incompleteHandled) newUnitParser(unit).parse() 52 | else newUnitParser(unit).smartParse() 53 | } 54 | 55 | // ripped/copied, see above 56 | private[this] class ParserPhase(prev: Phase) extends StdPhase(prev) { 57 | override val checkable = false 58 | override val keepsTypeParams = false 59 | def apply(unit: CompilationUnit) { 60 | informProgress("parsing " + unit) 61 | if (unit.body == EmptyTree) unit.body = initialUnitBody(unit) 62 | if (settings.Yrangepos && !reporter.hasErrors) validatePositions(unit.body) 63 | if (settings.Ymemberpos.isSetByUser) 64 | new MemberPosReporter(unit) show (style = settings.Ymemberpos.value) 65 | } 66 | } 67 | 68 | /** Our changes to the Scala parser */ 69 | sealed trait TCParser { self: Parser ⇒ 70 | 71 | import scala.tools.nsc.ast.parser.Tokens._ 72 | import scala.collection.mutable.ListBuffer 73 | 74 | // The basis for trait is Paul Phillip's fork of Scala, located at 75 | // https://github.com/paulp/policy 76 | 77 | // trailing commas for arguments 78 | override def argumentExprs(): List[Tree] = { 79 | def args(): List[Tree] = commaSeparatedOrTrailing( 80 | RPAREN, 81 | if (isIdent) treeInfo.assignmentToMaybeNamedArg(expr()) else expr() 82 | ) 83 | in.token match { 84 | case LBRACE ⇒ List(blockExpr()) 85 | case LPAREN ⇒ inParens(if (in.token == RPAREN) Nil else args()) 86 | case _ ⇒ Nil 87 | } 88 | } 89 | 90 | // trailing commas for imports selectors 91 | override def importSelectors(): List[ImportSelector] = { 92 | val selectors = inBracesOrNil(commaSeparatedOrTrailing( 93 | RBRACE, importSelector())) 94 | selectors.init foreach { 95 | case ImportSelector(nme.WILDCARD, pos, _, _) ⇒ 96 | syntaxError(pos, "Wildcard import must be in last position") 97 | case _ ⇒ () 98 | } 99 | selectors 100 | } 101 | 102 | // This is from Paul 103 | @inline private[this] final def commaSeparatedOrTrailing[T]( 104 | end: Token, part: ⇒ T 105 | ): List[T] = { 106 | val ts = ListBuffer[T](part) 107 | while (in.token == COMMA) { 108 | accept(COMMA) 109 | if (in.token == end) // that was a trailing comma 110 | return ts.toList 111 | else 112 | ts += part 113 | } 114 | ts.toList 115 | } 116 | 117 | } 118 | 119 | } 120 | 121 | /** Hijacking helper for compiler phases. */ 122 | private[commas] sealed trait PhaseJacking { self: NscPlugin ⇒ 123 | 124 | /** Hijack a field from global */ 125 | protected[this] final def hijackField[T](name: String, newValue: T): T = { 126 | val field = classOf[NscGlobal].getDeclaredField(name) 127 | field.setAccessible(true) 128 | val oldValue = field.get(global).asInstanceOf[T] 129 | field.set(global, newValue) 130 | oldValue 131 | } 132 | 133 | /** Hijack a phase from global */ 134 | protected[this] final def hijackPhase( 135 | name: String, newPhase: SubComponent 136 | ): Option[SubComponent] = { 137 | 138 | val phasesSet = classOf[NscGlobal].getDeclaredMethod("phasesSet") 139 | .invoke(global).asInstanceOf[scala.collection.mutable.Set[SubComponent]] 140 | 141 | phasesSet.find(_.phaseName == name).map { oldPhase ⇒ 142 | phasesSet -= oldPhase 143 | phasesSet += newPhase 144 | oldPhase 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/test/scala/arguments.scala: -------------------------------------------------------------------------------- 1 | object ArgumentCommas extends App { 2 | 3 | val foo: List[String] = List( 4 | "A", 5 | "B", 6 | "C", 7 | "D", 8 | ) 9 | 10 | println(foo) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/scala/importSelectors.scala: -------------------------------------------------------------------------------- 1 | object ImportSelectorCommas extends App { 2 | 3 | import scala.collection.immutable.{ 4 | List, 5 | Seq, 6 | Set, 7 | Map, 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.1.1-SNAPSHOT" 2 | --------------------------------------------------------------------------------