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

2 | Scala Clippy 3 | 4 | 5 | 6 | Join the chat at https://gitter.im/softwaremill/scala-clippy 7 | 8 |

9 | 10 |

11 | Did you ever see a Scala compiler error such as: 12 |

13 | 14 |

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

18 | 19 |

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

22 | 23 | 24 |

Adding the plugin

25 | 26 |

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

30 | 31 |
 32 | addSbtPlugin("com.softwaremill.clippy" % "plugin-sbt" % "0.6.1")
 33 | 
34 | 35 |

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

40 | 41 |

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

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

(NEW!) Turning selected compilation warnings into errors

56 |

57 | 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, 58 | use the clippyFatalWarnings setting, for example: 59 |

60 |
 61 |     import com.softwaremill.clippy.ClippySbtPlugin.WarningPatterns._
 62 |     // ...
 63 |     settings(
 64 |       clippyFatalWarnings ++= List(
 65 |         NonExhaustiveMatch,
 66 |         ".*\\[wartremover:.*\\].*[\\s\\S]*"
 67 |       )
 68 |     )
 69 | 
70 |

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

73 | 74 |

Enabling syntax and type mismatch diffs highlighting

75 | 76 |

77 | Clippy can highlight: 78 |

79 | 80 |
    81 |
  • 82 | in type mismatch errors, the diff between expected and actual types. This may be especially helpful for long type 83 | signatures 84 |
  • 85 |
  • 86 | syntax when displaying code fragments with errors. Example: 87 | 88 |
  • 89 |
90 | 91 |

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

95 | 96 |
 97 | import com.softwaremill.clippy.ClippySbtPlugin._ // needed in global configuration only
 98 | clippyColorsEnabled := true
 99 | 
100 | 101 |

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

106 | 107 |

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

110 |

Windows users

111 |

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.

112 |

Contributing advice

113 | 114 |

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

118 | 119 |

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

123 | 124 |

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

128 | 129 |

Project-specific advices and fatal warnings

130 | 131 |

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

134 |
135 | clippyProjectRoot := Some((baseDirectory in ThisBuild).value) 
136 | 
137 | 138 | Then create a file named .clippy.json in the root of your project directory and add the advice json in the format illustrated below: 139 | 140 |
141 | {
142 |   "version": "1",
143 |   "advices": [
144 |     {
145 |       "error": {
146 |         "type": "typeMismatch",
147 |         "found": "scala\\.concurrent\\.Future\\[Int\\]",
148 |         "required": "Int"
149 |       },
150 |       "text": "Maybe you used map where you should have used flatMap?",
151 |       "library": {
152 |         "groupId": "scala.lang",
153 |         "artifactId": "Future",
154 |         "version": "1.0"
155 |       }
156 |     }
157 |   ],
158 |   "fatalWarnings": [
159 |     {
160 |       "pattern": "match may not be exhaustive[\\s\\S]*",
161 |       "text": "Additional optional text to be displayed"
162 |     }
163 |   ]
164 | }
165 | 
166 |

Library-specific advice

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

Alternative ways to use Clippy

173 | 174 |

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

178 | 179 |
180 | addCompilerPlugin("com.softwaremill.clippy" %% "plugin" % "0.6.1" classifier "bundle")
181 | 
182 | 183 |

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

186 | 187 |
188 | -Xplugin:clippy-plugin_2.11-0.6.1-bundle.jar
189 | 
190 | 191 |

192 | See clippy README for more details.

193 |

194 | -------------------------------------------------------------------------------- /ui/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | play.application.loader = "ClippyApplicationLoader" 5 | 6 | # Secret key 7 | # ~~~~~ 8 | # The secret key is used to secure cryptographics functions. 9 | # 10 | # This must be changed for production, but we recommend not changing it in this file. 11 | # 12 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 13 | play.crypto.secret = "changeme" 14 | play.crypto.secret = ${?PLAY_CRYPTO_SECRET} 15 | 16 | # The application languages 17 | # ~~~~~ 18 | play.i18n.langs = [ "en" ] 19 | 20 | # Router 21 | # ~~~~~ 22 | # Define the Router object to use for this application. 23 | # This router will be looked up first when the application is starting up, 24 | # so make sure this is the entry point. 25 | # Furthermore, it's assumed your route file is named properly. 26 | # So for an application router like `my.application.Router`, 27 | # you may need to define a router file `conf/my.application.routes`. 28 | # Default to Routes in the root package (and conf/routes) 29 | # play.http.router = my.application.Routes 30 | 31 | db { 32 | h2 { 33 | dataSourceClass = "org.h2.jdbcx.JdbcDataSource" 34 | properties = { 35 | url = "jdbc:h2:file:./data/clippy" 36 | } 37 | } 38 | postgres { 39 | dataSourceClass = "org.postgresql.ds.PGSimpleDataSource" 40 | maxConnections = 16 41 | properties = { 42 | serverName = "" 43 | portNumber = "5432" 44 | databaseName = "" 45 | user = "" 46 | password = "" 47 | } 48 | } 49 | } 50 | 51 | email.contact="clippy@scala-clippy.org" 52 | 53 | play.server.http.port=${?PORT} -------------------------------------------------------------------------------- /ui/conf/db/migration/V1__create_schema.sql: -------------------------------------------------------------------------------- 1 | create table "advices" ( 2 | "id" bigint not null primary key, 3 | "error_text_raw" varchar not null, 4 | "compilation_error" varchar not null, 5 | "advice" varchar not null, 6 | "state" smallint not null, 7 | "library_group_id" varchar not null, 8 | "library_artifact_id" varchar not null, 9 | "library_version" varchar not null, 10 | "contributor_email" varchar, 11 | "contributor_twitter" varchar, 12 | "contributor_github" varchar, 13 | "comment" varchar 14 | ); 15 | -------------------------------------------------------------------------------- /ui/conf/db/migration/V2__add_pattern.sql: -------------------------------------------------------------------------------- 1 | alter table "advices" add column "pattern_raw" varchar not null default ''; 2 | -------------------------------------------------------------------------------- /ui/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n%rEx 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ui/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.ApplicationController.index 7 | 8 | # Map static resources from the /public folder to the /assets URL path 9 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 10 | 11 | # Webjars 12 | GET /webjars/*file controllers.WebJarAssets.at(file) 13 | 14 | GET /api/advices controllers.AdvicesController.get 15 | 16 | # Autowire calls 17 | POST /ui-api/*path controllers.AutowireController.autowireApi(path: String) -------------------------------------------------------------------------------- /ui/test/dal/AdvicesRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import com.softwaremill.clippy._ 4 | import com.softwaremill.id.DefaultIdGenerator 5 | import util.BaseSqlSpec 6 | import org.scalatest.concurrent.ScalaFutures 7 | import org.scalatest.concurrent.IntegrationPatience 8 | 9 | class AdvicesRepositoryTest extends BaseSqlSpec with ScalaFutures with IntegrationPatience { 10 | it should "store & read an advice" in { 11 | // given 12 | val ar = new AdvicesRepository(database, new DefaultIdGenerator()) 13 | 14 | // when 15 | val stored = ar 16 | .store( 17 | "zzz", 18 | "yyy", 19 | TypeMismatchError[RegexT](RegexT("x"), None, RegexT("y"), None, None), 20 | "z", 21 | AdviceState.Pending, 22 | Library("g", "a", "1"), 23 | Contributor(None, None, Some("t")), 24 | Some("c") 25 | ) 26 | .futureValue 27 | 28 | // then 29 | val r = ar.findAll().futureValue 30 | r should have size (1) 31 | 32 | val found = r.head 33 | 34 | stored should be(found) 35 | found.errorTextRaw should be("zzz") 36 | found.patternRaw should be("yyy") 37 | found.compilationError should be(TypeMismatchError(RegexT("x"), None, RegexT("y"), None, None)) 38 | found.advice should be("z") 39 | found.state should be(AdviceState.Pending) 40 | found.library should be(Library("g", "a", "1")) 41 | found.contributor should be(Contributor(None, None, Some("t"))) 42 | found.comment should be(Some("c")) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/test/util/BaseSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FlatSpec, Matchers} 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | trait BaseSqlSpec extends FlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with ScalaFutures { 9 | 10 | private val connectionString = "jdbc:h2:mem:clippy_test" + this.getClass.getSimpleName + ";DB_CLOSE_DELAY=-1" 11 | 12 | lazy val database = SqlDatabase.createEmbedded(connectionString) 13 | 14 | override protected def beforeAll() { 15 | super.beforeAll() 16 | createAll() 17 | } 18 | 19 | override protected def afterAll() { 20 | super.afterAll() 21 | dropAll() 22 | database.close() 23 | } 24 | 25 | private def dropAll() { 26 | import database.driver.api._ 27 | database.db.run(sqlu"DROP ALL OBJECTS").futureValue 28 | } 29 | 30 | private def createAll() { 31 | database.updateSchema() 32 | } 33 | 34 | override protected def afterEach() { 35 | try { 36 | dropAll() 37 | createAll() 38 | } catch { 39 | case e: Exception => e.printStackTrace() 40 | } 41 | 42 | super.afterEach() 43 | } 44 | 45 | implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global 46 | } 47 | --------------------------------------------------------------------------------