Repository: softwaremill/scala-clippy Branch: master Commit: 6829f56c1042 Files: 77 Total size: 185.5 KB Directory structure: gitextract_gwq_0d06/ ├── .gitignore ├── .scalafmt.conf ├── .travis.yml ├── LICENSE.txt ├── README.md ├── build.sbt ├── model/ │ └── src/ │ ├── main/ │ │ └── scala/ │ │ └── com/ │ │ └── softwaremill/ │ │ └── clippy/ │ │ ├── Advice.scala │ │ ├── Clippy.scala │ │ ├── CompilationError.scala │ │ ├── CompilationErrorParser.scala │ │ ├── Library.scala │ │ ├── StringDiff.scala │ │ ├── Template.scala │ │ └── Warning.scala │ └── test/ │ └── scala/ │ └── com/ │ └── softwaremill/ │ └── clippy/ │ ├── CompilationErrorParserTest.scala │ ├── CompilationErrorTest.scala │ ├── LibraryProperties.scala │ ├── RegexTTest.scala │ ├── StringDiffSpecification.scala │ ├── StringDiffTest.scala │ └── TypeNamesGenerators.scala ├── package.json ├── plugin/ │ └── src/ │ └── main/ │ ├── resources/ │ │ └── scalac-plugin.xml │ ├── scala/ │ │ └── com/ │ │ └── softwaremill/ │ │ └── clippy/ │ │ ├── AdviceLoader.scala │ │ ├── ClippyPlugin.scala │ │ ├── ColorsConfig.scala │ │ ├── FailOnWarningsReporter.scala │ │ ├── Highlighter.scala │ │ ├── InjectReporter.scala │ │ ├── RestoreReporter.scala │ │ └── Utils.scala │ ├── scala-2.11/ │ │ └── com/ │ │ └── softwaremill/ │ │ └── clippy/ │ │ ├── DelegatingPosition.scala │ │ └── DelegatingReporter.scala │ └── scala-2.12/ │ └── com/ │ └── softwaremill/ │ └── clippy/ │ ├── DelegatingPosition.scala │ └── DelegatingReporter.scala ├── plugin-sbt/ │ └── src/ │ └── main/ │ └── scala/ │ └── com/ │ └── softwaremill/ │ └── clippy/ │ └── ClippySbtPlugin.scala ├── project/ │ ├── build.properties │ └── plugins.sbt ├── tests/ │ └── src/ │ └── test/ │ └── scala/ │ └── org/ │ └── softwaremill/ │ └── clippy/ │ └── CompileTests.scala ├── ui/ │ ├── app/ │ │ ├── ClippyApplicationLoader.scala │ │ ├── api/ │ │ │ └── UiApiImpl.scala │ │ ├── assets/ │ │ │ └── stylesheets/ │ │ │ └── main.less │ │ ├── controllers/ │ │ │ ├── AdvicesController.scala │ │ │ ├── ApplicationController.scala │ │ │ ├── AutowireController.scala │ │ │ └── HttpsFilter.scala │ │ ├── dal/ │ │ │ └── AdvicesRepository.scala │ │ ├── util/ │ │ │ ├── ConfigWithDefault.scala │ │ │ ├── DatabaseConfig.scala │ │ │ ├── SqlDatabase.scala │ │ │ ├── Zip.scala │ │ │ └── email/ │ │ │ ├── DummyEmailService.scala │ │ │ ├── EmailService.scala │ │ │ └── SendgridEmailService.scala │ │ └── views/ │ │ ├── index.scala.html │ │ └── use.scala.html │ ├── conf/ │ │ ├── application.conf │ │ ├── db/ │ │ │ └── migration/ │ │ │ ├── V1__create_schema.sql │ │ │ └── V2__add_pattern.sql │ │ ├── logback.xml │ │ └── routes │ └── test/ │ ├── dal/ │ │ └── AdvicesRepositoryTest.scala │ └── util/ │ └── BaseSqlSpec.scala ├── ui-client/ │ └── src/ │ └── main/ │ └── scala/ │ └── com/ │ └── softwaremill/ │ └── clippy/ │ ├── App.scala │ ├── AutowireClient.scala │ ├── BsUtils.scala │ ├── Contribute.scala │ ├── Feedback.scala │ ├── FormField.scala │ ├── Listing.scala │ ├── Main.scala │ ├── Menu.scala │ ├── Use.scala │ └── Utils.scala └── ui-shared/ └── src/ └── main/ └── scala/ └── com/ └── softwaremill/ └── clippy/ ├── AdviceState.scala ├── Contributor.scala └── UiApi.scala ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules *.iml *.ipr *.iws .idea/ target/ data/ *.log .DS_Store local.sbt ================================================ FILE: .scalafmt.conf ================================================ style = defaultWithAlign maxColumn = 120 align.openParenCallSite = false align.openParenDefnSite = false danglingParentheses = true rewrite.rules = [RedundantBraces, RedundantParens, SortImports, PreferCurlyFors] rewrite.redundantBraces.includeUnitMethods = true rewrite.redundantBraces.stringInterpolation = true ================================================ FILE: .travis.yml ================================================ sudo: false language: scala jdk: - oraclejdk8 scala: - 2.11.8 install: - ". $HOME/.nvm/nvm.sh" - nvm install stable - nvm use stable - npm install script: - sbt test - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && sbt updateImpactSubmit || true' notifications: slack: secure: BLLFmdz5G6098EfMgw+CXNqqnwQPDHhaXp0Bg3GWkUoAh3/BoWFhI6iTRgJW3q85IBLa6A2el0wLY8mK1lyD4mRg5dLTPWCILqb8eaF6Shop/FQi0O3pyACvfYUitW9EBalug9VVZzceKsHDaW9pbuqDxCWv1qWyBlTv7aSfVnyYlh6LC+3YqsWFeMHA7/W/lSAklvHkk7GTYIp3A30DsYup2K/EHMJ4ZxuA0CZOOrE4jOfEOzZgm787asPT8o12yb3CWEBZai3Fxs4s6I25w1t6Y5JTPXnYi01OIug6mDtZRDAh1ZJHUdyhOXbxqib6QbCrayg1ou3yde8pKm3KgnMMAJtu6wB8XCMlCDCJ81qkfOCY7TtUfax2spTD5xyIkoQxHPSqOnKyaBYXMKaQ4gMMGiJQToZ8hPtQTYDZTMK5VD5CS1kNMqJBI4V7YlFnKchbv1NUBTKA18QouTCVu369hBb2gANzI1IR4AoP+LRgFjCqEyAAcqoa1jkuENyF1KZykTa3NrkENBlzhEJofYwIi9TP8JUY8mCJp2dnLqnWU/PlJJBTEkMd/00My92znleo2S4gaDgd79quD7iJuZotJt5pkad9auh18ZFQcgmgo9dc3nS3yCk+/vVfPvYKP0pHRWe5RmPkaVbCZ6kCP79LIVlMcA1oYhpNRiwXe5o= ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2013-2016 Softwaremill 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 http://www.apache.org/licenses/LICENSE-2.0 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. ================================================ FILE: README.md ================================================ # Scala clippy [![Join the chat at https://gitter.im/softwaremill/scala-clippy](https://badges.gitter.im/softwaremill/scala-clippy.svg)](https://gitter.im/softwaremill/scala-clippy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/softwaremill/scala-clippy.svg?branch=master)](https://travis-ci.org/softwaremill/scala-clippy) [![Dependencies](https://app.updateimpact.com/badge/634276070333485056/clippy.svg?config=compile)](https://app.updateimpact.com/latest/634276070333485056/clippy) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.clippy/plugin_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.clippy/plugin_2.11) Enrich your Scala compiler error output with additional advices and colors! ![enriched error example](ui/app/assets/img/clippy-akka-err-rich.png "") # Documentation Read the detailed [documentation](https://scala-clippy.org). # Contributing to the project You can also help developing the plugin and/or the UI for submitting new advices! The module structure is: * `model` - code shared between the UI and the plugin. Contains basic model case classes, such as `CompilationError` + parser * `plugin` - the compiler plugin which actually displays the advices and matches errors agains the database of known errors * `tests` - tests for the compiler plugin. Must be a separate project, as it requires the plugin jar to be ready * `ui` - the ui server project in Play * `ui-client` - the Scala.JS client-side code * `ui-shared` - code shared between the UI server and UI client (but not needed for the plugin) 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). If you want to write your own tests with compilation using `mkToolbox`, remember to add a `-P:clippy:testmode=true` compiler option. It ensures that a correct reporter replacement mechanism is used, which needs to be different specifically for tests. See [CompileTests.scala](tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala) for reference. To publish locally append "-SNAPSHOT" to the version number then run ````scala sbt "project plugin" "+ publishLocal" ```` Run advice tests with ````scala sbt tests/test ```` # Heroku deployment Locally: * Install the Heroku Toolbelt * link the local git repository with the Heroku application: `heroku git:remote -a scala-clippy` * run `sbt deployHeroku` to deploy the current code as a fat-jar Currently deployed on `https://www.scala-clippy.org` # Credits Clippy contributors: * [Krzysztof Ciesielski](https://github.com/kciesielski) * [Łukasz Żuchowski](https://github.com/Zuchos) * [Shane Delmore](https://github.com/ShaneDelmore) * [Adam Warski](https://github.com/adamw) Syntax highlighting code is copied from [Ammonite](http://www.lihaoyi.com/Ammonite/). ================================================ FILE: build.sbt ================================================ import sbt._ import Keys._ import sbtassembly.AssemblyKeys import scala.xml.transform.RuleTransformer import scala.xml.transform.RewriteRule import scala.xml.{Node => XNode} import scala.xml.{NodeSeq => XNodeSeq} import scala.xml.{Elem => XElem} val slickVersion = "3.1.1" val json4s = "org.json4s" %% "json4s-native" % "3.5.0" // testing val scalatest = "org.scalatest" %% "scalatest" % "3.0.1" % "test" val scalacheck = "org.scalacheck" %% "scalacheck" % "1.13.4" % "test" name := "clippy" // factor out common settings into a sequence lazy val commonSettingsNoScalaVersion = Seq( organization := "com.softwaremill.clippy", version := "0.6.1", scalacOptions ++= Seq("-unchecked", "-deprecation"), parallelExecution := false, // Sonatype OSS deployment publishTo := { val nexus = "https://oss.sonatype.org/" if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") else Some("releases" at nexus + "service/local/staging/deploy/maven2") }, credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"), publishMavenStyle := true, publishArtifact in Test := false, pomIncludeRepository := { _ => false }, pomExtra := git@github.com:softwaremill/scala-clippy.git scm:git:git@github.com:softwaremill/scala-clippy.git adamw Adam Warski http://www.warski.org , licenses := ("Apache2", new java.net.URL("http://www.apache.org/licenses/LICENSE-2.0.txt")) :: Nil, homepage := Some(new java.net.URL("http://www.softwaremill.com")), com.updateimpact.Plugin.apiKey in ThisBuild := sys.env .getOrElse("UPDATEIMPACT_API_KEY", (com.updateimpact.Plugin.apiKey in ThisBuild).value) ) lazy val commonSettings = commonSettingsNoScalaVersion ++ Seq( scalaVersion := "2.11.11" ) lazy val sbt10CompatSettings = Seq( sbtVersion in Global := (if (scalaVersion.value startsWith "2.12.") "1.1.6" else "0.13.15"), scalaCompilerBridgeSource := ("org.scala-sbt" % "compiler-interface" % "0.13.15" % "component").sources ) lazy val clippy = (project in file(".")) .settings(commonSettings) .settings( publishArtifact := false, // heroku herokuFatJar in Compile := Some((assemblyOutputPath in ui in assembly).value), deployHeroku in Compile := (deployHeroku in Compile).dependsOn(assembly in ui).value ) .aggregate(modelJvm, plugin, pluginSbt, tests, ui) lazy val model = (crossProject.crossType(CrossType.Pure) in file("model")) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq(scalatest, scalacheck, json4s) ) lazy val modelJvm = model.jvm.settings(name := "modelJvm") lazy val modelJs = model.js.settings(name := "modelJs") def removeDep(groupId: String, artifactId: String) = new RewriteRule { override def transform(n: XNode): XNodeSeq = n match { case e: XElem if (e \ "groupId").text == groupId && (e \ "artifactId").text.startsWith(artifactId) => XNodeSeq.Empty case _ => n } } lazy val plugin = (project in file("plugin")) .enablePlugins(BuildInfoPlugin) .settings(commonSettings) .settings( crossScalaVersions := Seq(scalaVersion.value, "2.12.1", "2.10.6"), libraryDependencies ++= Seq( "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided", "com.lihaoyi" %% "scalaparse" % "0.4.2", "com.lihaoyi" %% "fansi" % "0.2.3", scalatest, scalacheck, json4s ), // this is needed for fastparse to work on 2.10 libraryDependencies ++= (if (scalaVersion.value startsWith "2.10.") Seq(compilerPlugin("org.scalamacros" % s"paradise" % "2.1.0" cross CrossVersion.full)) else Seq()), pomPostProcess := { (node: XNode) => new RuleTransformer(removeDep("org.json4s", "json4s-native")).transform(node).head }, buildInfoPackage := "com.softwaremill.clippy", buildInfoObject := "ClippyBuildInfo", artifact in (Compile, assembly) := { val art = (artifact in (Compile, assembly)).value art.copy(`classifier` = Some("bundle")) }, assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = false), // including the model classes for re-compilation, as for some reason depending on modelJvm doesn't work unmanagedSourceDirectories in Compile ++= (sourceDirectories in (modelJvm, Compile)).value ) .settings(sbt10CompatSettings) .settings(addArtifact(artifact in (Compile, assembly), assembly)) lazy val pluginJar = AssemblyKeys.`assembly` in (plugin, Compile) lazy val pluginSbt = (project in file("plugin-sbt")) .enablePlugins(BuildInfoPlugin) .settings(commonSettingsNoScalaVersion) .settings( sbtPlugin := true, name := "plugin-sbt", buildInfoPackage := "com.softwaremill.clippy", buildInfoObject := "ClippyBuildInfo", scalaVersion := "2.10.6", crossSbtVersions := Vector("0.13.16", "1.0.0") ) .settings(sbt10CompatSettings) lazy val tests = (project in file("tests")) .settings(commonSettings) .settings( publishArtifact := false, libraryDependencies ++= Seq( json4s, scalatest, "com.typesafe.akka" %% "akka-http" % "10.0.0", "com.softwaremill.macwire" %% "macros" % "2.2.2" % "provided", "com.typesafe.slick" %% "slick" % slickVersion ), // during tests, read from the local repository, if at all available scalacOptions ++= List( s"-Xplugin:${pluginJar.value.getAbsolutePath}", "-P:clippy:url=http://localhost:9000", "-P:clippy:colors=true" ), envVars in Test := (envVars in Test).value + ("CLIPPY_PLUGIN_PATH" -> pluginJar.value.getAbsolutePath), fork in Test := true ) .dependsOn(modelJvm) lazy val ui: Project = (project in file("ui")) .enablePlugins(BuildInfoPlugin) .settings(commonSettings) .settings( libraryDependencies ++= Seq( "com.h2database" % "h2" % "1.4.190", // % "test", scalatest, "org.webjars" %% "webjars-play" % "2.4.0-1", "org.webjars" % "bootstrap" % "3.3.6", "org.webjars" % "jquery" % "1.11.3", "com.vmunier" %% "play-scalajs-scripts" % "0.3.0", "com.softwaremill.common" %% "id-generator" % "1.1.0", "com.sendgrid" % "sendgrid-java" % "2.2.2" exclude ("commons-logging", "commons-logging"), "org.postgresql" % "postgresql" % "9.4.1207", "com.typesafe.slick" %% "slick" % slickVersion, "com.typesafe.slick" %% "slick-hikaricp" % slickVersion, "org.flywaydb" % "flyway-core" % "3.2.1" ), scalaJSProjects := Seq(uiClient), pipelineStages in Assets := Seq(scalaJSProd), routesGenerator := InjectedRoutesGenerator, // heroku & fat-jar assemblyJarName in assembly := "app.jar", mainClass in assembly := Some("play.core.server.ProdServerStart"), fullClasspath in assembly += Attributed.blank(PlayKeys.playPackageAssets.value), buildInfoPackage := "util", buildInfoObject := "ClippyBuildInfo", assemblyMergeStrategy in assembly := { // anything in public/lib is copied from webjars and causes duplicate resources exceptions case PathList("public", "lib", xs @ _ *) => MergeStrategy.discard case "JS_DEPENDENCIES" => MergeStrategy.discard case x => val oldStrategy = (assemblyMergeStrategy in assembly).value oldStrategy(x) } ) .enablePlugins(PlayScala) .aggregate(uiClient) .dependsOn(uiSharedJvm) val scalaJsReactVersion = "0.11.3" lazy val uiClient: Project = (project in file("ui-client")) .settings(commonSettings) .settings(name := "uiClient") .settings( scalaJSUseMainModuleInitializer := true, scalaJSUseMainModuleInitializer in Test := false, addCompilerPlugin(compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)), // for @Lenses libraryDependencies ++= Seq( "org.scala-js" %%% "scalajs-dom" % "0.9.1", "be.doeraene" %%% "scalajs-jquery" % "0.9.1", "com.github.japgolly.scalajs-react" %%% "core" % scalaJsReactVersion, "com.github.japgolly.scalajs-react" %%% "ext-monocle" % scalaJsReactVersion, "com.github.japgolly.fork.monocle" %%% "monocle-macro" % "1.2.0" ), jsDependencies ++= Seq( RuntimeDOM % "test", "org.webjars.bower" % "react" % "15.3.2" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React", "org.webjars.bower" % "react" % "15.3.2" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM" ) ) .enablePlugins(ScalaJSPlugin, ScalaJSWeb) .dependsOn(uiSharedJs) lazy val uiShared = (crossProject.crossType(CrossType.Pure) in file("ui-shared")) .settings(commonSettings: _*) .settings( name := "uiShared", libraryDependencies ++= Seq( "com.lihaoyi" %%% "autowire" % "0.2.5", "com.lihaoyi" %%% "upickle" % "0.3.6" ) ) .jsConfigure(_ enablePlugins ScalaJSWeb) .dependsOn(model) lazy val uiSharedJvm = uiShared.jvm.settings(name := "uiSharedJvm") lazy val uiSharedJs = uiShared.js.settings(name := "uiSharedJs") ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/Advice.scala ================================================ package com.softwaremill.clippy import org.json4s.JsonAST._ final case class Advice(compilationError: CompilationError[RegexT], advice: String, library: Library) { def errMatching: PartialFunction[CompilationError[ExactT], String] = { case ce if compilationError.matches(ce) => advice } def toJson: JValue = JObject( "error" -> compilationError.toJson, "text" -> JString(advice), "library" -> library.toJson ) } object Advice { def fromJson(jvalue: JValue): Option[Advice] = (for { JObject(fields) <- jvalue JField("error", errorJV) <- fields error <- CompilationError.fromJson(errorJV).toList JField("text", JString(text)) <- fields JField("library", libraryJV) <- fields library <- Library.fromJson(libraryJV).toList } yield Advice(error, text, library)).headOption } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/Clippy.scala ================================================ package com.softwaremill.clippy import org.json4s.JsonAST._ case class Clippy(version: String, advices: List[Advice], fatalWarnings: List[Warning]) { def checkPluginVersion(ourVersion: String, logInfo: String => Unit) = if (version != ourVersion) { logInfo(s"New version of clippy plugin available: $version. Please update!") } def toJson: JValue = JObject( "version" -> JString(version), "advices" -> JArray(advices.map(_.toJson)), "fatalWarnings" -> JArray(fatalWarnings.map(_.toJson)) ) } object Clippy { def fromJson(jvalue: JValue): Option[Clippy] = (for { JObject(fields) <- jvalue JField("version", JString(version)) <- fields } yield { val advices = jvalue.findField { case JField("advices", _) => true case _ => false } match { case Some((_, JArray(advicesJV))) => advicesJV.flatMap(Advice.fromJson) case _ => Nil } val fatalWarnings = fields.find { tpl => tpl._1 == "fatalWarnings" } match { case Some((_, JArray(fatalWarningsJV))) => fatalWarningsJV.flatMap(Warning.fromJson) case _ => Nil } Clippy(version, advices, fatalWarnings) }).headOption } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/CompilationError.scala ================================================ package com.softwaremill.clippy import org.json4s.JValue import org.json4s.JsonAST.{JArray, JField, JObject, JString} import org.json4s.native.JsonMethods._ import CompilationError._ sealed trait CompilationError[T <: Template] { def toJson: JValue def toJsonString: String = compact(render(toJson)) def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT): Boolean def asRegex(implicit ev: T =:= ExactT): CompilationError[RegexT] } case class TypeMismatchError[T <: Template]( found: T, foundExpandsTo: Option[T], required: T, requiredExpandsTo: Option[T], notes: Option[String] ) extends CompilationError[T] { def notesAfterNewline = notes.fold("")(n => "\n" + n) override def toString = { def expandsTo(et: Option[T]): String = et.fold("")(e => s" (expands to: $e)") s"Type mismatch error.\nFound: $found${expandsTo(foundExpandsTo)},\nrequired: $required${expandsTo(requiredExpandsTo)}$notesAfterNewline" } override def toJson = JObject( List(TypeField -> JString("typeMismatch"), "found" -> JString(found.v), "required" -> JString(required.v)) ++ foundExpandsTo.fold[List[JField]](Nil)(e => List("foundExpandsTo" -> JString(e.v))) ++ requiredExpandsTo.fold[List[JField]](Nil)(e => List("requiredExpandsTo" -> JString(e.v))) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case TypeMismatchError(f, fe, r, re, _) => def optMatches(t: Option[T], v: Option[ExactT]) = (for { tt <- t vv <- v } yield tt.matches(vv)).getOrElse(true) found.matches(f) && optMatches(foundExpandsTo, fe) && required.matches(r) && optMatches(requiredExpandsTo, re) case _ => false } def hasExpands: Boolean = foundExpandsTo.nonEmpty || requiredExpandsTo.nonEmpty override def asRegex(implicit ev: T =:= ExactT) = TypeMismatchError( RegexT.fromPattern(found.v), foundExpandsTo.map(fe => RegexT.fromPattern(fe.v)), RegexT.fromPattern(required.v), requiredExpandsTo.map(re => RegexT.fromPattern(re.v)), notes ) } case class NotFoundError[T <: Template](what: T) extends CompilationError[T] { override def toString = s"Not found error: $what" override def toJson = JObject(TypeField -> JString("notFound"), "what" -> JString(what.v)) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case NotFoundError(w) => what.matches(w) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = NotFoundError(RegexT.fromPattern(what.v)) } case class NotAMemberError[T <: Template](what: T, notAMemberOf: T) extends CompilationError[T] { override def toString = s"Not a member error: $what isn't a member of $notAMemberOf" override def toJson = JObject( TypeField -> JString("notAMember"), "what" -> JString(what.v), "notAMemberOf" -> JString(notAMemberOf.v) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case NotAMemberError(w, n) => what.matches(w) && notAMemberOf.matches(n) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = NotAMemberError(RegexT.fromPattern(what.v), RegexT.fromPattern(notAMemberOf.v)) } case class ImplicitNotFoundError[T <: Template](parameter: T, implicitType: T) extends CompilationError[T] { override def toString = s"Implicit not found error: for parameter $parameter of type $implicitType" override def toJson = JObject( TypeField -> JString("implicitNotFound"), "parameter" -> JString(parameter.v), "implicitType" -> JString(implicitType.v) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case ImplicitNotFoundError(p, i) => parameter.matches(p) && implicitType.matches(i) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = ImplicitNotFoundError(RegexT.fromPattern(parameter.v), RegexT.fromPattern(implicitType.v)) } case class TypeclassNotFoundError[T <: Template](typeclass: T, forType: T) extends CompilationError[T] { override def toString = s"Implicit $typeclass typeclass not found error: for type $forType" override def toJson = JObject( TypeField -> JString("typeclassNotFound"), "typeclass" -> JString(typeclass.v), "forType" -> JString(forType.v) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case TypeclassNotFoundError(p, i) => typeclass.matches(p) && forType.matches(i) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = TypeclassNotFoundError(RegexT.fromPattern(typeclass.v), RegexT.fromPattern(forType.v)) } case class DivergingImplicitExpansionError[T <: Template](forType: T, startingWith: T, in: T) extends CompilationError[T] { override def toString = s"Diverging implicit expansion error: for type $forType starting with $startingWith in $in" override def toJson = JObject( TypeField -> JString("divergingImplicitExpansion"), "forType" -> JString(forType.v), "startingWith" -> JString(startingWith.v), "in" -> JString(in.v) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case DivergingImplicitExpansionError(f, s, i) => forType.matches(f) && startingWith.matches(s) && in.matches(i) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = DivergingImplicitExpansionError( RegexT.fromPattern(forType.v), RegexT.fromPattern(startingWith.v), RegexT.fromPattern(in.v) ) } case class TypeArgumentsDoNotConformToOverloadedBoundsError[T <: Template]( typeArgs: T, alternativesOf: T, alternatives: Set[T] ) extends CompilationError[T] { override def toString = s"Type arguments: $typeArgs for overloaded: $alternativesOf do not conform to any bounds: ${alternatives.map(_.toString).mkString(" ")}" override def toJson = JObject( TypeField -> JString("typeArgumentsDoNotConformToOverloadedBounds"), "typeArgs" -> JString(typeArgs.v), "alternativesOf" -> JString(alternativesOf.v), "alternatives" -> JArray(alternatives.map(a => JString(a.v)).toList) ) override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match { case TypeArgumentsDoNotConformToOverloadedBoundsError(t, af, a) => typeArgs.matches(t) && alternativesOf.matches(af) && RegexT.setMatches(alternatives.map(ev.apply), a) case _ => false } override def asRegex(implicit ev: T =:= ExactT) = TypeArgumentsDoNotConformToOverloadedBoundsError( RegexT.fromPattern(typeArgs.v), RegexT.fromPattern(alternativesOf.v), alternatives.map(a => RegexT.fromPattern(a.v)) ) } object CompilationError { val TypeField = "type" def fromJsonString(s: String): Option[CompilationError[RegexT]] = fromJson(parse(s)) def fromJson(jvalue: JValue): Option[CompilationError[RegexT]] = { def regexTFromJson(fields: List[JField], name: String): Option[RegexT] = (for { JField(`name`, JString(v)) <- fields } yield RegexT.fromRegex(v)).headOption def multipleRegexTFromJson(fields: List[JField], name: String): Option[Set[RegexT]] = (for { JField(`name`, JArray(vv)) <- fields } yield vv.collect { case JString(v) => RegexT.fromRegex(v) }.toSet).headOption def extractWithType(typeValue: String, fields: List[JField]): Option[CompilationError[RegexT]] = typeValue match { case "typeMismatch" => for { found <- regexTFromJson(fields, "found") foundExpandsTo = regexTFromJson(fields, "foundExpandsTo") required <- regexTFromJson(fields, "required") requiredExpandsTo = regexTFromJson(fields, "requiredExpandsTo") } yield TypeMismatchError(found, foundExpandsTo, required, requiredExpandsTo, None) case "notFound" => for { what <- regexTFromJson(fields, "what") } yield NotFoundError(what) case "notAMember" => for { what <- regexTFromJson(fields, "what") notAMemberOf <- regexTFromJson(fields, "notAMemberOf") } yield NotAMemberError(what, notAMemberOf) case "implicitNotFound" => for { parameter <- regexTFromJson(fields, "parameter") implicitType <- regexTFromJson(fields, "implicitType") } yield ImplicitNotFoundError(parameter, implicitType) case "divergingImplicitExpansion" => for { forType <- regexTFromJson(fields, "forType") startingWith <- regexTFromJson(fields, "startingWith") in <- regexTFromJson(fields, "in") } yield DivergingImplicitExpansionError(forType, startingWith, in) case "typeArgumentsDoNotConformToOverloadedBounds" => for { typeArgs <- regexTFromJson(fields, "typeArgs") alternativesOf <- regexTFromJson(fields, "alternativesOf") alternatives <- multipleRegexTFromJson(fields, "alternatives") } yield TypeArgumentsDoNotConformToOverloadedBoundsError(typeArgs, alternativesOf, alternatives) case "typeclassNotFound" => for { typeclass <- regexTFromJson(fields, "typeclass") forType <- regexTFromJson(fields, "forType") } yield TypeclassNotFoundError(typeclass, forType) case _ => None } (for { JObject(fields) <- jvalue JField(TypeField, JString(typeValue)) <- fields v <- extractWithType(typeValue, fields).toList } yield v).headOption } } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/CompilationErrorParser.scala ================================================ package com.softwaremill.clippy import java.util.regex.Pattern object CompilationErrorParser { private val FoundRegexp = """found\s*:\s*([^\n]+)""".r private val RequiredPrefixRegexp = """required\s*:""".r private val AfterRequiredRegexp = """required\s*:\s*([^\n]+)""".r private val WhichExpandsToRegexp = """\s*\(which expands to\)\s*([^\n]+)""".r private val NotFoundRegexp = """not found\s*:\s*([^\n]+)""".r private val NotAMemberRegexp = """:?\s*([^\n:]+) is not a member of""".r private val NotAMemberOfRegexp = """is not a member of\s*([^\n]+)""".r private val ImplicitNotFoundRegexp = """could not find implicit value for parameter\s*([^:]+):\s*([^\n]+)""".r private val DivergingImplicitExpansionRegexp = """diverging implicit expansion for type\s*([^\s]+)\s*.*\s*starting with method\s*([^\s]+)\s*in\s*([^\n]+)""".r private val TypeArgumentsDoNotConformToOverloadedBoundsRegexp = """type arguments \[([^\]]+)\] conform to the bounds of none of the overloaded alternatives of\s*([^:\n]+)[^:]*: ([^\n]+)""".r private val TypeclassNotFoundRegexp = """No implicit (.*) defined for ([^\n]+)""".r def parse(e: String): Option[CompilationError[ExactT]] = { val error = e.replaceAll(Pattern.quote("[error]"), "") if (error.contains("type mismatch")) { RequiredPrefixRegexp.split(error).toList match { case List(beforeReq, afterReq) => for { found <- FoundRegexp.findFirstMatchIn(beforeReq) foundExpandsTo = WhichExpandsToRegexp.findFirstMatchIn(beforeReq) required <- AfterRequiredRegexp.findFirstMatchIn(error) requiredExpandsTo = WhichExpandsToRegexp.findFirstMatchIn(afterReq) } yield { val notes = requiredExpandsTo match { case Some(et) => getNotesFromIndex(afterReq, et.end) case None => getNotesFromIndex(error, required.end) } TypeMismatchError[ExactT]( ExactT(found.group(1)), foundExpandsTo.map(m => ExactT(m.group(1))), ExactT(required.group(1)), requiredExpandsTo.map(m => ExactT(m.group(1))), notes ) } case _ => None } } else if (error.contains("not found")) { for { what <- NotFoundRegexp.findFirstMatchIn(error) } yield NotFoundError[ExactT](ExactT(what.group(1))) } else if (error.contains("is not a member of")) { for { what <- NotAMemberRegexp.findFirstMatchIn(error) notAMemberOf <- NotAMemberOfRegexp.findFirstMatchIn(error) } yield NotAMemberError[ExactT](ExactT(what.group(1)), ExactT(notAMemberOf.group(1))) } else if (error.contains("could not find implicit value for parameter")) { for { inf <- ImplicitNotFoundRegexp.findFirstMatchIn(error) } yield ImplicitNotFoundError[ExactT](ExactT(inf.group(1)), ExactT(inf.group(2))) } else if (error.contains("diverging implicit expansion for type")) { for { inf <- DivergingImplicitExpansionRegexp.findFirstMatchIn(error) } yield DivergingImplicitExpansionError[ExactT](ExactT(inf.group(1)), ExactT(inf.group(2)), ExactT(inf.group(3))) } else if (error.contains("conform to the bounds of none of the overloaded alternatives")) { for { inf <- TypeArgumentsDoNotConformToOverloadedBoundsRegexp.findFirstMatchIn(error) } yield TypeArgumentsDoNotConformToOverloadedBoundsError[ExactT]( ExactT(inf.group(1)), ExactT(inf.group(2)), inf.group(3).split(Pattern.quote(" ")).toSet.map(ExactT.apply) ) } else if (error.contains("No implicit")) { for { inf <- TypeclassNotFoundRegexp.findFirstMatchIn(error) group2 = inf.group(2) } yield TypeclassNotFoundError( ExactT(inf.group(1)), ExactT(if (group2.endsWith(".")) group2.substring(0, group2.length - 1) else group2) ) } else None } private def getNotesFromIndex(msg: String, afterIdx: Int): Option[String] = { val fromIdx = afterIdx + 1 if (msg.length >= fromIdx + 1) { val notes = msg.substring(fromIdx).trim if (notes == "") None else Some(notes) } else None } } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/Library.scala ================================================ package com.softwaremill.clippy import org.json4s.JsonAST.{JField, JObject, JString, JValue} case class Library(groupId: String, artifactId: String, version: String) { def toJson: JValue = JObject( "groupId" -> JString(groupId), "artifactId" -> JString(artifactId), "version" -> JString(version) ) override def toString = s"$groupId:$artifactId:$version" } object Library { def fromJson(jvalue: JValue): Option[Library] = (for { JObject(fields) <- jvalue JField("groupId", JString(groupId)) <- fields JField("artifactId", JString(artifactId)) <- fields JField("version", JString(version)) <- fields } yield Library(groupId, artifactId, version)).headOption } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/StringDiff.scala ================================================ package com.softwaremill.clippy object StringDiff { val separators = List(' ', ',', '(', ')', '[', ']', '#', '#', '=', '>', '{', '.') def isSeparator(char: Char): Boolean = separators.contains(char) } class StringDiff(expected: String, actual: String, color: String => String) { import StringDiff._ def diff(message: String): String = if (this.expected == this.actual) format(message, this.expected, this.actual) else format(message, markDiff(expected), markDiff(actual)) private def format(msg: String, expected: String, actual: String) = msg.format(expected, actual) private def markDiff(source: String) = { val prefix = findCommonPrefix() val suffix = findCommonSuffix() if (overlappingPrefixSuffix(source, prefix, suffix)) source else { val diff = color(source.substring(prefix.length, source.length - suffix.length)) prefix + diff + suffix } } private def overlappingPrefixSuffix(source: String, prefix: String, suffix: String) = prefix.length + suffix.length >= source.length def findCommonPrefix(expectedStr: String = expected, actualStr: String = actual): String = { val prefixChars = expectedStr.zip(actualStr).takeWhile(Function.tupled(_ == _)).map(_._1) val lastSeparatorIndex = prefixChars.lastIndexWhere(isSeparator) val prefixEndIndex = if (lastSeparatorIndex == -1) 0 else lastSeparatorIndex + 1 if (prefixChars.nonEmpty && prefixEndIndex < prefixChars.length) prefixChars.mkString.substring(0, prefixEndIndex) else { prefixChars.mkString } } def findCommonSuffix(): String = findCommonPrefix(expected.reverse, actual.reverse).reverse } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/Template.scala ================================================ package com.softwaremill.clippy import java.util.regex.Pattern import scala.util.Try import scala.util.matching.Regex sealed trait Template { def v: String } case class ExactT(v: String) extends Template { override def toString = v } case class RegexT(v: String) extends Template { lazy val regex = Try(new Regex(v)).getOrElse(new Regex("^$")) def matches(e: ExactT): Boolean = regex.pattern.matcher(e.v).matches() override def toString = v } object RegexT { /** * Patterns can include wildcards (`*`) */ def fromPattern(pattern: String): RegexT = { val regexp = pattern .split("\\*", -1) .map(el => if (el != "") Pattern.quote(el) else el) .flatMap(el => List(".*", el)) .tail .filter(_.nonEmpty) .mkString("") RegexT.fromRegex(regexp) } def fromRegex(v: String): RegexT = new RegexT(v) def setMatches(rr: Set[RegexT], ee: Set[ExactT]): Boolean = if (rr.size != ee.size) false else { rr.toList.forall { r => ee.exists(r.matches) } } } ================================================ FILE: model/src/main/scala/com/softwaremill/clippy/Warning.scala ================================================ package com.softwaremill.clippy import org.json4s.JsonAST._ final case class Warning(pattern: RegexT, text: Option[String]) { def toJson: JValue = JObject( "pattern" -> JString(pattern.toString) ) ++ text.map(t => JObject("text" -> JString(t))).getOrElse(JNothing) } object Warning { def fromJson(jvalue: JValue): Option[Warning] = (for { JObject(fields) <- jvalue JField("pattern", JString(patternStr)) <- fields pattern = RegexT.fromRegex(patternStr) } yield { val text = jvalue.findField { case (("text", _)) => true case _ => false } match { case Some((_, JString(textStr))) => Some(textStr) case _ => None } Warning(pattern, text) }).headOption } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/CompilationErrorParserTest.scala ================================================ package com.softwaremill.clippy import org.scalatest.{FlatSpec, Matchers} class CompilationErrorParserTest extends FlatSpec with Matchers { it should "parse akka's route error message" in { val e = """type mismatch; | found : akka.http.scaladsl.server.StandardRoute | required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin CompilationErrorParser.parse(e) should be( Some( 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, None ) ) ) } it should "parse an error message with [error] prefix" in { val e = """[error] /Users/adamw/projects/clippy/tests/src/main/scala/com/softwaremill/clippy/Working.scala:16: type mismatch; |[error] found : akka.http.scaladsl.server.StandardRoute |[error] required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin CompilationErrorParser.parse(e) should be( Some( 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, None ) ) ) } it should "parse a type mismatch error with a single expands to section" in { val e = """type mismatch; |found : japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField] |required: japgolly.scalajs.react.CompState.AccessRD[?] | (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]""".stripMargin CompilationErrorParser.parse(e) should be( Some( TypeMismatchError( ExactT( "japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]" ), None, ExactT("japgolly.scalajs.react.CompState.AccessRD[?]"), Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")), None ) ) ) } it should "parse a type mismatch error with two expands to sections" in { val e = """type mismatch; |found : japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField] | (which expands to) japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.FormField] |required: japgolly.scalajs.react.CompState.AccessRD[?] | (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]""".stripMargin CompilationErrorParser.parse(e) should be( Some( TypeMismatchError( ExactT( "japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]" ), Some( ExactT("japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.FormField]") ), ExactT("japgolly.scalajs.react.CompState.AccessRD[?]"), Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")), None ) ) ) } it should "parse macwire's wire not found error message" in { val e = "not found: value wire" CompilationErrorParser.parse(e) should be(Some(NotFoundError(ExactT("value wire")))) } it should "parse not a member of message" in { val e = "value call is not a member of scala.concurrent.Future[Unit]" CompilationErrorParser.parse(e) should be( Some(NotAMemberError(ExactT("value call"), ExactT("scala.concurrent.Future[Unit]"))) ) } it should "parse not a member of message with extra text" in { val e = "[error] /Users/adamw/projects/clippy/ui-client/src/main/scala/com/softwaremill/clippy/Listing.scala:33: value call is not a member of scala.concurrent.Future[Unit]" CompilationErrorParser.parse(e) should be( Some(NotAMemberError(ExactT("value call"), ExactT("scala.concurrent.Future[Unit]"))) ) } it should "parse an implicit not found" in { val e = "could not find implicit value for parameter marshaller: spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[String]]" CompilationErrorParser.parse(e) should be( Some( ImplicitNotFoundError( ExactT("marshaller"), ExactT("spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[String]]") ) ) ) } it should "parse a diverging implicit error " in { val e = "diverging implicit expansion for type io.circe.Decoder.Secondary[this.Out] starting with method decodeCaseClass in trait GenericInstances" CompilationErrorParser.parse(e) should be( Some( DivergingImplicitExpansionError( ExactT("io.circe.Decoder.Secondary[this.Out]"), ExactT("decodeCaseClass"), ExactT("trait GenericInstances") ) ) ) } it should "parse a diverging implicit error with extra text" in { val e = """ |[error] /home/src/main/scala/Routes.scala:19: diverging implicit expansion for type io.circe.Decoder.Secondary[this.Out] |[error] starting with method decodeCaseClass in trait GenericInstances """.stripMargin CompilationErrorParser.parse(e) should be( Some( DivergingImplicitExpansionError( ExactT("io.circe.Decoder.Secondary[this.Out]"), ExactT("decodeCaseClass"), ExactT("trait GenericInstances") ) ) ) } it should "parse a type arguments do not conform to any overloaded bounds error" in { val e = """ |[error] clippy/Working.scala:32: type arguments [org.softwaremill.clippy.User] conform to the bounds of none of the overloaded alternatives of |value apply: [E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E] [E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E] |protected val users = TableQuery[User] """.stripMargin CompilationErrorParser.parse(e) should be( Some( TypeArgumentsDoNotConformToOverloadedBoundsError( ExactT("org.softwaremill.clippy.User"), ExactT("value apply"), Set( ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"), ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]") ) ) ) ) } it should "parse a no implicit defined for" in { val e = """ |[error] /Users/clippy/model/src/main/scala/com/softwaremill/clippy/CompilationErrorParser.scala:18: No implicit Ordering defined for java.time.LocalDate. |[error] Seq(java.time.LocalDate.MIN, java.time.LocalDate.MAX).sorted """.stripMargin CompilationErrorParser.parse(e) should be( Some(TypeclassNotFoundError(ExactT("Ordering"), ExactT("java.time.LocalDate"))) ) } it should "parse an error with notes" in { val e = """ |type mismatch; | found : org.softwaremill.clippy.ImplicitResolutionDiamond.C | required: Array[String] |Note that implicit conversions are not applicable because they are ambiguous: | both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String] | and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String] | are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String] """.stripMargin CompilationErrorParser.parse(e) should be( Some( TypeMismatchError( ExactT("org.softwaremill.clippy.ImplicitResolutionDiamond.C"), None, ExactT("Array[String]"), None, Some( """Note that implicit conversions are not applicable because they are ambiguous: | both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String] | and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String] | are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]""".stripMargin ) ) ) ) } it should "parse an error with expands to & notes" in { val e = """ |type mismatch; | found : org.softwaremill.clippy.ImplicitResolutionDiamond.C | required: Array[String] | (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?] |Note that implicit conversions are not applicable because they are ambiguous: | both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String] | and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String] | are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String] """.stripMargin CompilationErrorParser.parse(e) should be( Some( TypeMismatchError( ExactT("org.softwaremill.clippy.ImplicitResolutionDiamond.C"), None, ExactT("Array[String]"), Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")), Some( """Note that implicit conversions are not applicable because they are ambiguous: | both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String] | and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String] | are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]""".stripMargin ) ) ) ) } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/CompilationErrorTest.scala ================================================ package com.softwaremill.clippy import org.scalacheck.Prop._ import org.scalacheck.Properties import org.scalatest.{FlatSpec, Matchers} class CompilationErrorTest extends FlatSpec with Matchers { it should "do not match different exact not found errors" in { // given val err = NotFoundError(RegexT.fromPattern("value wire[]")) val nonMatchingErrs = List( NotFoundError(ExactT("value wirex")), NotFoundError(ExactT("value wir")), NotFoundError(ExactT("avalue wire")), NotFoundError(ExactT("hakuna matata")) ) // then nonMatchingErrs.foreach(err.matches(_) should be(false)) } it should "match regex in not found errors" in { // given val err = NotFoundError(RegexT.fromPattern("value wi*")) val matchingErrs = List( NotFoundError(ExactT("value wire")), NotFoundError(ExactT("value wirex")), NotFoundError(ExactT("value wi")), NotFoundError(ExactT("value wire55")) ) val nonMatchingErrs = List( NotFoundError(ExactT("avalue wire")), NotFoundError(ExactT("avalue w5")) ) // then matchingErrs.foreach(err.matches(_) should be(true)) nonMatchingErrs.foreach(err.matches(_) should be(false)) } it should "not match different exact type mismatch errors" in { // given val err = TypeMismatchError( RegexT.fromPattern("com.softwaremill.String"), None, RegexT.fromPattern("com.softwaremill.RequiredType[String]"), None, None ) val nonMatchingErrs = List( TypeMismatchError(ExactT("com.softwaremill.String"), None, ExactT("com.softwaremill.OtherType"), None, None), TypeMismatchError(ExactT("com.softwaremill.Int"), None, ExactT("com.softwaremill.OtherType"), None, None), TypeMismatchError( ExactT("com.softwaremill.Int"), None, ExactT("com.softwaremill.RequiredType[String]"), None, None ) ) // then nonMatchingErrs.foreach(err.matches(_) should be(false)) } it should "match regex in type mismatch errors" in { // given val err = TypeMismatchError( RegexT.fromPattern("slick.dbio.DBIOAction[*]"), None, RegexT.fromPattern("slick.lifted.Rep[Option[*]]"), None, None ) val matchingErrs = List( TypeMismatchError( ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"), None, ExactT("slick.lifted.Rep[Option[?]]"), None, Some("notes") ), TypeMismatchError( ExactT("slick.dbio.DBIOAction[String,slick.dbio.NoStream,slick.dbio.Effect.Read]"), None, ExactT("slick.lifted.Rep[Option[Int]]"), None, None ), TypeMismatchError( ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read]"), None, ExactT("slick.lifted.Rep[Option[Option[Int]]"), None, None ) ) val nonMatchingErrs = List( TypeMismatchError( ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read]"), None, ExactT("String"), None, Some("notes") ), TypeMismatchError(ExactT("String"), None, ExactT("slick.lifted.Rep[Option[?]]"), None, None), TypeMismatchError(ExactT("String"), None, ExactT("com.softwaremill.AweSomeType"), None, None) ) // then matchingErrs.foreach(err.matches(_) should be(true)) nonMatchingErrs.foreach(err.matches(_) should be(false)) } } class CompilationErrorProperties extends Properties("CompilationError") { property("obj -> json -> obj works for type mismatch error") = forAll { (found: String, foundExpandsTo: Option[String], required: String, requiredExpandsTo: Option[String]) => val e = TypeMismatchError( RegexT.fromPattern(found), foundExpandsTo.map(RegexT.fromPattern), RegexT.fromPattern(required), requiredExpandsTo.map(RegexT.fromPattern), None ) CompilationError.fromJson(e.toJson).contains(e) } property("obj -> json -> obj works for not found error") = forAll { (what: String) => val e = NotFoundError(RegexT.fromPattern(what)) CompilationError.fromJson(e.toJson).contains(e) } property("obj -> json -> obj works for not a member error") = forAll { (what: String, notAMemberOf: String) => val e = NotAMemberError(RegexT.fromPattern(what), RegexT.fromPattern(notAMemberOf)) CompilationError.fromJson(e.toJson).contains(e) } property("obj -> json -> obj works for implicit not found") = forAll { (parameter: String, implicitType: String) => val e = ImplicitNotFoundError(RegexT.fromPattern(parameter), RegexT.fromPattern(implicitType)) CompilationError.fromJson(e.toJson).contains(e) } property("obj -> json -> obj works for diverging implicit expansions") = forAll { (forType: String, startingWith: String, in: String) => val e = DivergingImplicitExpansionError( RegexT.fromPattern(forType), RegexT.fromPattern(startingWith), RegexT.fromPattern(in) ) CompilationError.fromJson(e.toJson).contains(e) } property("obj -> json -> obj works for type arguments do not conform to overloaded bounds") = forAll { (typeArgs: String, alternativesOf: String, alternatives: Set[String]) => val e = TypeArgumentsDoNotConformToOverloadedBoundsError( RegexT.fromPattern(typeArgs), RegexT.fromPattern(alternativesOf), alternatives.map(a => RegexT.fromPattern(a)) ) CompilationError.fromJson(e.toJson).contains(e) } property("match identical not found error") = forAll { (what: String) => NotFoundError(RegexT.fromPattern(what)).matches(NotFoundError(ExactT(what))) } property("matches identical type mismatch error") = forAll { (found: String, required: String, notes: Option[String]) => TypeMismatchError(RegexT.fromPattern(found), None, RegexT.fromPattern(required), None, None) .matches(TypeMismatchError(ExactT(found), None, ExactT(required), None, notes)) } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/LibraryProperties.scala ================================================ package com.softwaremill.clippy import org.scalacheck.Prop._ import org.scalacheck.Properties class LibraryProperties extends Properties("Library") { property("obj -> json -> obj") = forAll { (gid: String, aid: String, v: String) => val l = Library(gid, aid, v) Library.fromJson(l.toJson).contains(l) } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/RegexTTest.scala ================================================ package com.softwaremill.clippy import org.scalatest.{FlatSpec, Matchers} class RegexTTest extends FlatSpec with Matchers { val matchingTests = List( ("slick.dbio.DBIOAction[*]", "slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"), ("slick.dbio.DBIOAction[Unit,*,*]", "slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"), ( "slick.dbio.DBIOAction[*,slick.dbio.NoStream,*]", "slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]" ) ) val nonMatchingTests = List( ("slick.dbio.DBIOAction[*]", "scala.concurrent.Future[Unit]"), ("slick.dbio.DBIOAction[Unit,*,*]", "slick.dbio.DBIOAction[String,slick.dbio.NoStream,slick.dbio.Effect.Write]"), ( "slick.dbio.DBIOAction[*,slick.dbio.NoStream,*]", "slick.dbio.DBIOAction[Unit,slick.dbio.Stream,slick.dbio.Effect.Write]" ) ) for ((pattern, test) <- matchingTests) { pattern should s"match $test" in { RegexT.fromPattern(pattern).matches(ExactT(test)) should be(true) } } for ((pattern, test) <- nonMatchingTests) { pattern should s"not match $test" in { RegexT.fromPattern(pattern).matches(ExactT(test)) should be(false) } } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/StringDiffSpecification.scala ================================================ package com.softwaremill.clippy import org.scalacheck.Prop.forAll import org.scalacheck.Properties class StringDiffSpecification extends Properties("StringDiff") with TypeNamesGenerators { val S = "S" val E = "E" val AddSE = (s: String) => S + s + E def innerTypeDiffsCorrectly(fourTypes: List[String]): Boolean = { val List(x, y, v, z) = fourTypes val expected = s"$x[$y[$z]]" val actual = s"$x[$v[$z]]" val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s") msg == s"""expected: $x[$S$y$E[$z]] actual: $x[$S$v$E[$z]]""" } def twoTypesAreFullyDiff(twoTypes: List[String]): Boolean = { val List(x, y) = twoTypes new StringDiff(x, y, AddSE).diff("expected: %s actual: %s") == s"""expected: $S$x$E actual: $S$y$E""" } property("X[Y[Z]] vs X[V[Z]] always gives X[[Z]] excluding packages") = forAll(different(singleTypeName)(4))(innerTypeDiffsCorrectly) property("X[Y[Z]] vs X[V[Z]] always gives X[[Z]] if Y and V have common prefix") = forAll(typesWithCommonPrefix(4))(innerTypeDiffsCorrectly) property("X[Y[Z]] vs X[V[Z]] always gives X[[Z]] if Y and V have common suffix") = forAll(typesWithCommonSuffix(4))(innerTypeDiffsCorrectly) property("A[X] vs B[X] always marks outer as diff for A != B when A and B have common prefix") = forAll(typesWithCommonPrefix(2), complexTypeName(maxDepth = 3)) { (outerTypes, x) => val List(a, b) = outerTypes val expected = s"$a[$x]" val actual = s"$b[$x]" val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s") msg == s"""expected: $S$a$E[$x] actual: $S$b$E[$x]""" } property("package.A[X] vs package.B[X] always gives package.A[X]") = forAll(javaPackage, different(singleTypeName)(2), complexTypeName(maxDepth = 3)) { (pkg, outerTypes, x) => val List(a, b) = outerTypes val expected = s"$pkg$a[$x]" val actual = s"$pkg$b[$x]" val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s") msg == s"""expected: $pkg$S$a$E[$x] actual: $pkg$S$b$E[$x]""" } property("any complex X vs Y is a full diff when X and Y don't have common suffix nor prefix") = forAll(different(complexTypeName(maxDepth = 4))(2).suchThat(noCommonPrefixSuffix))(twoTypesAreFullyDiff) property("any single X vs Y is a full diff") = forAll(different(singleTypeName)(2))(twoTypesAreFullyDiff) def noCommonPrefixSuffix(twoTypes: List[String]): Boolean = { val List(x, y) = twoTypes x.head != y.head && x.last != y.last } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/StringDiffTest.scala ================================================ package com.softwaremill.clippy import org.scalatest.{FlatSpec, Matchers} class StringDiffTest extends FlatSpec with Matchers { val S = "S" val E = "E" val AddSE = (s: String) => S + s + E val testData = List( ( "Super[String, String]", "Super[Option[String], String]", "expected Super[" + S + "String" + E + ", String] but was Super[" + S + "Option[String]" + E + ", String]" ), ( "Cool[String, String]", "Super[Option[String], String]", "expected " + S + "Cool[String" + E + ", String] but was " + S + "Super[Option[String]" + E + ", String]" ), ( "(String, String)", "Super[Option[String], String]", "expected " + S + "(String, String)" + E + " but was " + S + "Super[Option[String], String]" + E ), ( "Map[Long, Double]", "Map[String, Double]", "expected Map[" + S + "Long" + E + ", Double] but was Map[" + S + "String" + E + ", Double]" ), ( "(Int, Int, Float, Int, Char)", "(Int, Int, Int, Char)", "expected (Int, Int, " + S + "Float" + E + ", Int, Char) but was (Int, Int, Int, Char)" ) ) "StringDiff" should "diff" in { for ((expected, actual, expectedDiff) <- testData) { val diff = new StringDiff(expected, actual, AddSE).diff("expected %s but was %s") diff should be(expectedDiff) } } it should "find common prefix" in { new StringDiff("Map[Long, Double]", "Map[String, Double]", AddSE).findCommonPrefix() should be("Map[") } it should "find common suffix" in { new StringDiff("Map[Long, Double]", "Map[String, Double]", AddSE).findCommonSuffix() should be(", Double]") } } ================================================ FILE: model/src/test/scala/com/softwaremill/clippy/TypeNamesGenerators.scala ================================================ package com.softwaremill.clippy import org.scalacheck.Gen trait TypeNamesGenerators { import Gen._ def typeNameChar = frequency((1, numChar), (3, Gen.const('_')), (14, alphaChar)) val specialTypeName = Gen.oneOf(List("Option", "Operation", "Op", "String", "Long", "Set", "Aux")) def javaPackage: Gen[String] = for { depth <- Gen.choose(0, 3) names <- Gen.listOfN(depth, packageIdentifer) } yield { val str = names.filter(_.nonEmpty).mkString(".") if (str.nonEmpty) str + "." else str } def packageIdentifer: Gen[String] = for { c <- alphaLowerChar cs <- Gen.resize(7, listOf(alphaNumChar)) } yield { (c :: cs).mkString } def randomTypeWithPackage: Gen[String] = for { p <- javaPackage t <- randomTypeWithPackage } yield p + t def randomTypeName: Gen[String] = for { c <- alphaChar cs <- Gen.resize(7, listOf(typeNameChar)) } yield (c :: cs).mkString def singleTypeName = frequency((3, randomTypeName), (7, specialTypeName)) def functionalTypeName(maxResultDepth: Int): Gen[String] = for { argsMemberCount <- Gen.choose(2, 4) args <- Gen.oneOf(singleTypeName, tupleTypeName(depth = 0, argsMemberCount)) result <- complexTypeName(maxResultDepth) } yield { s"$args => $result" } def genericTypeName(depth: Int, memberCount: Int): Gen[String] = for { name <- singleTypeName innerNames <- Gen.listOfN(memberCount, if (depth == 0) singleTypeName else complexTypeName(depth - 1)) } yield s"$name[${innerNames.mkString(", ")}]" def tupleTypeName(depth: Int, memberCount: Int): Gen[String] = for { innerNames <- Gen.listOfN(memberCount, if (depth == 0) singleTypeName else complexTypeName(depth - 1)) } yield s"(${innerNames.mkString(", ")})" def different[T](gen: Gen[T]) = (count: Int) => Gen.listOfN(count, gen).suchThat { list => list.distinct == list } def typesWithCommonPrefix = (count: Int) => for { types <- different(singleTypeName)(count) commonPrefix <- singleTypeName } yield types.map(commonPrefix + _) def typesWithCommonSuffix = (count: Int) => for { types <- different(singleTypeName)(count) commonSuffix <- singleTypeName } yield types.map(_ + commonSuffix) def flatTypeIdentifier = for { tupleMemberCount <- Gen.choose(2, 4) generator <- Gen.oneOf( Seq(singleTypeName, tupleTypeName(0, tupleMemberCount), innerTypeName(0), functionalTypeName(0)) ) typeStr <- generator } yield typeStr def deepTypeName(maxDepth: Int): Gen[String] = for { genericMemberCount <- Gen.choose(1, 3) tupleMemberCount <- Gen.choose(2, 4) generator <- Gen.oneOf( Seq( singleTypeName, tupleTypeName(maxDepth, tupleMemberCount), genericTypeName(maxDepth, genericMemberCount), innerTypeName(maxDepth), functionalTypeName(maxDepth) ) ) typeStr <- generator } yield typeStr def complexTypeName(maxDepth: Int): Gen[String] = if (maxDepth == 0) flatTypeIdentifier else deepTypeName(maxDepth) def innerTypeName(maxDepth: Int): Gen[String] = for { outerName <- singleTypeName innerType <- complexTypeName(maxDepth) } yield s"$outerName#$innerType" } ================================================ FILE: package.json ================================================ { "name": "scala-clippy-site", "dependencies": { "jsdom": "9.12.0" } } ================================================ FILE: plugin/src/main/resources/scalac-plugin.xml ================================================ clippy com.softwaremill.clippy.ClippyPlugin ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/AdviceLoader.scala ================================================ package com.softwaremill.clippy import java.io._ import java.net.{HttpURLConnection, URL} import java.util.zip.GZIPInputStream import com.softwaremill.clippy.Utils._ import scala.concurrent.{ExecutionContext, Future} import scala.io.Source import scala.tools.nsc.Global import scala.util.{Failure, Success, Try} import scala.collection.JavaConverters._ class AdviceLoader( global: Global, url: String, localStoreDir: File, projectAdviceFile: Option[File], localAdviceFiles: List[URL] )(implicit ec: ExecutionContext) { private val OneDayMillis = 1000L * 60 * 60 * 24 private val localStore = new File(localStoreDir, "clippy.json.gz") private lazy val resourcesAdvice: AdvicesAndWarnings = localAdviceFiles.map(loadAdviceFromUrL).reduceOption(_ ++ _).getOrElse(AdvicesAndWarnings.empty) private def loadAdviceFromUrL(url: URL): AdvicesAndWarnings = TryWith(url.openStream())(inputStreamToClippy(_)) match { case Success(clippyData) => AdvicesAndWarnings(clippyData.advices, clippyData.fatalWarnings) case Failure(_) => global.inform(s"Cannot load advice from ${url.getPath} : Ignoring.") AdvicesAndWarnings.empty } private lazy val projectAdvice: AdvicesAndWarnings = projectAdviceFile.map(file => loadAdviceFromUrL(file.toURI.toURL)).getOrElse(AdvicesAndWarnings.empty) def load(): Future[Clippy] = { val localClippy = if (!localStore.exists()) { fetchStoreParse() } else { val needsUpdate = System.currentTimeMillis() - localStore.lastModified() > OneDayMillis // fetching in the background val runningFetch = if (needsUpdate) { Some(fetchStoreParseInBackground()) } else None val localLoad = Try(loadLocally()) match { case Success(v) => Future.successful(v) case Failure(t) => Future.failed(t) } localLoad.map(bytes => inputStreamToClippy(decodeZippedBytes(bytes))).recoverWith { case e: Exception => global.warning(s"Cannot load advice from local store: $localStore. Trying to fetch from server") runningFetch.getOrElse(fetchStoreParse()) } } // Add in advice found in resources and project root localClippy.map( clippy => clippy.copy( advices = (projectAdvice.advices ++ resourcesAdvice.advices ++ clippy.advices).distinct, fatalWarnings = (projectAdvice.fatalWarnings ++ resourcesAdvice.fatalWarnings ++ clippy.fatalWarnings).distinct ) ) } private def fetchStoreParse(): Future[Clippy] = fetchCompressedJson() .map { bytes => storeLocallyInBackground(bytes) bytes } .map(bytes => inputStreamToClippy(decodeZippedBytes(bytes))) .recover { case e: Exception => global.inform(s"Unable to load/store local Clippy advice due to: ${e.getMessage}") Clippy(ClippyBuildInfo.version, Nil, Nil) } .andThen { case Success(v) => v.checkPluginVersion(ClippyBuildInfo.version, println) } private def fetchStoreParseInBackground(): Future[Clippy] = { val f = fetchStoreParse() f.onFailure { case e: Exception => global.inform(s"Cannot fetch data from $url due to: $e") } f } private def fetchCompressedJson(): Future[Array[Byte]] = Future { val u = new URL(url) val conn = u.openConnection().asInstanceOf[HttpURLConnection] try { conn.setRequestMethod("GET") inputStreamToBytes(conn.getInputStream) } finally conn.disconnect() } private def decodeZippedBytes(bytes: Array[Byte]): GZIPInputStream = new GZIPInputStream(decodeUtf8Bytes(bytes)) private def decodeUtf8Bytes(bytes: Array[Byte]): ByteArrayInputStream = new ByteArrayInputStream(bytes) private def inputStreamToClippy(byteStream: InputStream): Clippy = { import org.json4s.native.JsonMethods._ val data = Source.fromInputStream(byteStream, "UTF-8").getLines().mkString("\n") Clippy .fromJson(parse(data)) .getOrElse(throw new IllegalArgumentException("Cannot deserialize Clippy data")) } private def storeLocally(bytes: Array[Byte]): Unit = { if (!localStoreDir.isDirectory && !localStoreDir.mkdir()) { throw new IOException(s"Cannot create directory $localStoreDir") } TryWith(new FileOutputStream(localStore))(_.write(bytes)).get } private def storeLocallyInBackground(bytes: Array[Byte]): Unit = Future { runNonDaemon { storeLocally(bytes) } }.onFailure { case e: Exception => global.inform(s"Cannot store data at $localStore due to: $e") } private def loadLocally(source: File = localStore): Array[Byte] = inputStreamToBytes(new FileInputStream(source)) } object AdviceLoader { val localFile = "clippy.json.gz" } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/ClippyPlugin.scala ================================================ package com.softwaremill.clippy import java.io.File import java.net.{URL, URLClassLoader} import java.util.concurrent.TimeoutException import scala.collection.JavaConverters._ import scala.concurrent.Await import scala.concurrent.duration._ import scala.reflect.internal.util.Position import scala.tools.nsc.Global import scala.tools.nsc.plugins.Plugin import scala.tools.nsc.plugins.PluginComponent import scala.tools.util.PathResolver final case class AdvicesAndWarnings(advices: List[Advice], fatalWarnings: List[Warning]) { def ++(other: AdvicesAndWarnings): AdvicesAndWarnings = copy(advices = advices ++ other.advices, fatalWarnings = fatalWarnings ++ other.fatalWarnings) } object AdvicesAndWarnings { def empty: AdvicesAndWarnings = AdvicesAndWarnings(Nil, Nil) } class ClippyPlugin(val global: Global) extends Plugin { override val name: String = "clippy" override val description: String = "gives good advice" var url: String = "" var colorsConfig: ColorsConfig = ColorsConfig.Disabled var testMode = false val DefaultStoreDir = new File(System.getProperty("user.home"), ".clippy") var localStoreDir = DefaultStoreDir var projectRoot: Option[File] = None var initialFatalWarnings: List[Warning] = Nil lazy val localAdviceFiles = { val classPathURLs = new PathResolver(global.settings).result.asURLs val classLoader = new URLClassLoader(classPathURLs.toArray, getClass.getClassLoader) classLoader.getResources("clippy.json").asScala.toList } lazy val advicesAndWarnings = loadAdvicesAndWarnings(url, localStoreDir, projectRoot, localAdviceFiles) def getFatalWarningAdvice(warningText: String): Option[Warning] = advicesAndWarnings.fatalWarnings.find(warning => warning.pattern.matches(ExactT(warningText))) def handleError(pos: Position, msg: String): String = { val parsedMsg = CompilationErrorParser.parse(msg) val matchers = advicesAndWarnings.advices.map(_.errMatching.lift) val matches = matchers.flatMap(pf => parsedMsg.flatMap(pf)).distinct matches.size match { case 0 => (parsedMsg, colorsConfig) match { case (Some(tme: TypeMismatchError[ExactT]), cc: ColorsConfig.Enabled) => prettyPrintTypeMismatchError(tme, cc) case _ => msg } case 1 => matches.mkString(s"$msg\n Clippy advises: ", "", "") case _ => matches.mkString(s"$msg\n Clippy advises you to try one of these solutions: \n ", "\n or\n ", "") } } override def processOptions(options: List[String], error: (String) => Unit): Unit = { colorsConfig = colorsFromOptions(options) url = urlFromOptions(options) testMode = testModeFromOptions(options) localStoreDir = localStoreDirFromOptions(options) projectRoot = projectRootFromOptions(options) initialFatalWarnings = initialFatalWarningsFromOptions(options) if (testMode) { val r = global.reporter global.reporter = new FailOnWarningsReporter( new DelegatingReporter(r, handleError, colorsConfig), getFatalWarningAdvice, colorsConfig ) } } override val components: List[PluginComponent] = { List( new InjectReporter(handleError, getFatalWarningAdvice, global) { override def colorsConfig = ClippyPlugin.this.colorsConfig override def isEnabled = !testMode }, new RestoreReporter(global) { override def isEnabled = !testMode } ) } private def prettyPrintTypeMismatchError(tme: TypeMismatchError[ExactT], colors: ColorsConfig.Enabled): String = { val colorDiff = (s: String) => colors.diff(s).toString val plain = new StringDiff(tme.found.toString, tme.required.toString, colorDiff) val expandsMsg = if (tme.hasExpands) { val reqExpandsTo = tme.requiredExpandsTo.getOrElse(tme.required) val foundExpandsTo = tme.foundExpandsTo.getOrElse(tme.found) val expands = new StringDiff(foundExpandsTo.toString, reqExpandsTo.toString, colorDiff) s"""${expands.diff("\nExpanded types:\nfound : %s\nrequired: %s\"")}""" } else "" s""" type mismatch; | Clippy advises, pay attention to the marked parts: | ${plain.diff("found : %s\n required: %s")}$expandsMsg${tme.notesAfterNewline}""".stripMargin } private def urlFromOptions(options: List[String]): String = options.find(_.startsWith("url=")).map(_.substring(4)).getOrElse("https://www.scala-clippy.org") + "/api/advices" private def colorsFromOptions(options: List[String]): ColorsConfig = if (boolFromOptions(options, "colors")) { def colorToFansi(color: String): fansi.Attrs = color match { case "black" => fansi.Color.Black case "light-gray" => fansi.Color.LightGray case "dark-gray" => fansi.Color.DarkGray case "red" => fansi.Color.Red case "light-red" => fansi.Color.LightRed case "green" => fansi.Color.Green case "light-green" => fansi.Color.LightGreen case "yellow" => fansi.Color.Yellow case "light-yellow" => fansi.Color.LightYellow case "blue" => fansi.Color.Blue case "light-blue" => fansi.Color.LightBlue case "magenta" => fansi.Color.Magenta case "light-magenta" => fansi.Color.LightMagenta case "cyan" => fansi.Color.Cyan case "light-cyan" => fansi.Color.LightCyan case "white" => fansi.Color.White case "none" => fansi.Attrs.Empty case x => global.warning("Unknown color: " + x) fansi.Attrs.Empty } val partColorPattern = "colors-(.*)=(.*)".r options.filter(_.startsWith("colors-")).foldLeft(ColorsConfig.defaultEnabled) { case (current, partAndColor) => val partColorPattern(part, colorStr) = partAndColor val color = colorToFansi(colorStr.trim.toLowerCase()) part.trim.toLowerCase match { case "diff" => current.copy(diff = color) case "comment" => current.copy(comment = color) case "type" => current.copy(`type` = color) case "literal" => current.copy(literal = color) case "keyword" => current.copy(keyword = color) case "reset" => current.copy(reset = color) case x => global.warning("Unknown colored part: " + x) current } } } else ColorsConfig.Disabled private def testModeFromOptions(options: List[String]): Boolean = boolFromOptions(options, "testmode") private def boolFromOptions(options: List[String], option: String): Boolean = options .find(_.startsWith(s"$option=")) .map(_.substring(option.length + 1)) .getOrElse("false") .toBoolean private def projectRootFromOptions(options: List[String]): Option[File] = options .find(_.startsWith("projectRoot=")) .map(_.substring(12)) .map(new File(_, ".clippy.json")) .filter(_.exists()) private def initialFatalWarningsFromOptions(options: List[String]): List[Warning] = options .find(_.startsWith("fatalWarnings=")) .map(_.substring(14)) .map { str => str.split('|').toList.map(str => Warning(RegexT(str), text = None)) } .getOrElse(Nil) private def localStoreDirFromOptions(options: List[String]): File = options.find(_.startsWith("store=")).map(_.substring(6)).map(new File(_)).getOrElse(DefaultStoreDir) private def loadAdvicesAndWarnings( url: String, localStoreDir: File, projectAdviceFile: Option[File], localAdviceFiles: List[URL] ): AdvicesAndWarnings = { implicit val ec = scala.concurrent.ExecutionContext.Implicits.global try { val clippyData = Await .result( new AdviceLoader(global, url, localStoreDir, projectAdviceFile, localAdviceFiles).load(), 10.seconds ) AdvicesAndWarnings(clippyData.advices, clippyData.fatalWarnings ++ initialFatalWarnings) } catch { case e: TimeoutException => global.warning(s"Unable to read advices from $url and store to $localStoreDir within 10 seconds.") AdvicesAndWarnings.empty case e: Exception => global.warning(s"Exception when reading advices from $url and storing to $localStoreDir: $e") AdvicesAndWarnings.empty } } } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/ColorsConfig.scala ================================================ package com.softwaremill.clippy sealed trait ColorsConfig object ColorsConfig { case object Disabled extends ColorsConfig case class Enabled( diff: fansi.Attrs, comment: fansi.Attrs, `type`: fansi.Attrs, literal: fansi.Attrs, keyword: fansi.Attrs, reset: fansi.Attrs ) extends ColorsConfig val defaultEnabled = Enabled( fansi.Color.Red, fansi.Color.Blue, fansi.Color.Green, fansi.Color.Magenta, fansi.Color.Yellow, fansi.Attr.Reset ) } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/FailOnWarningsReporter.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.Position import scala.tools.nsc.reporters.Reporter class FailOnWarningsReporter(r: Reporter, warningMatcher: String => Option[Warning], colorsConfig: ColorsConfig) extends Reporter { override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) // cannot delegate to info0 as it's protected, hence special-casing on the possible severity values if (severity == INFO) { r.info(wrapped, msg, force) } else if (severity == WARNING) { warning(wrapped, msg) } else if (severity == ERROR) { error(wrapped, msg) } else { error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg") } } override def echo(msg: String) = r.echo(msg) override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg) override def hasErrors = r.hasErrors || cancelled override def reset() = { cancelled = false r.reset() } // override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg) override def errorCount = r.errorCount override def warningCount = r.warningCount override def hasWarnings = r.hasWarnings override def flush() = r.flush() override def count(severity: Severity): Int = r.count(conv(severity)) override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity)) // private def conv(s: Severity): r.Severity = s match { case INFO => r.INFO case WARNING => r.WARNING case ERROR => r.ERROR } // override def warning(pos: Position, msg: String) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) warningMatcher(msg) match { case Some(Warning(_, adviceOpt)) => val finalMsg = adviceOpt.map(advice => msg + s"\nClippy advises: $advice").getOrElse(msg) r.error(wrapped, finalMsg) case None => r.warning(wrapped, msg) } } override def error(pos: Position, msg: String) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) r.error(wrapped, msg) } } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/Highlighter.scala ================================================ package com.softwaremill.clippy import fastparse.all._ import scalaparse.Scala._ import scalaparse.syntax.Identifiers._ /** * Copied from https://github.com/lihaoyi/Ammonite/blob/master/amm/repl/src/main/scala/ammonite/repl/Highlighter.scala */ object Highlighter { object BackTicked { def unapplySeq(s: Any): Option[List[String]] = "`([^`]+)`".r.unapplySeq(s.toString) } def flattenIndices( boundedIndices: Seq[(Int, fansi.Attrs, Boolean)], buffer: Vector[Char] ) = boundedIndices .sliding(2) .map { case Seq((s, c1, _), (e, c2, _)) => assert(e >= s, s"s: $s e: $e") c1(fansi.Str(buffer.slice(s, e), errorMode = fansi.ErrorMode.Sanitize)) } .reduce(_ ++ _) .render .toVector def defaultHighlight( buffer: Vector[Char], comment: fansi.Attrs, `type`: fansi.Attrs, literal: fansi.Attrs, keyword: fansi.Attrs, reset: fansi.Attrs ) = { val boundedIndices = defaultHighlightIndices(buffer, comment, `type`, literal, keyword, reset) flattenIndices(boundedIndices, buffer) } def defaultHighlightIndices( buffer: Vector[Char], comment: fansi.Attrs, `type`: fansi.Attrs, literal: fansi.Attrs, keyword: fansi.Attrs, reset: fansi.Attrs ) = Highlighter.highlightIndices( Parsers.Splitter, buffer, { case Literals.Expr.Interp | Literals.Pat.Interp => reset case Literals.Comment => comment case ExprLiteral => literal case TypeId => `type` case BackTicked(body) if alphaKeywords.contains(body) => keyword }, reset ) def highlightIndices[T]( parser: Parser[_], buffer: Vector[Char], ruleColors: PartialFunction[Parser[_], T], endColor: T ): Seq[(Int, T, Boolean)] = { val indices = { var indices = collection.mutable.Buffer((0, endColor, false)) var done = false val input = buffer.mkString parser.parse( input, instrument = (rule, idx, res) => { for (color <- ruleColors.lift(rule)) { val closeColor = indices.last._2 val startIndex = indices.length indices += ((idx, color, true)) res() match { case s: Parsed.Success[_] => val prev = indices(startIndex - 1)._1 if (idx < prev && s.index <= prev) { indices.remove(startIndex, indices.length - startIndex) } while (idx < indices.last._1 && s.index <= indices.last._1) { indices.remove(indices.length - 1) } indices += ((s.index, closeColor, false)) if (s.index == buffer.length) done = true case f: Parsed.Failure if f.index == buffer.length && (WL ~ End).parse(input, idx).isInstanceOf[Parsed.Failure] => // EOF, stop all further parsing done = true case _ => // hard failure, or parsed nothing. Discard all progress indices.remove(startIndex, indices.length - startIndex) } } } ) indices } // Make sure there's an index right at the start and right at the end! This // resets the colors at the snippet's end so they don't bleed into later output indices ++ Seq((999999999, endColor, false)) } def highlight( parser: Parser[_], buffer: Vector[Char], ruleColors: PartialFunction[Parser[_], fansi.Attrs], endColor: fansi.Attrs ) = { val boundedIndices = highlightIndices(parser, buffer, ruleColors, endColor) flattenIndices(boundedIndices, buffer) } } object Parsers { import fastparse.noApi._ import scalaparse.Scala._ import WhitespaceApi._ val Prelude = P((Annot ~ OneNLMax).rep ~ (Mod ~/ Pass).rep) val Statement = P(scalaparse.Scala.TopPkgSeq | scalaparse.Scala.Import | Prelude ~ BlockDef | StatCtx.Expr) def StatementBlock(blockSep: P0) = P(Semis.? ~ (!blockSep ~ Statement ~~ WS ~~ (Semis | End)).!.repX) val Splitter = P(StatementBlock(Fail) ~ WL ~ End) } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/InjectReporter.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.Position import scala.tools.nsc.plugins.PluginComponent import scala.tools.nsc.{Global, Phase} /** * Responsible for replacing the global reporter with our custom Clippy reporter after the first phase of compilation. */ abstract class InjectReporter( handleError: (Position, String) => String, getFatalWarningAdvice: String => Option[Warning], superGlobal: Global ) extends PluginComponent { override val global = superGlobal def colorsConfig: ColorsConfig def isEnabled: Boolean override val runsAfter = List[String]("parser") override val runsBefore = List[String]("namer") override val phaseName = "inject-clippy-reporter" override def newPhase(prev: Phase) = new Phase(prev) { override def name = phaseName override def description = "Switches the reporter to Clippy's reporter chain" override def run(): Unit = if (isEnabled) { val r = global.reporter global.reporter = new FailOnWarningsReporter( new DelegatingReporter(r, handleError, colorsConfig), getFatalWarningAdvice, colorsConfig ) } } } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/RestoreReporter.scala ================================================ package com.softwaremill.clippy import scala.tools.nsc.plugins.PluginComponent import scala.tools.nsc.{Global, Phase} /** * Replaces global reporter back with the original global reporter. Sbt uses its own xsbt.DelegatingReporter * which we cannot replace outside of Scala compilation phases. This component makes sure that before the compilation * is over, original reporter gets reassigned to the global field. */ class RestoreReporter(val global: Global) extends PluginComponent { val originalReporter = global.reporter def isEnabled: Boolean = true override val runsAfter = List[String]("jvm") override val runsBefore = List[String]("terminal") override val phaseName = "restore-original-reporter" override def newPhase(prev: Phase) = new Phase(prev) { override def name = phaseName override def description = "Switches the reporter from Clippy's DelegatingReporter back to original one" override def run(): Unit = if (isEnabled) global.reporter = originalReporter } } ================================================ FILE: plugin/src/main/scala/com/softwaremill/clippy/Utils.scala ================================================ package com.softwaremill.clippy import java.io.{ByteArrayOutputStream, InputStream} import java.io.Closeable import scala.util.control.NonFatal import scala.util.{Failure, Try} object Utils { /** * All future callbacks will be running on a daemon thread pool which can be interrupted at any time if the JVM * exits, if the compiler finished its job. * * Here we are trying to make as sure as possible (unless the JVM crashes) that we'll run the given code. */ def runNonDaemon(t: => Unit) = { val shutdownHook = new Thread() { private val lock = new Object @volatile private var didRun = false override def run() = lock.synchronized { if (!didRun) { t didRun = true } } } Runtime.getRuntime.addShutdownHook(shutdownHook) try shutdownHook.run() finally Runtime.getRuntime.removeShutdownHook(shutdownHook) } def inputStreamToBytes(is: InputStream): Array[Byte] = try { val baos = new ByteArrayOutputStream() val buf = new Array[Byte](512) var read = 0 while ({ read = is.read(buf, 0, buf.length); read } != -1) { baos.write(buf, 0, read) } baos.toByteArray } finally is.close() object TryWith { def apply[C <: Closeable, R](resource: => C)(f: C => R): Try[R] = Try(resource).flatMap(resourceInstance => { try { val returnValue = f(resourceInstance) Try(resourceInstance.close()).map(_ => returnValue) } catch { case NonFatal(exceptionInFunction) => try { resourceInstance.close() Failure(exceptionInFunction) } catch { case NonFatal(exceptionInClose) => exceptionInFunction.addSuppressed(exceptionInClose) Failure(exceptionInFunction) } } }) } } ================================================ FILE: plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingPosition.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.{NoPosition, Position, SourceFile} import scala.reflect.macros.Attachments class DelegatingPosition(delegate: Position, colorsConfig: ColorsConfig) extends Position { // used by scalac to report errors override def showError(msg: String): String = highlight(delegate.showError(msg)) // used by sbt override def lineContent: String = highlight(delegate.lineContent) def highlight(str: String): String = colorsConfig match { case e: ColorsConfig.Enabled => Highlighter .defaultHighlight( str.toVector, e.comment, e.`type`, e.literal, e.keyword, e.reset ) .mkString case _ => str } // impl copied override def fail(what: String): Nothing = throw new UnsupportedOperationException(s"Position.$what on $this") // simple delegates with position wrapping when position is returned @scala.deprecated("use `point`") override def offset: Option[Int] = delegate.offset override def all: Set[Any] = delegate.all @scala.deprecated("use `focus`") override def toSingleLine: Position = DelegatingPosition.wrap(delegate.toSingleLine, colorsConfig) override def pos: Position = DelegatingPosition.wrap(delegate.pos, colorsConfig) override def get[T](implicit evidence$2: ClassManifest[T]): Option[T] = delegate.get(evidence$2) override def finalPosition: Pos = DelegatingPosition.wrap(delegate.finalPosition, colorsConfig) override def withPos(newPos: Position): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.withPos(newPos) @scala.deprecated("use `line`") override def safeLine: Int = delegate.safeLine override def isTransparent: Boolean = delegate.isTransparent override def contains[T](implicit evidence$3: ClassManifest[T]): Boolean = delegate.contains(evidence$3) override def isOffset: Boolean = delegate.isOffset @scala.deprecated("use `showDebug`") override def dbgString: String = delegate.dbgString override def isOpaqueRange: Boolean = delegate.isOpaqueRange override def update[T](attachment: T)( implicit evidence$4: ClassManifest[T] ): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.update(attachment)(evidence$4) override def pointOrElse(alt: Int): Int = delegate.pointOrElse(alt) @scala.deprecated("use `finalPosition`") override def inUltimateSource(source: SourceFile): Position = DelegatingPosition.wrap(delegate.inUltimateSource(source), colorsConfig) override def isDefined: Boolean = delegate.isDefined override def makeTransparent: Position = DelegatingPosition.wrap(delegate.makeTransparent, colorsConfig) override def isRange: Boolean = delegate.isRange override def source: SourceFile = delegate.source override def remove[T]( implicit evidence$5: ClassManifest[T] ): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.remove(evidence$5) override def withStart(start: Int): Position = DelegatingPosition.wrap(delegate.withStart(start), colorsConfig) @scala.deprecated("use `lineCaret`") override def lineWithCarat(maxWidth: Int): (String, String) = delegate.lineWithCarat(maxWidth) override def start: Int = delegate.start override def withPoint(point: Int): Position = DelegatingPosition.wrap(delegate.withPoint(point), colorsConfig) override def point: Int = delegate.point override def isEmpty: Boolean = delegate.isEmpty override def end: Int = delegate.end override def withEnd(end: Int): Position = DelegatingPosition.wrap(delegate.withEnd(end), colorsConfig) @scala.deprecated("Use `withSource(source)` and `withShift`") override def withSource(source: SourceFile, shift: Int): Position = DelegatingPosition.wrap(delegate.withSource(source, shift), colorsConfig) override def withSource(source: SourceFile): Position = DelegatingPosition.wrap(delegate.withSource(source), colorsConfig) @scala.deprecated("Use `start` instead") override def startOrPoint: Int = delegate.startOrPoint override def withShift(shift: Int): Position = DelegatingPosition.wrap(delegate.withShift(shift), colorsConfig) @scala.deprecated("Use `end` instead") override def endOrPoint: Int = delegate.endOrPoint override def focusStart: Position = DelegatingPosition.wrap(delegate.focusStart, colorsConfig) override def focus: Position = DelegatingPosition.wrap(delegate.focus, colorsConfig) override def focusEnd: Position = DelegatingPosition.wrap(delegate.focusEnd, colorsConfig) override def |(that: Position, poses: Position*): Position = DelegatingPosition.wrap(delegate.|(that, poses: _*), colorsConfig) override def |(that: Position): Position = DelegatingPosition.wrap(delegate.|(that), colorsConfig) override def ^(point: Int): Position = DelegatingPosition.wrap(delegate.^(point), colorsConfig) override def |^(that: Position): Position = DelegatingPosition.wrap(delegate.|^(that), colorsConfig) override def ^|(that: Position): Position = DelegatingPosition.wrap(delegate.^|(that), colorsConfig) override def union(pos: Position): Position = DelegatingPosition.wrap(delegate.union(pos), colorsConfig) override def includes(pos: Position): Boolean = delegate.includes(pos) override def properlyIncludes(pos: Position): Boolean = delegate.properlyIncludes(pos) override def precedes(pos: Position): Boolean = delegate.precedes(pos) override def properlyPrecedes(pos: Position): Boolean = delegate.properlyPrecedes(pos) override def sameRange(pos: Position): Boolean = delegate.sameRange(pos) override def overlaps(pos: Position): Boolean = delegate.overlaps(pos) override def line: Int = delegate.line override def column: Int = delegate.column override def lineCaret: String = delegate.lineCaret @scala.deprecated("use `lineCaret`") override def lineCarat: String = delegate.lineCarat override def showDebug: String = delegate.showDebug override def show: String = delegate.show } object DelegatingPosition { def wrap(pos: Position, colorsConfig: ColorsConfig): Position = pos match { case NoPosition => pos case wrapped: DelegatingPosition => wrapped case _ => new DelegatingPosition(pos, colorsConfig) } } ================================================ FILE: plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingReporter.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.Position import scala.tools.nsc.reporters.Reporter class DelegatingReporter(r: Reporter, handleError: (Position, String) => String, colorsConfig: ColorsConfig) extends Reporter { override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) // cannot delegate to info0 as it's protected, hence special-casing on the possible severity values if (severity == INFO) { r.info(wrapped, msg, force) } else if (severity == WARNING) { warning(wrapped, msg) } else if (severity == ERROR) { error(wrapped, msg) } else { error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg") } } override def echo(msg: String) = r.echo(msg) override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg) override def hasErrors = r.hasErrors || cancelled override def reset() = { cancelled = false r.reset() } // override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg) override def warning(pos: Position, msg: String) = r.warning(DelegatingPosition.wrap(pos, colorsConfig), msg) override def errorCount = r.errorCount override def warningCount = r.warningCount override def hasWarnings = r.hasWarnings override def flush() = r.flush() override def count(severity: Severity): Int = r.count(conv(severity)) override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity)) // private def conv(s: Severity): r.Severity = s match { case INFO => r.INFO case WARNING => r.WARNING case ERROR => r.ERROR } // override def error(pos: Position, msg: String) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) r.error(wrapped, handleError(wrapped, msg)) } } ================================================ FILE: plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingPosition.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.{NoPosition, Position, SourceFile} import scala.reflect.macros.Attachments class DelegatingPosition(delegate: Position, colorsConfig: ColorsConfig) extends Position { // used by scalac to report errors override def showError(msg: String): String = highlight(delegate.showError(msg)) // used by sbt override def lineContent: String = highlight(delegate.lineContent) def highlight(str: String): String = colorsConfig match { case e: ColorsConfig.Enabled => Highlighter .defaultHighlight( str.toVector, e.comment, e.`type`, e.literal, e.keyword, e.reset ) .mkString case _ => str } // impl copied override def fail(what: String): Nothing = throw new UnsupportedOperationException(s"Position.$what on $this") // simple delegates with position wrapping when position is returned @scala.deprecated("use `point`") override def offset: Option[Int] = delegate.offset override def all: Set[Any] = delegate.all @scala.deprecated("use `focus`") override def toSingleLine: Position = DelegatingPosition.wrap(delegate.toSingleLine, colorsConfig) override def pos: Position = DelegatingPosition.wrap(delegate.pos, colorsConfig) override def get[T](implicit evidence$2: ClassManifest[T]): Option[T] = delegate.get(evidence$2) override def finalPosition: Pos = DelegatingPosition.wrap(delegate.finalPosition, colorsConfig) override def withPos(newPos: Position): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.withPos(newPos) @scala.deprecated("use `line`") override def safeLine: Int = delegate.safeLine override def isTransparent: Boolean = delegate.isTransparent override def contains[T](implicit evidence$3: ClassManifest[T]): Boolean = delegate.contains(evidence$3) override def isOffset: Boolean = delegate.isOffset @scala.deprecated("use `showDebug`") override def dbgString: String = delegate.dbgString override def isOpaqueRange: Boolean = delegate.isOpaqueRange override def update[T](attachment: T)( implicit evidence$4: ClassManifest[T] ): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.update(attachment)(evidence$4) override def pointOrElse(alt: Int): Int = delegate.pointOrElse(alt) @scala.deprecated("use `finalPosition`") override def inUltimateSource(source: SourceFile): Position = DelegatingPosition.wrap(delegate.inUltimateSource(source), colorsConfig) override def isDefined: Boolean = delegate.isDefined override def makeTransparent: Position = DelegatingPosition.wrap(delegate.makeTransparent, colorsConfig) override def isRange: Boolean = delegate.isRange override def source: SourceFile = delegate.source override def remove[T]( implicit evidence$5: ClassManifest[T] ): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.remove(evidence$5) override def withStart(start: Int): Position = DelegatingPosition.wrap(delegate.withStart(start), colorsConfig) @scala.deprecated("use `lineCaret`") override def lineWithCarat(maxWidth: Int): (String, String) = delegate.lineWithCarat(maxWidth) override def start: Int = delegate.start override def withPoint(point: Int): Position = DelegatingPosition.wrap(delegate.withPoint(point), colorsConfig) override def point: Int = delegate.point override def isEmpty: Boolean = delegate.isEmpty override def end: Int = delegate.end override def withEnd(end: Int): Position = DelegatingPosition.wrap(delegate.withEnd(end), colorsConfig) @scala.deprecated("Use `withSource(source)` and `withShift`") override def withSource(source: SourceFile, shift: Int): Position = DelegatingPosition.wrap(delegate.withSource(source, shift), colorsConfig) override def withSource(source: SourceFile): Position = DelegatingPosition.wrap(delegate.withSource(source), colorsConfig) @scala.deprecated("Use `start` instead") override def startOrPoint: Int = delegate.startOrPoint override def withShift(shift: Int): Position = DelegatingPosition.wrap(delegate.withShift(shift), colorsConfig) @scala.deprecated("Use `end` instead") override def endOrPoint: Int = delegate.endOrPoint override def focusStart: Position = DelegatingPosition.wrap(delegate.focusStart, colorsConfig) override def focus: Position = DelegatingPosition.wrap(delegate.focus, colorsConfig) override def focusEnd: Position = DelegatingPosition.wrap(delegate.focusEnd, colorsConfig) override def |(that: Position, poses: Position*): Position = DelegatingPosition.wrap(delegate.|(that, poses: _*), colorsConfig) override def |(that: Position): Position = DelegatingPosition.wrap(delegate.|(that), colorsConfig) override def ^(point: Int): Position = DelegatingPosition.wrap(delegate.^(point), colorsConfig) override def |^(that: Position): Position = DelegatingPosition.wrap(delegate.|^(that), colorsConfig) override def ^|(that: Position): Position = DelegatingPosition.wrap(delegate.^|(that), colorsConfig) override def union(pos: Position): Position = DelegatingPosition.wrap(delegate.union(pos), colorsConfig) override def includes(pos: Position): Boolean = delegate.includes(pos) override def properlyIncludes(pos: Position): Boolean = delegate.properlyIncludes(pos) override def precedes(pos: Position): Boolean = delegate.precedes(pos) override def properlyPrecedes(pos: Position): Boolean = delegate.properlyPrecedes(pos) override def sameRange(pos: Position): Boolean = delegate.sameRange(pos) override def overlaps(pos: Position): Boolean = delegate.overlaps(pos) override def line: Int = delegate.line override def column: Int = delegate.column override def lineCaret: String = delegate.lineCaret @scala.deprecated("use `lineCaret`") override def lineCarat: String = delegate.lineCarat override def showDebug: String = delegate.showDebug override def show: String = delegate.show } object DelegatingPosition { def wrap(pos: Position, colorsConfig: ColorsConfig): Position = pos match { case NoPosition => pos case wrapped: DelegatingPosition => wrapped case _ => new DelegatingPosition(pos, colorsConfig) } } ================================================ FILE: plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingReporter.scala ================================================ package com.softwaremill.clippy import scala.reflect.internal.util.Position import scala.tools.nsc.reporters.Reporter class DelegatingReporter(r: Reporter, handleError: (Position, String) => String, colorsConfig: ColorsConfig) extends Reporter { override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) // cannot delegate to info0 as it's protected, hence special-casing on the possible severity values if (severity == INFO) { r.info(wrapped, msg, force) } else if (severity == WARNING) { warning(wrapped, msg) } else if (severity == ERROR) { error(wrapped, msg) } else { error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg") } } override def echo(msg: String) = r.echo(msg) override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg) override def hasErrors = r.hasErrors || cancelled override def reset() = { cancelled = false r.reset() } // override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg) override def warning(pos: Position, msg: String) = r.warning(DelegatingPosition.wrap(pos, colorsConfig), msg) override def errorCount = r.errorCount override def warningCount = r.warningCount override def hasWarnings = r.hasWarnings override def flush() = r.flush() override def count(severity: Severity): Int = r.count(conv(severity)) override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity)) // private def conv(s: Severity): r.Severity = s match { case INFO => r.INFO case WARNING => r.WARNING case ERROR => r.ERROR } // override def error(pos: Position, msg: String) = { val wrapped = DelegatingPosition.wrap(pos, colorsConfig) r.error(wrapped, handleError(wrapped, msg)) } } ================================================ FILE: plugin-sbt/src/main/scala/com/softwaremill/clippy/ClippySbtPlugin.scala ================================================ package com.softwaremill.clippy import sbt._ import sbt.Keys._ import scala.collection.mutable.ListBuffer object ClippySbtPlugin extends AutoPlugin { object ClippyColor extends Enumeration { val Black = Value("black") val LightGray = Value("light-gray") val DarkGray = Value("dark-gray") val Red = Value("red") val LightRed = Value("light-red") val Green = Value("green") val LightGreen = Value("light-green") val Yellow = Value("yellow") val LightYellow = Value("light-yellow") val Blue = Value("blue") val LightBlue = Value("light-blue") val Magenta = Value("magenta") val LightMagenta = Value("light-magenta") val Cyan = Value("cyan") val LightCyan = Value("light-cyan") val White = Value("white") val None = Value("none") } object WarningPatterns { val NonExhaustiveMatch = "match may not be exhaustive[\\s\\S]*" } object autoImport { val clippyColorsEnabled = settingKey[Boolean]("Should Clippy color type mismatch diffs and highlight syntax") val clippyColorDiff = settingKey[Option[ClippyColor.Value]]("The color to use for diffs, if other than default") val clippyColorComment = settingKey[Option[ClippyColor.Value]]("The color to use for comments, if other than default") val clippyColorType = settingKey[Option[ClippyColor.Value]]("The color to use for types, if other than default") val clippyColorLiteral = settingKey[Option[ClippyColor.Value]]("The color to use for literals, if other than default") val clippyColorKeyword = settingKey[Option[ClippyColor.Value]]("The color to use for keywords, if other than default") val clippyColorReset = settingKey[Option[ClippyColor.Value]]("The color to use for resetting to neutral, if other than default") val clippyUrl = settingKey[Option[String]]("Url from which to fetch advice, if other than default") val clippyLocalStoreDir = settingKey[Option[String]]("Directory where cached advice data should be stored, if other than default") val clippyProjectRoot = settingKey[Option[String]]("Project root in which project-specific advice is stored, if any") val clippyFatalWarnings = settingKey[List[String]]("Regular expressions of warning messages which should fail compilation") val NonExhaustiveMatch = "match may not be exhaustive[\\s\\S]*" } // in ~/.sbt auto import doesn't work, so providing aliases here for convenience val clippyColorsEnabled = autoImport.clippyColorsEnabled val clippyColorDiff = autoImport.clippyColorDiff val clippyColorComment = autoImport.clippyColorComment val clippyColorType = autoImport.clippyColorType val clippyColorLiteral = autoImport.clippyColorLiteral val clippyColorKeyword = autoImport.clippyColorKeyword val clippyColorReset = autoImport.clippyColorReset val clippyUrl = autoImport.clippyUrl val clippyLocalStoreDir = autoImport.clippyLocalStoreDir val clippyProjectRoot = autoImport.clippyProjectRoot val clippyFatalWarnings = autoImport.clippyFatalWarnings override def projectSettings = Seq( clippyColorsEnabled := false, clippyColorDiff := None, clippyColorComment := None, clippyColorType := None, clippyColorLiteral := None, clippyColorKeyword := None, clippyColorReset := None, clippyUrl := None, clippyLocalStoreDir := None, clippyProjectRoot := None, clippyFatalWarnings := Nil, addCompilerPlugin("com.softwaremill.clippy" %% "plugin" % ClippyBuildInfo.version classifier "bundle"), scalacOptions := { val result = ListBuffer(scalacOptions.value: _*) if (clippyColorsEnabled.value) result += "-P:clippy:colors=true" clippyColorDiff.value.foreach(c => result += s"-P:clippy:colors-diff=$c") clippyColorComment.value.foreach(c => result += s"-P:clippy:colors-comment=$c") clippyColorType.value.foreach(c => result += s"-P:clippy:colors-type=$c") clippyColorLiteral.value.foreach(c => result += s"-P:clippy:colors-literal=$c") clippyColorKeyword.value.foreach(c => result += s"-P:clippy:colors-keyword=$c") clippyColorReset.value.foreach(c => result += s"-P:clippy:colors-reset=$c") clippyUrl.value.foreach(c => result += s"-P:clippy:url=$c") clippyLocalStoreDir.value.foreach(c => result += s"-P:clippy:store=$c") clippyProjectRoot.value.foreach(c => result += s"-P:clippy:projectRoot=$c") if (clippyFatalWarnings.value.nonEmpty) result += s"-P:clippy:fatalWarnings=${clippyFatalWarnings.value.mkString("|")}" result.toList } ) override def trigger = allRequirements } ================================================ FILE: project/build.properties ================================================ sbt.version=0.13.17 ================================================ FILE: project/plugins.sbt ================================================ resolvers += Resolver.typesafeRepo("releases") // Workaround for the bug: https://github.com/sbt/sbt-assembly/issues/236 resolvers += "JBoss" at "https://repository.jboss.org" addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.4.0") addSbtPlugin("com.updateimpact" % "updateimpact-sbt-plugin" % "2.1.1") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.4.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1") addSbtPlugin("com.heroku" % "sbt-heroku" % "0.5.4") addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.6.8") // The Play plugin addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.6") // web plugins addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.0") addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3") addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0") addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0") // scalajs addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.3") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.15") ================================================ FILE: tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala ================================================ package org.softwaremill.clippy import java.io.{File, FileOutputStream} import java.util.zip.GZIPOutputStream import scala.reflect.runtime.currentMirror import com.softwaremill.clippy._ import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import scala.tools.reflect.ToolBox import scala.tools.reflect.ToolBoxError class CompileTests extends FlatSpec with Matchers with BeforeAndAfterAll { val localStoreDir = new File(System.getProperty("user.home"), ".clippy") val localStore = new File(localStoreDir, "clippy.json.gz") val localStore2 = new File(localStoreDir, "clippy2.json.gz") /** * Writing test json data to where the plugin will expect to have it cached. */ override protected def beforeAll() = { super.beforeAll() localStoreDir.mkdirs() if (localStore.exists()) { localStore.renameTo(localStore2) } val advices = List( Advice( TypeMismatchError(ExactT("slick.dbio.DBIOAction[*]"), None, ExactT("slick.lifted.Rep[Option[*]]"), None, None).asRegex, "Perhaps you forgot to call .result on your Rep[]? This will give you a DBIOAction that you can compose with other DBIOActions.", Library("com.typesafe.slick", "slick", "3.1.0") ), Advice( 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, None ).asRegex, "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", Library("com.typesafe.akka", "akka-http-experimental", "2.0.0") ), Advice( NotFoundError(ExactT("value wire")).asRegex, "you need to import com.softwaremill.macwire._", Library("com.softwaremill.macwire", "macros", "2.0.0") ), Advice( NotFoundError(ExactT("value wire")).asRegex, "If you need further help check out the macwire readme at https://github.com/adamw/macwire", Library("com.softwaremill.macwire", "macros", "2.0.0") ), Advice( TypeArgumentsDoNotConformToOverloadedBoundsError( ExactT("*"), ExactT("value apply"), Set( ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"), ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]") ) ).asRegex, "incorrect class name passed to TableQuery", Library("com.typesafe.slick", "slick", "3.1.1") ), Advice( TypeclassNotFoundError( ExactT("Ordering"), ExactT("java.time.LocalDate") ).asRegex, "implicit val localDateOrdering: Ordering[java.time.LocalDate] = Ordering.by(_.toEpochDay)", Library("java-lang", "time", "8+") ) ) import org.json4s.native.JsonMethods._ val data = compact(render(Clippy("0.1", advices, Nil).toJson)) val os = new GZIPOutputStream(new FileOutputStream(localStore)) try os.write(data.getBytes("UTF-8")) finally os.close() } override protected def afterAll() = { localStore.delete() if (localStore2.exists()) { localStore2.renameTo(localStore) } super.afterAll() } val snippets = Map( "akka http" -> """ |import akka.actor.ActorSystem |import akka.http.scaladsl.Http | |import akka.http.scaladsl.server.Directives._ | |implicit val system = ActorSystem() | |val r = complete("ok") | |Http().bindAndHandle(r, "localhost", 8080) """.stripMargin, "macwire" -> """ |class A() |val a = wire[A] """.stripMargin, "slick" -> """ |case class User(id1: Long, id2: Long) |trait TestSchema { | | val db: slick.jdbc.JdbcBackend#DatabaseDef | val driver: slick.driver.JdbcProfile | | import driver.api._ | | protected val users = TableQuery[User] | | protected class Users(tag: Tag) extends Table[User](tag, "users") { | def id1 = column[Long]("id") | def id2 = column[Long]("id") | | def * = (id1, id2) <> (User.tupled, User.unapply) | } |} """.stripMargin, "Type mismatch pretty diff" -> """ |class Test { | | type Cool = (String, String, Int, Option[String], Long) | type Bool = (String, String, Int, String, Long) | | def test(cool: Cool): Bool = cool | |} """.stripMargin ) val tb = { val cpp = sys.env("CLIPPY_PLUGIN_PATH") currentMirror.mkToolBox( options = s"-Xplugin:$cpp -Xplugin-require:clippy -P:clippy:colors=true -P:clippy:testmode=true" ) } def tryCompile(snippet: String) = tb.compile(tb.parse(snippet)) for ((name, s) <- snippets) { name should "compile with errors" in { (the[ToolBoxError] thrownBy tryCompile(s)).message should include("Clippy advises") } } "Clippy" should "return all matching advice" in { (the[ToolBoxError] thrownBy tryCompile(snippets("macwire"))).message should include( "Clippy advises you to try one of these" ) } } ================================================ FILE: ui/app/ClippyApplicationLoader.scala ================================================ import api.UiApiImpl import com.softwaremill.id.DefaultIdGenerator import controllers._ import dal.AdvicesRepository import play.api.ApplicationLoader.Context import play.api._ import play.api.i18n.I18nComponents import play.api.mvc.EssentialFilter import router.Routes import util.{DatabaseConfig, SqlDatabase} import util.email.{DummyEmailService, SendgridEmailService} class ClippyApplicationLoader extends ApplicationLoader { def load(context: Context) = { Logger.configure(context.environment) val c = new ClippyComponents(context) c.database.updateSchema() c.application } } class ClippyComponents(context: Context) extends BuiltInComponentsFromContext(context) with I18nComponents { implicit val ec = scala.concurrent.ExecutionContext.Implicits.global lazy val router = new Routes(httpErrorHandler, applicationController, assets, webJarAssets, advicesController, autowireController) lazy val contactEmail = configuration.getString("email.contact").getOrElse("?") lazy val emailService = SendgridEmailService .createFromEnv(contactEmail) .getOrElse(new DummyEmailService) lazy val webJarAssets = new WebJarAssets(httpErrorHandler, configuration, environment) lazy val assets = new controllers.Assets(httpErrorHandler) lazy val idGenerator = new DefaultIdGenerator() lazy val applicationController = new ApplicationController() lazy val database = SqlDatabase.create(new DatabaseConfig { override val rootConfig = configuration.underlying }) lazy val advicesRepository = new AdvicesRepository(database, idGenerator) lazy val uiApiImpl = new UiApiImpl(advicesRepository, emailService, contactEmail) lazy val autowireController = new AutowireController(uiApiImpl) lazy val advicesController = new AdvicesController(advicesRepository) override lazy val httpFilters: Seq[EssentialFilter] = List(new HttpsFilter()) } ================================================ FILE: ui/app/api/UiApiImpl.scala ================================================ package api import com.softwaremill.clippy._ import dal.AdvicesRepository import util.email.EmailService import scala.concurrent.{ExecutionContext, Future} class UiApiImpl( advicesRepository: AdvicesRepository, emailService: EmailService, contactEmail: String )(implicit ec: ExecutionContext) extends UiApi { override def sendCannotParse(errorText: String, contributorEmail: String) = emailService.send( contactEmail, "Clippy: unparseable message", s""" |Contributor email: $contributorEmail | |Error text: |$errorText """.stripMargin ) override def sendAdviceProposal(ap: AdviceProposal): Future[Unit] = advicesRepository .store( ap.errorTextRaw, ap.patternRaw, ap.compilationError, ap.advice, AdviceState.Pending, ap.library, ap.contributor, ap.comment ) .flatMap { a => emailService.send(contactEmail, "Clippy: new advice proposal", s""" |Advice proposal: |$a |""".stripMargin) } override def listAccepted() = advicesRepository.findAll().map(_.map(_.toAdviceListing)) override def sendSuggestEdit(text: String, contactEmail: String, adviceListing: AdviceListing) = emailService.send( contactEmail, "Clippy: edit suggestion", s""" |Edit suggestion for: $adviceListing |Contact email: $contactEmail | |Suggestion: |$text """.stripMargin ) override def feedback(text: String, contactEmail: String) = emailService.send(contactEmail, "Clippy: feedback", s""" |Contact email: $contactEmail | |Feedback: |$text """.stripMargin) } ================================================ FILE: ui/app/assets/stylesheets/main.less ================================================ #reactmain { padding-top: 60px; } html { position: relative; min-height: 100%; } .cout { width: 100%; padding-top: 5px; margin-bottom: 5px; } body { margin-bottom: 70px; } .footer { position: absolute; bottom: 0; width: 100%; height: 60px; background-color: #f5f5f5; margin-top: 10px; } .glyphicon-edit { cursor: pointer; } .advice-listing { table-layout: fixed; word-wrap: break-word; } ================================================ FILE: ui/app/controllers/AdvicesController.scala ================================================ package controllers import com.softwaremill.clippy.{Advice, Clippy} import dal.AdvicesRepository import play.api.mvc.{Action, Controller} import util.{ClippyBuildInfo, Zip} import scala.concurrent.{ExecutionContext, Future} class AdvicesController(advicesRepository: AdvicesRepository)(implicit ec: ExecutionContext) extends Controller { def get = Action.async { gzippedAdvices.map(a => Ok(a).withHeaders("Content-Encoding" -> "gzip")) } def gzippedAdvices: Future[Array[Byte]] = advicesRepository.findAll().map { storedAdvices => // TODO, once there's an admin: filter out not accepted advice val advices = storedAdvices //.filter(_.accepted) .map(_.toAdvice) Zip.compress(toJsonString(advices.toList)) } private def toJsonString(advices: List[Advice]): String = { import org.json4s.native.JsonMethods.{render => r, compact} compact(r(Clippy(ClippyBuildInfo.version, advices, Nil).toJson)) } } ================================================ FILE: ui/app/controllers/ApplicationController.scala ================================================ package controllers import play.api.mvc.{Action, Controller} class ApplicationController extends Controller { def index = Action { Ok(views.html.index()) } } ================================================ FILE: ui/app/controllers/AutowireController.scala ================================================ package controllers import com.softwaremill.clippy.UiApi import play.api.mvc.{Action, Controller} import upickle.default._ import upickle.Js import scala.concurrent.ExecutionContext class AutowireController(uiApi: UiApi)(implicit ec: ExecutionContext) extends Controller { def autowireApi(path: String) = Action.async { implicit request => val b = request.body.asText.getOrElse("") AutowireServer .route[UiApi](uiApi)( autowire.Core.Request( path.split("/"), upickle.json.read(b).asInstanceOf[Js.Obj].value.toMap ) ) .map(jsv => Ok(upickle.json.write(jsv))) } } object AutowireServer extends autowire.Server[Js.Value, Reader, Writer] { def read[Result: Reader](p: Js.Value) = upickle.default.readJs[Result](p) def write[Result: Writer](r: Result) = upickle.default.writeJs(r) } ================================================ FILE: ui/app/controllers/HttpsFilter.scala ================================================ package controllers import play.api.http.Status import play.api.mvc.{Filter, RequestHeader, Result, Results} import scala.concurrent.Future class HttpsFilter extends Filter { def apply(nextFilter: (RequestHeader) => Future[Result])(requestHeader: RequestHeader): Future[Result] = requestHeader.headers.get("x-forwarded-proto") match { case Some(header) => if (header == "https") { nextFilter(requestHeader) } else { Future.successful( Results.Redirect("https://" + requestHeader.host + requestHeader.uri, Status.MOVED_PERMANENTLY) ) } case None => nextFilter(requestHeader) } } ================================================ FILE: ui/app/dal/AdvicesRepository.scala ================================================ package dal import com.softwaremill.clippy.AdviceState.AdviceState import com.softwaremill.clippy._ import com.softwaremill.id.IdGenerator import util.SqlDatabase import scala.concurrent.{ExecutionContext, Future} class AdvicesRepository(database: SqlDatabase, idGenerator: IdGenerator)(implicit ec: ExecutionContext) { import database._ import database.driver.api._ private class AdvicesTable(tag: Tag) extends Table[StoredAdvice](tag, "advices") { def id = column[Long]("id", O.PrimaryKey) def errorTextRaw = column[String]("error_text_raw") def patternRaw = column[String]("pattern_raw") def compilationError = column[String]("compilation_error") def advice = column[String]("advice") def state = column[Int]("state") def libraryGroupId = column[String]("library_group_id") def libraryArtifactId = column[String]("library_artifact_id") def libraryVersion = column[String]("library_version") def contributorEmail = column[Option[String]]("contributor_email") def contributorTwitter = column[Option[String]]("contributor_twitter") def contributorGithub = column[Option[String]]("contributor_github") def comment = column[Option[String]]("comment") def * = ( id, errorTextRaw, patternRaw, compilationError, advice, state, (libraryGroupId, libraryArtifactId, libraryVersion), (contributorEmail, contributorGithub, contributorTwitter), comment ).shaped <> ({ t => StoredAdvice( t._1, t._2, t._3, CompilationError.fromJsonString(t._4).get, t._5, AdviceState(t._6), (Library.apply _).tupled(t._7), Contributor.tupled(t._8), t._9 ) }, { (a: StoredAdvice) => Some( ( a.id, a.errorTextRaw, a.patternRaw, a.compilationError.toJsonString, a.advice, a.state.id, Library.unapply(a.library).get, Contributor.unapply(a.contributor).get, a.comment ) ) }) } private val advices = TableQuery[AdvicesTable] def store( errorTextRaw: String, patternRaw: String, compilationError: CompilationError[RegexT], advice: String, state: AdviceState, library: Library, contributor: Contributor, comment: Option[String] ): Future[StoredAdvice] = { val a = StoredAdvice( idGenerator.nextId(), errorTextRaw, patternRaw, compilationError, advice, state, library, contributor, comment ) db.run(advices += a).map(_ => a) } def findAll(): Future[Seq[StoredAdvice]] = db.run(advices.result) } case class StoredAdvice( id: Long, errorTextRaw: String, patternRaw: String, compilationError: CompilationError[RegexT], advice: String, state: AdviceState, library: Library, contributor: Contributor, comment: Option[String] ) { def toAdvice = Advice(compilationError, advice, library) def toAdviceListing = AdviceListing(id, compilationError, advice, library, ContributorListing(contributor.github, contributor.twitter)) } ================================================ FILE: ui/app/util/ConfigWithDefault.scala ================================================ package util import java.util.concurrent.TimeUnit import com.typesafe.config.Config trait ConfigWithDefault { def rootConfig: Config def getBoolean(path: String, default: Boolean) = ifHasPath(path, default) { _.getBoolean(path) } def getString(path: String, default: String) = ifHasPath(path, default) { _.getString(path) } def getInt(path: String, default: Int) = ifHasPath(path, default) { _.getInt(path) } def getConfig(path: String, default: Config) = ifHasPath(path, default) { _.getConfig(path) } def getMilliseconds(path: String, default: Long) = ifHasPath(path, default) { _.getDuration(path, TimeUnit.MILLISECONDS) } def getOptionalString(path: String, default: Option[String] = None) = getOptional(path) { _.getString(path) } private def ifHasPath[T](path: String, default: T)(get: Config => T): T = if (rootConfig.hasPath(path)) get(rootConfig) else default private def getOptional[T](fullPath: String, default: Option[T] = None)(get: Config => T) = if (rootConfig.hasPath(fullPath)) { Some(get(rootConfig)) } else { default } } ================================================ FILE: ui/app/util/DatabaseConfig.scala ================================================ package util import com.typesafe.config.Config trait DatabaseConfig extends ConfigWithDefault { def rootConfig: Config import DatabaseConfig._ lazy val dbH2Url = getString(s"db.h2.properties.url", "jdbc:h2:file:./data") lazy val dbPostgresServerName = getString(PostgresServerNameKey, "") lazy val dbPostgresPort = getString(PostgresPortKey, "5432") lazy val dbPostgresDbName = getString(PostgresDbNameKey, "") lazy val dbPostgresUsername = getString(PostgresUsernameKey, "") lazy val dbPostgresPassword = getString(PostgresPasswordKey, "") } object DatabaseConfig { val PostgresDSClass = "db.postgres.dataSourceClass" val PostgresServerNameKey = "db.postgres.properties.serverName" val PostgresPortKey = "db.postgres.properties.portNumber" val PostgresDbNameKey = "db.postgres.properties.databaseName" val PostgresUsernameKey = "db.postgres.properties.user" val PostgresPasswordKey = "db.postgres.properties.password" } ================================================ FILE: ui/app/util/SqlDatabase.scala ================================================ package util import java.net.URI import com.typesafe.config.ConfigValueFactory._ import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.StrictLogging import org.flywaydb.core.Flyway import slick.driver.JdbcProfile import slick.jdbc.JdbcBackend._ case class SqlDatabase( db: slick.jdbc.JdbcBackend#Database, driver: JdbcProfile, connectionString: JdbcConnectionString ) { def updateSchema() { val flyway = new Flyway() flyway.setDataSource(connectionString.url, connectionString.username, connectionString.password) flyway.migrate() } def close() { db.close() } } case class JdbcConnectionString(url: String, username: String = "", password: String = "") object SqlDatabase extends StrictLogging { def create(config: DatabaseConfig): SqlDatabase = { val envDatabaseUrl = System.getenv("DATABASE_URL") if (config.dbPostgresServerName.length > 0) createPostgresFromConfig(config) else if (envDatabaseUrl != null) createPostgresFromEnv(envDatabaseUrl) else createEmbedded(config) } def createEmbedded(connectionString: String): SqlDatabase = { val db = Database.forURL(connectionString) SqlDatabase(db, slick.driver.H2Driver, JdbcConnectionString(connectionString)) } private def createPostgresFromEnv(envDatabaseUrl: String) = { import DatabaseConfig._ /* The DATABASE_URL is set by Heroku (if deploying there) and must be converted to a proper object of type Config (for Slick). Expected format: postgres://:@:/ */ val dbUri = new URI(envDatabaseUrl) val username = dbUri.getUserInfo.split(":")(0) val password = dbUri.getUserInfo.split(":")(1) val intermediaryConfig = new DatabaseConfig { override def rootConfig: Config = ConfigFactory .empty() .withValue(PostgresDSClass, fromAnyRef("org.postgresql.ds.PGSimpleDataSource")) .withValue(PostgresServerNameKey, fromAnyRef(dbUri.getHost)) .withValue(PostgresPortKey, fromAnyRef(dbUri.getPort)) .withValue(PostgresDbNameKey, fromAnyRef(dbUri.getPath.tail)) .withValue(PostgresUsernameKey, fromAnyRef(username)) .withValue(PostgresPasswordKey, fromAnyRef(password)) .withFallback(ConfigFactory.load()) } createPostgresFromConfig(intermediaryConfig) } private def postgresUrl(host: String, port: String, dbName: String) = s"jdbc:postgresql://$host:$port/$dbName" private def postgresConnectionString(config: DatabaseConfig) = { val host = config.dbPostgresServerName val port = config.dbPostgresPort val dbName = config.dbPostgresDbName val username = config.dbPostgresUsername val password = config.dbPostgresPassword JdbcConnectionString(postgresUrl(host, port, dbName), username, password) } private def createPostgresFromConfig(config: DatabaseConfig) = { val db = Database.forConfig("db.postgres", config.rootConfig) SqlDatabase(db, slick.driver.PostgresDriver, postgresConnectionString(config)) } private def createEmbedded(config: DatabaseConfig): SqlDatabase = { val db = Database.forConfig("db.h2") SqlDatabase(db, slick.driver.H2Driver, JdbcConnectionString(embeddedConnectionStringFromConfig(config))) } private def embeddedConnectionStringFromConfig(config: DatabaseConfig): String = { val url = config.dbH2Url val fullPath = url.split(":")(3) logger.info(s"Using an embedded database, with data files located at: $fullPath") url } } ================================================ FILE: ui/app/util/Zip.scala ================================================ package util import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.util.zip.{GZIPInputStream, GZIPOutputStream} object Zip { private val BufferSize = 512 def compress(string: String): Array[Byte] = { val os = new ByteArrayOutputStream(string.length() / 5) val gos = new GZIPOutputStream(os) gos.write(string.getBytes("UTF-8")) gos.close() os.close() os.toByteArray } def decompress(compressed: Array[Byte]): String = { val is = new ByteArrayInputStream(compressed) val gis = new GZIPInputStream(is, BufferSize) val string = new StringBuilder() val data = new Array[Byte](BufferSize) var bytesRead = gis.read(data) while (bytesRead != -1) { string.append(new String(data, 0, bytesRead, "UTF-8")) bytesRead = gis.read(data) } gis.close() is.close() string.toString() } } ================================================ FILE: ui/app/util/email/DummyEmailService.scala ================================================ package util.email import com.typesafe.scalalogging.StrictLogging import scala.collection.mutable.ListBuffer import scala.concurrent.Future class DummyEmailService extends EmailService with StrictLogging { private val sentEmails: ListBuffer[(String, String, String)] = ListBuffer() logger.info("Using dummy email service") def reset() { sentEmails.clear() } override def send(to: String, subject: String, body: String) = { this.synchronized { sentEmails.+=((to, subject, body)) } logger.info(s"Would send email to $to, with subject: $subject, body: $body") Future.successful(()) } def wasEmailSent(to: String, subject: String): Boolean = sentEmails.exists(email => email._1.contains(to) && email._2 == subject) } ================================================ FILE: ui/app/util/email/EmailService.scala ================================================ package util.email import scala.concurrent.Future trait EmailService { def send(to: String, subject: String, body: String): Future[Unit] } ================================================ FILE: ui/app/util/email/SendgridEmailService.scala ================================================ package util.email import com.sendgrid.SendGrid import com.typesafe.scalalogging.StrictLogging import scala.concurrent.Future import scala.util.Properties class SendgridEmailService(sendgridUsername: String, sendgridPassword: String, emailFrom: String) extends EmailService with StrictLogging { private lazy val sendgrid = new SendGrid(sendgridUsername, sendgridPassword) override def send(to: String, subject: String, body: String) = { val email = new SendGrid.Email() email.addTo(to) email.setFrom(emailFrom) email.setSubject(subject) email.setText(body) val response = sendgrid.send(email) if (response.getStatus) { logger.info(s"Email to $to sent") } else { logger.error( s"Email to $to, subject: $subject, body: $body, not sent: " + s"${response.getCode}/${response.getMessage}" ) } Future.successful(()) } } object SendgridEmailService extends StrictLogging { def createFromEnv(emailFrom: String): Option[SendgridEmailService] = for { u <- Properties.envOrNone("SENDGRID_USERNAME") p <- Properties.envOrNone("SENDGRID_PASSWORD") } yield { logger.info("Using SendGrid email service") new SendgridEmailService(u, p, emailFrom) } } ================================================ FILE: ui/app/views/index.scala.html ================================================ Scala Clippy
@playscalajs.html.scripts("uiclient") Fork me on GitHub
================================================ FILE: ui/app/views/use.scala.html ================================================

Scala Clippy Join the chat at https://gitter.im/softwaremill/scala-clippy

Did you ever see a Scala compiler error such as:

and had no idea what to do next? Well in this case, you need to provide an implicit instance of an ActorMaterializer, but the compiler isn't smart enough to be able to tell you that. Luckily, Scala Clippy is here to help!

Just add the Scala Clippy compiler plugin, and you'll see this additional helpful message, with optional additional colors:

Adding the plugin

The easiest to use Clippy is via an SBT plugin. If you'd like Clippy to be enabled for all projects, without the need to modify each project's build, add the following to ~/.sbt/0.13/plugins/build.sbt:

addSbtPlugin("com.softwaremill.clippy" % "plugin-sbt" % "0.6.1")

Upon first use, the plugin will download the advice dataset from https://scala-clippy.org and store it in the $HOME/.clippy directory. The dataset will be updated at most once a day, in the background. You can customize the dataset URL and local store by setting the clippyUrl and clippyLocalStoreDir sbt options to non-None-values.

Note: to customize a global sbt plugin (a plugin which is added via `~/.sbt/0.13/plugins/build.sbt`) keep in mind that:

  • customize the plugin settings in ~/.sbt/0.13/build.sbt (one directory up!). These settings will be automatically added to all of your projects.
  • you'll need to add import com.softwaremill.clippy.ClippySbtPlugin._ to access the setting names as auto-imports don't work in the global settings
  • the clippy sbt settings are just a convenient syntax for adding compiler options (e.g., enabling colors is same as scalacOptions += "-P:clippy:colors=true")

(NEW!) Turning selected compilation warnings into errors

From version 0.6.0 you can selectively define regexes for warnings which will be treated as fatal compilation errors. To do so with sbt, use the clippyFatalWarnings setting, for example:

    import com.softwaremill.clippy.ClippySbtPlugin.WarningPatterns._
    // ...
    settings(
      clippyFatalWarnings ++= List(
        NonExhaustiveMatch,
        ".*\\[wartremover:.*\\].*[\\s\\S]*"
      )
    )

You can also define fatal warnings in your .clippy.json file (see further sections for examples).

Enabling syntax and type mismatch diffs highlighting

Clippy can highlight:

  • in type mismatch errors, the diff between expected and actual types. This may be especially helpful for long type signatures
  • syntax when displaying code fragments with errors. Example:

If you'd like to enable this feature in sbt globally, add the following to `~/.sbt/0.13/build.sbt`: (see also notes above)

import com.softwaremill.clippy.ClippySbtPlugin._ // needed in global configuration only
clippyColorsEnabled := true

To customize the colors, set any of `clippyColorDiff`, `clippyColorComment`, `clippyColorType`, `clippyColorLiteral`, `clippyColorKeyword` to `Some(ClippyColor.[name])`, where `[name]` can be: `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `White` or `None`.

You can of course add clippy on a per-project basis as well.

Windows users

If you notice that colors don't get correctly reset, that's probably caused by problems with interpreting the standard ANSI Reset code. You can set `clippyColorReset` to a custom value like `LightGray` to solve this issue.

Contributing advice

Help others users by submitting an advice for a compilation error that you have encountered! Just click "contribute" above and paste in your error!

Alternatively, create an issue on GitHub or chat with us on Gitter in case of any doubts.

Speaking of GitHub, you are also welcome to check out (and improve!) the plugin's & website's source code.

Project-specific advices and fatal warnings

If you have advice that you feel is too specific to be worth sharing publicly, you can add it to your project specific advice file. First set your project root:

clippyProjectRoot := Some((baseDirectory in ThisBuild).value) 
Then create a file named .clippy.json in the root of your project directory and add the advice json in the format illustrated below:
{
  "version": "1",
  "advices": [
    {
      "error": {
        "type": "typeMismatch",
        "found": "scala\\.concurrent\\.Future\\[Int\\]",
        "required": "Int"
      },
      "text": "Maybe you used map where you should have used flatMap?",
      "library": {
        "groupId": "scala.lang",
        "artifactId": "Future",
        "version": "1.0"
      }
    }
  ],
  "fatalWarnings": [
    {
      "pattern": "match may not be exhaustive[\\s\\S]*",
      "text": "Additional optional text to be displayed"
    }
  ]
}

Library-specific advice

If you have advice that is specific to a library or library version you can also bundle the advice with your library. If your users have Scala-Clippy installed they will see your advice if your library is inclued in their project. This can be helpful in the common case where users of your library need specific imports to be able to use your functionality. To bundle clippy advice with your library just put it in a file named clippy.json in your resources directory.

Alternative ways to use Clippy

You can also use Clippy directly as a compiler plugin. If you use SBT, add the following setting to your project's .sbt file:

addCompilerPlugin("com.softwaremill.clippy" %% "plugin" % "0.6.1" classifier "bundle")

If you are using scalac directly, add the following option:

-Xplugin:clippy-plugin_2.11-0.6.1-bundle.jar

See clippy README for more details.

================================================ FILE: ui/conf/application.conf ================================================ # This is the main configuration file for the application. # ~~~~~ play.application.loader = "ClippyApplicationLoader" # Secret key # ~~~~~ # The secret key is used to secure cryptographics functions. # # This must be changed for production, but we recommend not changing it in this file. # # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. play.crypto.secret = "changeme" play.crypto.secret = ${?PLAY_CRYPTO_SECRET} # The application languages # ~~~~~ play.i18n.langs = [ "en" ] # Router # ~~~~~ # Define the Router object to use for this application. # This router will be looked up first when the application is starting up, # so make sure this is the entry point. # Furthermore, it's assumed your route file is named properly. # So for an application router like `my.application.Router`, # you may need to define a router file `conf/my.application.routes`. # Default to Routes in the root package (and conf/routes) # play.http.router = my.application.Routes db { h2 { dataSourceClass = "org.h2.jdbcx.JdbcDataSource" properties = { url = "jdbc:h2:file:./data/clippy" } } postgres { dataSourceClass = "org.postgresql.ds.PGSimpleDataSource" maxConnections = 16 properties = { serverName = "" portNumber = "5432" databaseName = "" user = "" password = "" } } } email.contact="clippy@scala-clippy.org" play.server.http.port=${?PORT} ================================================ FILE: ui/conf/db/migration/V1__create_schema.sql ================================================ create table "advices" ( "id" bigint not null primary key, "error_text_raw" varchar not null, "compilation_error" varchar not null, "advice" varchar not null, "state" smallint not null, "library_group_id" varchar not null, "library_artifact_id" varchar not null, "library_version" varchar not null, "contributor_email" varchar, "contributor_twitter" varchar, "contributor_github" varchar, "comment" varchar ); ================================================ FILE: ui/conf/db/migration/V2__add_pattern.sql ================================================ alter table "advices" add column "pattern_raw" varchar not null default ''; ================================================ FILE: ui/conf/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n%rEx ================================================ FILE: ui/conf/routes ================================================ # Routes # This file defines all application routes (Higher priority routes first) # ~~~~ # Home page GET / controllers.ApplicationController.index # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) # Webjars GET /webjars/*file controllers.WebJarAssets.at(file) GET /api/advices controllers.AdvicesController.get # Autowire calls POST /ui-api/*path controllers.AutowireController.autowireApi(path: String) ================================================ FILE: ui/test/dal/AdvicesRepositoryTest.scala ================================================ package dal import com.softwaremill.clippy._ import com.softwaremill.id.DefaultIdGenerator import util.BaseSqlSpec import org.scalatest.concurrent.ScalaFutures import org.scalatest.concurrent.IntegrationPatience class AdvicesRepositoryTest extends BaseSqlSpec with ScalaFutures with IntegrationPatience { it should "store & read an advice" in { // given val ar = new AdvicesRepository(database, new DefaultIdGenerator()) // when val stored = ar .store( "zzz", "yyy", TypeMismatchError[RegexT](RegexT("x"), None, RegexT("y"), None, None), "z", AdviceState.Pending, Library("g", "a", "1"), Contributor(None, None, Some("t")), Some("c") ) .futureValue // then val r = ar.findAll().futureValue r should have size (1) val found = r.head stored should be(found) found.errorTextRaw should be("zzz") found.patternRaw should be("yyy") found.compilationError should be(TypeMismatchError(RegexT("x"), None, RegexT("y"), None, None)) found.advice should be("z") found.state should be(AdviceState.Pending) found.library should be(Library("g", "a", "1")) found.contributor should be(Contributor(None, None, Some("t"))) found.comment should be(Some("c")) } } ================================================ FILE: ui/test/util/BaseSqlSpec.scala ================================================ package util import org.scalatest.concurrent.ScalaFutures import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FlatSpec, Matchers} import scala.concurrent.ExecutionContext trait BaseSqlSpec extends FlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with ScalaFutures { private val connectionString = "jdbc:h2:mem:clippy_test" + this.getClass.getSimpleName + ";DB_CLOSE_DELAY=-1" lazy val database = SqlDatabase.createEmbedded(connectionString) override protected def beforeAll() { super.beforeAll() createAll() } override protected def afterAll() { super.afterAll() dropAll() database.close() } private def dropAll() { import database.driver.api._ database.db.run(sqlu"DROP ALL OBJECTS").futureValue } private def createAll() { database.updateSchema() } override protected def afterEach() { try { dropAll() createAll() } catch { case e: Exception => e.printStackTrace() } super.afterEach() } implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/App.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ import autowire._ import japgolly.scalajs.react.vdom.prefix_<^._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.{Failure, Success} object App { sealed trait Page case object UsePage extends Page case object ContributeStep1InputError extends Page case class ContributeParseError(errorText: String) extends Page case class ContributeStep2EditPattern(errorTextRaw: String, ce: CompilationError[ExactT]) extends Page case class ContributeStep3SubmitAdvice( errorTextRaw: String, patternText: String, ceFromPattern: CompilationError[ExactT] ) extends Page case object ListingPage extends Page case object FeedbackPage extends Page case class State(page: Page, errorMsgs: List[String], infoMsgs: List[String]) class Backend($ : BackendScope[Unit, State]) { private val handleReset: Callback = clearMsgs >> $.modState(_.copy(page = ContributeStep1InputError)) private def handleErrorTextSubmitted(errorText: String): Callback = CompilationErrorParser.parse(errorText) match { case None => clearMsgs >> $.modState(_.copy(page = ContributeParseError(errorText))) case Some(ce) => clearMsgs >> $.modState(_.copy(page = ContributeStep2EditPattern(errorText, ce))) } private def handleSendParseError(errorText: String)(email: String): Callback = handleFuture( AutowireClient[UiApi].sendCannotParse(errorText, email).call(), Some("Error submitted successfully! We'll get in touch soon."), Some((_: Unit) => $.modState(s => s.copy(page = ContributeStep1InputError))) ) private def handlePatternSubmitted( errorTextRaw: String, patternText: String, ceFromPattern: CompilationError[ExactT] ): Callback = $.modState(s => s.copy(page = ContributeStep3SubmitAdvice(errorTextRaw, patternText, ceFromPattern))) private def handleSendAdviceProposal(ap: AdviceProposal): Callback = handleFuture( AutowireClient[UiApi].sendAdviceProposal(ap).call(), Some("Advice submitted successfully!"), Some((_: Unit) => $.modState(s => s.copy(page = ContributeStep1InputError))) ) private lazy val handleFuture = new HandleFuture { override def apply[T](f: Future[T], successMsg: Option[String], successCallback: Option[(T) => Callback]) = CallbackTo(f onComplete { case Success(v) => val msgCallback = successMsg map handleShowInfo val vCallback = successCallback map (_(v)) (msgCallback.getOrElse(Callback.empty) >> vCallback.getOrElse(Callback.empty)).runNow() case Failure(e) => handleShowError("Error communicating with the server").runNow() }) } private def handleShowError(error: String): Callback = clearMsgs >> $.modState(s => s.copy(errorMsgs = error :: s.errorMsgs)) private def handleShowInfo(info: String): Callback = clearMsgs >> $.modState(s => s.copy(infoMsgs = info :: s.infoMsgs)) private def clearMsgs = $.modState(_.copy(errorMsgs = Nil, infoMsgs = Nil)) private def handleSwitchPage(newPage: Page): Callback = clearMsgs >> $.modState { s => def isContribute(p: Page) = p != UsePage && p != ListingPage && p != FeedbackPage if (s.page == newPage || (isContribute(s.page) && isContribute(newPage))) s else s.copy(page = newPage) } private def showMsgs(s: State) = <.span( s.infoMsgs.map(m => <.div(^.cls := "alert alert-success", ^.role := "alert")(m)) ++ s.errorMsgs.map(m => <.div(^.cls := "alert alert-danger", ^.role := "alert")(m)): _* ) private def showPage(s: State) = s.page match { case UsePage => Use.component() case ContributeStep1InputError => Contribute.Step1InputError.component( Contribute.Step1InputError.Props(handleErrorTextSubmitted, handleShowError) ) case ContributeParseError(et) => Contribute.ParseError.component( Contribute.ParseError.Props(handleReset, handleSendParseError(et), handleShowError) ) case ContributeStep2EditPattern(errorTextRaw, ce) => Contribute.Step2EditPattern.component( Contribute.Step2EditPattern.Props(errorTextRaw, ce, handleReset, handlePatternSubmitted, handleShowError) ) case ContributeStep3SubmitAdvice(errorTextRaw, pattern, ceFromPattern) => Contribute.Step3SubmitAdvice.component( Contribute.Step3SubmitAdvice .Props(errorTextRaw, pattern, ceFromPattern, handleReset, handleSendAdviceProposal, handleShowError) ) case ListingPage => Listing.component(Listing.Props(handleShowError, clearMsgs, handleFuture)) case FeedbackPage => Feedback.component(Feedback.Props(handleShowError, clearMsgs, handleFuture)) } def render(s: State) = <.span( Menu.component((s.page, handleSwitchPage)), <.div(^.cls := "container")( showMsgs(s), showPage(s) ) ) } } trait HandleFuture { def apply[T](f: Future[T], successMsg: Option[String], successCallback: Option[T => Callback]): Callback } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/AutowireClient.scala ================================================ package com.softwaremill.clippy import org.scalajs.dom import scala.concurrent.Future import scalajs.concurrent.JSExecutionContext.Implicits.queue import upickle.default._ import upickle.Js object AutowireClient extends autowire.Client[Js.Value, Reader, Writer] { override def doCall(req: Request): Future[Js.Value] = dom.ext.Ajax .post( url = "/ui-api/" + req.path.mkString("/"), data = upickle.json.write(Js.Obj(req.args.toSeq: _*)) ) .map(_.responseText) .map(upickle.json.read) def read[Result: Reader](p: Js.Value) = readJs[Result](p) def write[Result: Writer](r: Result) = writeJs(r) } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/BsUtils.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ import japgolly.scalajs.react.extra.ExternalVar import japgolly.scalajs.react.vdom.prefix_<^._ object BsUtils { def bsPanel(body: TagMod*) = <.div(^.cls := "panel panel-default") { <.div(^.cls := "panel-body")(body) } // Maybe this could be a component? But a function works for now def bsFormEl(ev: ExternalVar[FormField])(body: Seq[TagMod] => ReactTag) = { val formField = ev.value val elId = Utils.randomString(8) <.div(^.cls := "form-group", formField.error ?= (^.cls := "has-error"))( <.label(^.htmlFor := elId, if (formField.required) <.strong(formField.label) else formField.label), body( Seq( ^.id := elId, formField.required ?= (^.required := "required"), ^.value := formField.v, ^.onChange ==> ((e: ReactEventI) => ev.set(formField.copy(v = e.target.value))) ) ) ) } } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Contribute.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.prefix_<^._ import BsUtils._ import Utils._ import monocle.macros.Lenses object Contribute { object Step1InputError { case class Props(next: String => Callback, showError: String => Callback) @Lenses case class State(errorText: FormField) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy(errorText = s.errorText.validated) override def fields(s: State) = List(s.errorText) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = <.div( bsPanel( <.p( "Scala Clippy is only as good as its advice database. Help other users by submitting an advice for a compilation error that you have encountered!" ), <.p("First, paste in the error and we'll see if we can parse it. Only the error message is needed, e.g.:"), <.pre( """[error] type mismatch; |[error] found : akka.http.scaladsl.server.StandardRoute |[error] required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin ), <.p("You will be able to edit the pattern that will be used when matching errors later.") ), <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)(s => p.next(s.errorText.v)), bsFormEl(externalVar($, s, State.errorText))(mods => <.textarea(^.cls := "form-control", ^.rows := 5)(mods)), <.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Next") ) ) } val component = ReactComponentB[Props]("Step1InputError") .initialState(State(FormField("Error text", required = true))) .renderBackend[Backend] .build } object Step2EditPattern { case class Props( errorTextRaw: String, ce: CompilationError[ExactT], reset: Callback, next: (String, String, CompilationError[ExactT]) => Callback, showError: String => Callback ) @Lenses case class State(patternText: FormField) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy(patternText = s.patternText.validated) override def fields(s: State) = List(s.patternText) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = { val parsedPattern = CompilationErrorParser.parse(s.patternText.v) val errorMatchesPattern = parsedPattern.map(_.asRegex).map(_.matches(p.ce)) val alert = errorMatchesPattern match { case None => <.div(^.cls := "alert alert-danger", ^.role := "alert")("Cannot parse the error") case Some(false) => <.div(^.cls := "alert alert-danger", ^.role := "alert")("The pattern doesn't match the original error") case Some(true) => <.div(^.cls := "alert alert-success", ^.role := "alert")("The pattern matches the original error") } val canProceed = errorMatchesPattern.getOrElse(false) val nextCallback = parsedPattern match { case None => Callback.empty case Some(ceFromPattern) => p.next(p.errorTextRaw, s.patternText.v, ceFromPattern) } <.div( bsPanel( <.p("Parsing successfull! Here's what we've found:"), <.pre(p.ce.toString), <.p( "You can now create a pattern for matching errors using a wildcard character: *. For example, you can generalize a pattern by replacing concrete type parameters with *:" ), <.pre("cats.Monad[List] -> cats.Monad[*]"), <.p("Or, you can just click next and leave the submitted error to be an exact pattern.") ), alert, <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)(_ => nextCallback), bsFormEl(externalVar($, s, State.patternText))( mods => <.textarea(^.cls := "form-control", ^.rows := 5)(mods) ), <.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"), <.span(" "), <.button(^.`type` := "submit", ^.cls := "btn btn-primary", ^.disabled := !canProceed)("Next") ) ) } } val component = ReactComponentB[Props]("Step2EditPattern") .initialState_P { p => State(FormField("Pattern", required = true, p.errorTextRaw, error = false)) } .renderBackend[Backend] .build } object Step3SubmitAdvice { case class Props( errorTextRaw: String, patternRaw: String, ceFromPattern: CompilationError[ExactT], reset: Callback, send: AdviceProposal => Callback, showError: String => Callback ) @Lenses case class State( advice: FormField, libraryGroupId: FormField, libraryArtifactId: FormField, libraryVersion: FormField, email: FormField, twitter: FormField, github: FormField, comment: FormField ) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy( advice = s.advice.validated, libraryGroupId = s.libraryGroupId.validated, libraryArtifactId = s.libraryArtifactId.validated, libraryVersion = s.libraryVersion.validated, email = s.email.validated, twitter = s.twitter.validated, github = s.github.validated, comment = s.comment.validated ) override def fields(s: State) = List( s.advice, s.libraryGroupId, s.libraryArtifactId, s.libraryVersion, s.email, s.twitter, s.github, s.comment ) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = <.div( bsPanel( <.p("You can now define the advice associated with the error. Here's the pattern you have entered:"), <.pre(p.ceFromPattern.toString), <.p( "You can optionally leave an e-mail so that we can let you know when your proposal is accepted, and a twitter/github handle so that we can add proper attribution, visible in the advice browser." ) ), <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)( s => p.send( AdviceProposal( p.errorTextRaw, p.patternRaw, p.ceFromPattern.asRegex, s.advice.v, Library(s.libraryGroupId.v, s.libraryArtifactId.v, s.libraryVersion.v), Contributor(s.email.vOpt, s.twitter.vOpt, s.github.vOpt), s.comment.vOpt ) ) ), bsFormEl(externalVar($, s, State.advice))(mods => <.textarea(^.cls := "form-control", ^.rows := 3)(mods)), <.hr, bsFormEl(externalVar($, s, State.libraryGroupId))( mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "org.scala")(mods) ), bsFormEl(externalVar($, s, State.libraryArtifactId))( mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "scala-lang")(mods) ), bsFormEl(externalVar($, s, State.libraryVersion))( mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "2.11-M3")(mods) ), <.hr, bsFormEl(externalVar($, s, State.email))( mods => <.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods) ), bsFormEl(externalVar($, s, State.twitter))( mods => <.div(^.cls := "input-group")( <.div(^.cls := "input-group-addon")("@"), <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "twitter")(mods) ) ), bsFormEl(externalVar($, s, State.github))( mods => <.div(^.cls := "input-group")( <.div(^.cls := "input-group-addon")("@"), <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "github")(mods) ) ), <.hr, bsFormEl(externalVar($, s, State.comment))(mods => <.textarea(^.cls := "form-control", ^.rows := 3)(mods)), <.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"), <.span(" "), <.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Send") ) ) } val component = ReactComponentB[Props]("Step3SubmitAdvice") .initialState( State( FormField("Advice", required = true), FormField("Library group id", required = true), FormField("Library artifact id", required = true), FormField("Library version", required = true), FormField("E-mail (optional)", required = false), FormField("Twitter handle (optional)", required = false), FormField("Github handle (optional)", required = false), FormField("Comment (optional)", required = false) ) ) .renderBackend[Backend] .build } object ParseError { case class Props(reset: Callback, send: String => Callback, showError: String => Callback) @Lenses case class State(email: FormField) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy(email = s.email.validated) override def fields(s: State) = List(s.email) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = <.div( bsPanel( <.p( "Unfortunately we cannot parse the error. Let us know how to contact you, we'll try to find out what's wrong and get back to you." ) ), <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)(s => p.send(s.email.v)), bsFormEl(externalVar($, s, State.email))( mods => <.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods) ), <.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"), <.span(" "), <.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Send") ) ) } val component = ReactComponentB[Props]("ParseError") .initialState(State(FormField("Email", required = true))) .renderBackend[Backend] .build } } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Feedback.scala ================================================ package com.softwaremill.clippy import autowire._ import com.softwaremill.clippy.BsUtils._ import com.softwaremill.clippy.Utils._ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.prefix_<^._ import monocle.macros.Lenses import scala.concurrent.ExecutionContext.Implicits.global object Feedback { case class Props(showError: String => Callback, clearMsgs: Callback, handleFuture: HandleFuture) @Lenses case class State(contact: FormField, feedback: FormField) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy( contact = s.contact.validated, feedback = s.feedback.validated ) override def fields(s: State) = List(s.contact, s.feedback) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = { def sendFeedbackCallback() = p.handleFuture( AutowireClient[UiApi].feedback(s.feedback.v, s.contact.v).call(), Some("Feedback sent, thank you!"), Some( (_: Unit) => $.modState(s => s.copy(contact = s.contact.copy(v = ""), feedback = s.feedback.copy(v = ""))) ) ) <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)(s => sendFeedbackCallback()), bsFormEl(externalVar($, s, State.contact))( mods => <.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods) ), bsFormEl(externalVar($, s, State.feedback))(mods => <.textarea(^.cls := "form-control", ^.rows := "3")(mods)), <.button(^.`type` := "send", ^.cls := "btn btn-primary")("Send") ) } } val component = ReactComponentB[Props]("Use") .initialState( State( FormField("Contact email", required = true), FormField("Feedback", required = true) ) ) .renderBackend[Backend] .build } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/FormField.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ case class FormField(label: String, required: Boolean, v: String, error: Boolean) { def validated = if (required) { if (v.isEmpty) copy(error = true) else copy(error = false) } else this def vOpt: Option[String] = if (v.isEmpty) None else Some(v) } object FormField { def apply(label: String, required: Boolean): FormField = FormField(label, required, "", error = false) def errorMsgIfAny(fields: Seq[FormField]): Option[String] = fields.find(_.error).map(ff => s"${ff.label} is required") // required is the only type of error there could be def submitValidated[P, S: Validatable]( $ : BackendScope[P, S], showError: String => Callback )(submit: S => Callback)(e: ReactEventI): Callback = for { _ <- e.preventDefaultCB props <- $.props s <- $.state v = implicitly[Validatable[S]] s2 = v.validated(s) _ <- $.setState(s2) fields = v.fields(s2) em = errorMsgIfAny(fields) _ <- em.fold(submit(s2))(showError) } yield () } trait Validatable[S] { def validated(s: S): S def fields(s: S): Seq[FormField] } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Listing.scala ================================================ package com.softwaremill.clippy import com.softwaremill.clippy.BsUtils._ import com.softwaremill.clippy.Utils._ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.prefix_<^._ import autowire._ import monocle.macros.Lenses import scala.concurrent.ExecutionContext.Implicits.global object Listing { case class Props(showError: String => Callback, clearMsgs: Callback, handleFuture: HandleFuture) @Lenses case class State( advices: Seq[AdviceListing], suggestEditId: Option[Long], suggestContact: FormField, suggestText: FormField ) implicit val stateVal = new Validatable[State] { override def validated(s: State) = s.copy( suggestContact = s.suggestContact.validated, suggestText = s.suggestText.validated ) override def fields(s: State) = List(s.suggestContact, s.suggestText) } class Backend($ : BackendScope[Props, State]) { def render(s: State, p: Props) = { def suggestEditCallback(a: AdviceListing) = p.clearMsgs >> $.modState(s => s.copy(suggestEditId = Some(a.id), suggestText = s.suggestText.copy(v = ""))) def cancelSuggestEditCallback(a: AdviceListing) = $.modState(_.copy(suggestEditId = None)) def sendSuggestEditCallback(a: AdviceListing) = cancelSuggestEditCallback(a) >> p.handleFuture( AutowireClient[UiApi].sendSuggestEdit(s.suggestText.v, s.suggestContact.v, a).call(), Some("Suggestion sent, thank you!"), None ) def rowForAdvice(a: AdviceListing) = <.tr( <.td(<.pre(a.compilationError.toString)), <.td(a.advice), <.td(a.library.toString), <.td(<.span(^.cls := "glyphicon glyphicon-edit", ^.onClick --> suggestEditCallback(a))) ) def suggestEdit(a: AdviceListing) = <.tr( <.td(^.colSpan := 4)( <.form( ^.onSubmit ==> FormField.submitValidated($, p.showError)(s => sendSuggestEditCallback(a)), bsFormEl(externalVar($, s, State.suggestContact))( mods => <.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods) ), bsFormEl(externalVar($, s, State.suggestText))( mods => <.textarea(^.cls := "form-control", ^.rows := "3")(mods) ), <.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> cancelSuggestEditCallback(a))( "Cancel" ), <.span(" "), <.button(^.`type` := "send", ^.cls := "btn btn-primary")("Send") ) ) ) <.table(^.cls := "table table-striped advice-listing")( <.thead( <.tr( <.th("Compilation error (as regex)"), <.th(^.width := "25%")("Advice"), <.th(^.width := "20%")("Library"), <.th(^.width := "8%")("Suggest edit") ) ), <.tbody( s.advices.flatMap( a => List(rowForAdvice(a)) ++ s.suggestEditId.filter(_ == a.id).map(_ => suggestEdit(a)).toList ): _* ) ) } def initAdvices(p: Props): Callback = p.handleFuture( AutowireClient[UiApi].listAccepted().call(), None, Some((s: Seq[AdviceListing]) => $.modState(_.copy(advices = s))) ) } val component = ReactComponentB[Props]("Use") .initialState( State( Nil, None, FormField("Contact email (optional)", required = false), FormField("Suggestion", required = true) ) ) .renderBackend[Backend] .componentDidMount(ctx => ctx.backend.initAdvices(ctx.props)) .build } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Main.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ import org.scalajs.jquery._ import scala.scalajs.js import japgolly.scalajs.react.vdom.prefix_<^._ object Main extends js.JSApp { type HtmlId = String def main(): Unit = jQuery(setupUI _) def setupUI(): Unit = { val mountNode = org.scalajs.dom.document.getElementById("reactmain") val app = ReactComponentB[Unit]("App") .initialState(App.State(App.UsePage, Nil, Nil)) .renderBackend[App.Backend] .build ReactDOM.render(app(), mountNode) } } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Menu.scala ================================================ package com.softwaremill.clippy import com.softwaremill.clippy.App._ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.prefix_<^._ object Menu { val component = ReactComponentB[(Page, Page => Callback)]("Menu").render { $ => val isUsePage = $.props._1 == UsePage val isListingPage = $.props._1 == ListingPage val isFeedbackPage = $.props._1 == FeedbackPage val isContributePage = !isUsePage && !isListingPage && !isFeedbackPage def switchTo(p: Page)(e: ReactEventI) = e.preventDefaultCB >> $.props._2(p) <.nav(^.cls := "navbar navbar-inverse navbar-fixed-top")( <.div(^.cls := "container")( <.div(^.cls := "navbar-header")( <.button( ^.`type` := "button", ^.cls := "navbar-toggle collapsed", "data-toggle".reactAttr := "collapse", "data-target".reactAttr := "navbar", "aria-expanded".reactAttr := "false", "aria-controls".reactAttr := "navbar" )( <.span(^.cls := "sr-only")("Toggle navigation"), <.span(^.cls := "icon-bar"), <.span(^.cls := "icon-bar"), <.span(^.cls := "icon-bar") ), <.a(^.cls := "navbar-brand", ^.onClick ==> switchTo(UsePage), ^.href := "#")("Scala Clippy") ), <.div(^.id := "navbar", ^.cls := "collapse navbar-collapse")( <.ul(^.cls := "nav navbar-nav")( <.li(isUsePage ?= (^.cls := "active"))( <.a("Use", ^.onClick ==> switchTo(UsePage), ^.href := "#") ), <.li(isContributePage ?= (^.cls := "active"))( <.a("Contribute", ^.onClick ==> switchTo(ContributeStep1InputError), ^.href := "#") ), <.li(isListingPage ?= (^.cls := "active"))( <.a("Browse", ^.onClick ==> switchTo(ListingPage), ^.href := "#") ), <.li(isFeedbackPage ?= (^.cls := "active"))( <.a("Send feedback", ^.onClick ==> switchTo(FeedbackPage), ^.href := "#") ) ) ) ) ) }.build } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Use.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.prefix_<^._ object Use { val component = ReactComponentB[Unit]("Use").render { $ => val html = org.scalajs.dom.document.getElementById("use").innerHTML <.span(^.dangerouslySetInnerHtml(html)) }.build } ================================================ FILE: ui-client/src/main/scala/com/softwaremill/clippy/Utils.scala ================================================ package com.softwaremill.clippy import japgolly.scalajs.react.BackendScope import japgolly.scalajs.react.extra.ExternalVar import monocle._ import scala.util.Random object Utils { def randomString(length: Int) = Random.alphanumeric take length mkString "" // ExternalVar companion has only methods for creating a var from AccessRD (read direct), here we are reading // through callbacks, so we need that extra method def externalVar[S, A]($ : BackendScope[_, S], s: S, l: Lens[S, A]): ExternalVar[A] = ExternalVar(l.get(s))(a => $.modState(l.set(a))) } ================================================ FILE: ui-shared/src/main/scala/com/softwaremill/clippy/AdviceState.scala ================================================ package com.softwaremill.clippy object AdviceState extends Enumeration { type AdviceState = Value val Pending, Accepted, Rejected = Value } ================================================ FILE: ui-shared/src/main/scala/com/softwaremill/clippy/Contributor.scala ================================================ package com.softwaremill.clippy case class Contributor(email: Option[String], twitter: Option[String], github: Option[String]) ================================================ FILE: ui-shared/src/main/scala/com/softwaremill/clippy/UiApi.scala ================================================ package com.softwaremill.clippy import scala.concurrent.Future trait UiApi extends ContributeApi with ListingApi with FeedbackApi trait FeedbackApi { def feedback(text: String, contactEmail: String): Future[Unit] } trait ContributeApi { def sendCannotParse(errorText: String, contributorEmail: String): Future[Unit] def sendAdviceProposal(adviceProposal: AdviceProposal): Future[Unit] } trait ListingApi { def listAccepted(): Future[Seq[AdviceListing]] def sendSuggestEdit(text: String, contactEmail: String, adviceListing: AdviceListing): Future[Unit] } case class AdviceProposal( errorTextRaw: String, patternRaw: String, compilationError: CompilationError[RegexT], advice: String, library: Library, contributor: Contributor, comment: Option[String] ) case class ContributorListing(twitter: Option[String], github: Option[String]) case class AdviceListing( id: Long, compilationError: CompilationError[RegexT], advice: String, library: Library, contributor: ContributorListing )