├── .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 | [](https://gitter.im/softwaremill/scala-clippy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/softwaremill/scala-clippy)
5 | [](https://app.updateimpact.com/latest/634276070333485056/clippy)
6 | [](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 | 
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 |
@use()
31 |
32 |
33 |
34 | @playscalajs.html.scripts("uiclient")
35 |
36 |
37 |
38 |
45 |
46 |
47 |
63 |
64 |
65 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ui/app/views/use.scala.html:
--------------------------------------------------------------------------------
1 |
2 | Scala Clippy
3 |
4 |
5 |
6 |
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 |
--------------------------------------------------------------------------------