Showing preview only (208K chars total). Download the full file or copy to clipboard to get everything.
Repository: softwaremill/scala-clippy
Branch: master
Commit: 6829f56c1042
Files: 77
Total size: 185.5 KB
Directory structure:
gitextract_gwq_0d06/
├── .gitignore
├── .scalafmt.conf
├── .travis.yml
├── LICENSE.txt
├── README.md
├── build.sbt
├── model/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── Advice.scala
│ │ ├── Clippy.scala
│ │ ├── CompilationError.scala
│ │ ├── CompilationErrorParser.scala
│ │ ├── Library.scala
│ │ ├── StringDiff.scala
│ │ ├── Template.scala
│ │ └── Warning.scala
│ └── test/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── CompilationErrorParserTest.scala
│ ├── CompilationErrorTest.scala
│ ├── LibraryProperties.scala
│ ├── RegexTTest.scala
│ ├── StringDiffSpecification.scala
│ ├── StringDiffTest.scala
│ └── TypeNamesGenerators.scala
├── package.json
├── plugin/
│ └── src/
│ └── main/
│ ├── resources/
│ │ └── scalac-plugin.xml
│ ├── scala/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── AdviceLoader.scala
│ │ ├── ClippyPlugin.scala
│ │ ├── ColorsConfig.scala
│ │ ├── FailOnWarningsReporter.scala
│ │ ├── Highlighter.scala
│ │ ├── InjectReporter.scala
│ │ ├── RestoreReporter.scala
│ │ └── Utils.scala
│ ├── scala-2.11/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── DelegatingPosition.scala
│ │ └── DelegatingReporter.scala
│ └── scala-2.12/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── DelegatingPosition.scala
│ └── DelegatingReporter.scala
├── plugin-sbt/
│ └── src/
│ └── main/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ └── ClippySbtPlugin.scala
├── project/
│ ├── build.properties
│ └── plugins.sbt
├── tests/
│ └── src/
│ └── test/
│ └── scala/
│ └── org/
│ └── softwaremill/
│ └── clippy/
│ └── CompileTests.scala
├── ui/
│ ├── app/
│ │ ├── ClippyApplicationLoader.scala
│ │ ├── api/
│ │ │ └── UiApiImpl.scala
│ │ ├── assets/
│ │ │ └── stylesheets/
│ │ │ └── main.less
│ │ ├── controllers/
│ │ │ ├── AdvicesController.scala
│ │ │ ├── ApplicationController.scala
│ │ │ ├── AutowireController.scala
│ │ │ └── HttpsFilter.scala
│ │ ├── dal/
│ │ │ └── AdvicesRepository.scala
│ │ ├── util/
│ │ │ ├── ConfigWithDefault.scala
│ │ │ ├── DatabaseConfig.scala
│ │ │ ├── SqlDatabase.scala
│ │ │ ├── Zip.scala
│ │ │ └── email/
│ │ │ ├── DummyEmailService.scala
│ │ │ ├── EmailService.scala
│ │ │ └── SendgridEmailService.scala
│ │ └── views/
│ │ ├── index.scala.html
│ │ └── use.scala.html
│ ├── conf/
│ │ ├── application.conf
│ │ ├── db/
│ │ │ └── migration/
│ │ │ ├── V1__create_schema.sql
│ │ │ └── V2__add_pattern.sql
│ │ ├── logback.xml
│ │ └── routes
│ └── test/
│ ├── dal/
│ │ └── AdvicesRepositoryTest.scala
│ └── util/
│ └── BaseSqlSpec.scala
├── ui-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── App.scala
│ ├── AutowireClient.scala
│ ├── BsUtils.scala
│ ├── Contribute.scala
│ ├── Feedback.scala
│ ├── FormField.scala
│ ├── Listing.scala
│ ├── Main.scala
│ ├── Menu.scala
│ ├── Use.scala
│ └── Utils.scala
└── ui-shared/
└── src/
└── main/
└── scala/
└── com/
└── softwaremill/
└── clippy/
├── AdviceState.scala
├── Contributor.scala
└── UiApi.scala
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
*.iml
*.ipr
*.iws
.idea/
target/
data/
*.log
.DS_Store
local.sbt
================================================
FILE: .scalafmt.conf
================================================
style = defaultWithAlign
maxColumn = 120
align.openParenCallSite = false
align.openParenDefnSite = false
danglingParentheses = true
rewrite.rules = [RedundantBraces, RedundantParens, SortImports, PreferCurlyFors]
rewrite.redundantBraces.includeUnitMethods = true
rewrite.redundantBraces.stringInterpolation = true
================================================
FILE: .travis.yml
================================================
sudo: false
language: scala
jdk:
- oraclejdk8
scala:
- 2.11.8
install:
- ". $HOME/.nvm/nvm.sh"
- nvm install stable
- nvm use stable
- npm install
script:
- sbt test
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && sbt updateImpactSubmit || true'
notifications:
slack:
secure: BLLFmdz5G6098EfMgw+CXNqqnwQPDHhaXp0Bg3GWkUoAh3/BoWFhI6iTRgJW3q85IBLa6A2el0wLY8mK1lyD4mRg5dLTPWCILqb8eaF6Shop/FQi0O3pyACvfYUitW9EBalug9VVZzceKsHDaW9pbuqDxCWv1qWyBlTv7aSfVnyYlh6LC+3YqsWFeMHA7/W/lSAklvHkk7GTYIp3A30DsYup2K/EHMJ4ZxuA0CZOOrE4jOfEOzZgm787asPT8o12yb3CWEBZai3Fxs4s6I25w1t6Y5JTPXnYi01OIug6mDtZRDAh1ZJHUdyhOXbxqib6QbCrayg1ou3yde8pKm3KgnMMAJtu6wB8XCMlCDCJ81qkfOCY7TtUfax2spTD5xyIkoQxHPSqOnKyaBYXMKaQ4gMMGiJQToZ8hPtQTYDZTMK5VD5CS1kNMqJBI4V7YlFnKchbv1NUBTKA18QouTCVu369hBb2gANzI1IR4AoP+LRgFjCqEyAAcqoa1jkuENyF1KZykTa3NrkENBlzhEJofYwIi9TP8JUY8mCJp2dnLqnWU/PlJJBTEkMd/00My92znleo2S4gaDgd79quD7iJuZotJt5pkad9auh18ZFQcgmgo9dc3nS3yCk+/vVfPvYKP0pHRWe5RmPkaVbCZ6kCP79LIVlMcA1oYhpNRiwXe5o=
================================================
FILE: LICENSE.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2013-2016 Softwaremill
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Scala clippy
[](https://gitter.im/softwaremill/scala-clippy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://travis-ci.org/softwaremill/scala-clippy)
[](https://app.updateimpact.com/latest/634276070333485056/clippy)
[](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.clippy/plugin_2.11)
Enrich your Scala compiler error output with additional advices and colors!

# Documentation
Read the detailed [documentation](https://scala-clippy.org).
# Contributing to the project
You can also help developing the plugin and/or the UI for submitting new advices! The module structure is:
* `model` - code shared between the UI and the plugin. Contains basic model case classes, such as `CompilationError` + parser
* `plugin` - the compiler plugin which actually displays the advices and matches errors agains the database of known errors
* `tests` - tests for the compiler plugin. Must be a separate project, as it requires the plugin jar to be ready
* `ui` - the ui server project in Play
* `ui-client` - the Scala.JS client-side code
* `ui-shared` - code shared between the UI server and UI client (but not needed for the plugin)
For examples on how to write tests for advice to ensure it does not go out of date see [CompileTests.scala](./tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala).
If you want to write your own tests with compilation using `mkToolbox`, remember to add a `-P:clippy:testmode=true`
compiler option. It ensures that a correct reporter replacement mechanism is used, which needs to be different
specifically for tests. See [CompileTests.scala](tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala) for reference.
To publish locally append "-SNAPSHOT" to the version number then run
````scala
sbt "project plugin" "+ publishLocal"
````
Run advice tests with
````scala
sbt tests/test
````
# Heroku deployment
Locally:
* Install the Heroku Toolbelt
* link the local git repository with the Heroku application: `heroku git:remote -a scala-clippy`
* run `sbt deployHeroku` to deploy the current code as a fat-jar
Currently deployed on `https://www.scala-clippy.org`
# Credits
Clippy contributors:
* [Krzysztof Ciesielski](https://github.com/kciesielski)
* [Łukasz Żuchowski](https://github.com/Zuchos)
* [Shane Delmore](https://github.com/ShaneDelmore)
* [Adam Warski](https://github.com/adamw)
Syntax highlighting code is copied from [Ammonite](http://www.lihaoyi.com/Ammonite/).
================================================
FILE: build.sbt
================================================
import sbt._
import Keys._
import sbtassembly.AssemblyKeys
import scala.xml.transform.RuleTransformer
import scala.xml.transform.RewriteRule
import scala.xml.{Node => XNode}
import scala.xml.{NodeSeq => XNodeSeq}
import scala.xml.{Elem => XElem}
val slickVersion = "3.1.1"
val json4s = "org.json4s" %% "json4s-native" % "3.5.0"
// testing
val scalatest = "org.scalatest" %% "scalatest" % "3.0.1" % "test"
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.13.4" % "test"
name := "clippy"
// factor out common settings into a sequence
lazy val commonSettingsNoScalaVersion = Seq(
organization := "com.softwaremill.clippy",
version := "0.6.1",
scalacOptions ++= Seq("-unchecked", "-deprecation"),
parallelExecution := false,
// Sonatype OSS deployment
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (version.value.trim.endsWith("SNAPSHOT"))
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
},
credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"),
publishMavenStyle := true,
publishArtifact in Test := false,
pomIncludeRepository := { _ =>
false
},
pomExtra :=
<scm>
<url>git@github.com:softwaremill/scala-clippy.git</url>
<connection>scm:git:git@github.com:softwaremill/scala-clippy.git</connection>
</scm>
<developers>
<developer>
<id>adamw</id>
<name>Adam Warski</name>
<url>http://www.warski.org</url>
</developer>
</developers>,
licenses := ("Apache2", new java.net.URL("http://www.apache.org/licenses/LICENSE-2.0.txt")) :: Nil,
homepage := Some(new java.net.URL("http://www.softwaremill.com")),
com.updateimpact.Plugin.apiKey in ThisBuild := sys.env
.getOrElse("UPDATEIMPACT_API_KEY", (com.updateimpact.Plugin.apiKey in ThisBuild).value)
)
lazy val commonSettings = commonSettingsNoScalaVersion ++ Seq(
scalaVersion := "2.11.11"
)
lazy val sbt10CompatSettings = Seq(
sbtVersion in Global := (if (scalaVersion.value startsWith "2.12.") "1.1.6" else "0.13.15"),
scalaCompilerBridgeSource := ("org.scala-sbt" % "compiler-interface" % "0.13.15" % "component").sources
)
lazy val clippy = (project in file("."))
.settings(commonSettings)
.settings(
publishArtifact := false,
// heroku
herokuFatJar in Compile := Some((assemblyOutputPath in ui in assembly).value),
deployHeroku in Compile := (deployHeroku in Compile).dependsOn(assembly in ui).value
)
.aggregate(modelJvm, plugin, pluginSbt, tests, ui)
lazy val model = (crossProject.crossType(CrossType.Pure) in file("model"))
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(scalatest, scalacheck, json4s)
)
lazy val modelJvm = model.jvm.settings(name := "modelJvm")
lazy val modelJs = model.js.settings(name := "modelJs")
def removeDep(groupId: String, artifactId: String) = new RewriteRule {
override def transform(n: XNode): XNodeSeq = n match {
case e: XElem if (e \ "groupId").text == groupId && (e \ "artifactId").text.startsWith(artifactId) =>
XNodeSeq.Empty
case _ => n
}
}
lazy val plugin = (project in file("plugin"))
.enablePlugins(BuildInfoPlugin)
.settings(commonSettings)
.settings(
crossScalaVersions := Seq(scalaVersion.value, "2.12.1", "2.10.6"),
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided",
"com.lihaoyi" %% "scalaparse" % "0.4.2",
"com.lihaoyi" %% "fansi" % "0.2.3",
scalatest,
scalacheck,
json4s
),
// this is needed for fastparse to work on 2.10
libraryDependencies ++= (if (scalaVersion.value startsWith "2.10.")
Seq(compilerPlugin("org.scalamacros" % s"paradise" % "2.1.0" cross CrossVersion.full))
else Seq()),
pomPostProcess := { (node: XNode) =>
new RuleTransformer(removeDep("org.json4s", "json4s-native")).transform(node).head
},
buildInfoPackage := "com.softwaremill.clippy",
buildInfoObject := "ClippyBuildInfo",
artifact in (Compile, assembly) := {
val art = (artifact in (Compile, assembly)).value
art.copy(`classifier` = Some("bundle"))
},
assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = false),
// including the model classes for re-compilation, as for some reason depending on modelJvm doesn't work
unmanagedSourceDirectories in Compile ++= (sourceDirectories in (modelJvm, Compile)).value
)
.settings(sbt10CompatSettings)
.settings(addArtifact(artifact in (Compile, assembly), assembly))
lazy val pluginJar = AssemblyKeys.`assembly` in (plugin, Compile)
lazy val pluginSbt = (project in file("plugin-sbt"))
.enablePlugins(BuildInfoPlugin)
.settings(commonSettingsNoScalaVersion)
.settings(
sbtPlugin := true,
name := "plugin-sbt",
buildInfoPackage := "com.softwaremill.clippy",
buildInfoObject := "ClippyBuildInfo",
scalaVersion := "2.10.6",
crossSbtVersions := Vector("0.13.16", "1.0.0")
)
.settings(sbt10CompatSettings)
lazy val tests = (project in file("tests"))
.settings(commonSettings)
.settings(
publishArtifact := false,
libraryDependencies ++= Seq(
json4s,
scalatest,
"com.typesafe.akka" %% "akka-http" % "10.0.0",
"com.softwaremill.macwire" %% "macros" % "2.2.2" % "provided",
"com.typesafe.slick" %% "slick" % slickVersion
),
// during tests, read from the local repository, if at all available
scalacOptions ++= List(
s"-Xplugin:${pluginJar.value.getAbsolutePath}",
"-P:clippy:url=http://localhost:9000",
"-P:clippy:colors=true"
),
envVars in Test := (envVars in Test).value + ("CLIPPY_PLUGIN_PATH" -> pluginJar.value.getAbsolutePath),
fork in Test := true
)
.dependsOn(modelJvm)
lazy val ui: Project = (project in file("ui"))
.enablePlugins(BuildInfoPlugin)
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"com.h2database" % "h2" % "1.4.190", // % "test",
scalatest,
"org.webjars" %% "webjars-play" % "2.4.0-1",
"org.webjars" % "bootstrap" % "3.3.6",
"org.webjars" % "jquery" % "1.11.3",
"com.vmunier" %% "play-scalajs-scripts" % "0.3.0",
"com.softwaremill.common" %% "id-generator" % "1.1.0",
"com.sendgrid" % "sendgrid-java" % "2.2.2" exclude ("commons-logging", "commons-logging"),
"org.postgresql" % "postgresql" % "9.4.1207",
"com.typesafe.slick" %% "slick" % slickVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion,
"org.flywaydb" % "flyway-core" % "3.2.1"
),
scalaJSProjects := Seq(uiClient),
pipelineStages in Assets := Seq(scalaJSProd),
routesGenerator := InjectedRoutesGenerator,
// heroku & fat-jar
assemblyJarName in assembly := "app.jar",
mainClass in assembly := Some("play.core.server.ProdServerStart"),
fullClasspath in assembly += Attributed.blank(PlayKeys.playPackageAssets.value),
buildInfoPackage := "util",
buildInfoObject := "ClippyBuildInfo",
assemblyMergeStrategy in assembly := {
// anything in public/lib is copied from webjars and causes duplicate resources exceptions
case PathList("public", "lib", xs @ _ *) => MergeStrategy.discard
case "JS_DEPENDENCIES" => MergeStrategy.discard
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
}
)
.enablePlugins(PlayScala)
.aggregate(uiClient)
.dependsOn(uiSharedJvm)
val scalaJsReactVersion = "0.11.3"
lazy val uiClient: Project = (project in file("ui-client"))
.settings(commonSettings)
.settings(name := "uiClient")
.settings(
scalaJSUseMainModuleInitializer := true,
scalaJSUseMainModuleInitializer in Test := false,
addCompilerPlugin(compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)), // for @Lenses
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.1",
"be.doeraene" %%% "scalajs-jquery" % "0.9.1",
"com.github.japgolly.scalajs-react" %%% "core" % scalaJsReactVersion,
"com.github.japgolly.scalajs-react" %%% "ext-monocle" % scalaJsReactVersion,
"com.github.japgolly.fork.monocle" %%% "monocle-macro" % "1.2.0"
),
jsDependencies ++= Seq(
RuntimeDOM % "test",
"org.webjars.bower" % "react" % "15.3.2" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React",
"org.webjars.bower" % "react" % "15.3.2" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM"
)
)
.enablePlugins(ScalaJSPlugin, ScalaJSWeb)
.dependsOn(uiSharedJs)
lazy val uiShared = (crossProject.crossType(CrossType.Pure) in file("ui-shared"))
.settings(commonSettings: _*)
.settings(
name := "uiShared",
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "autowire" % "0.2.5",
"com.lihaoyi" %%% "upickle" % "0.3.6"
)
)
.jsConfigure(_ enablePlugins ScalaJSWeb)
.dependsOn(model)
lazy val uiSharedJvm = uiShared.jvm.settings(name := "uiSharedJvm")
lazy val uiSharedJs = uiShared.js.settings(name := "uiSharedJs")
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/Advice.scala
================================================
package com.softwaremill.clippy
import org.json4s.JsonAST._
final case class Advice(compilationError: CompilationError[RegexT], advice: String, library: Library) {
def errMatching: PartialFunction[CompilationError[ExactT], String] = {
case ce if compilationError.matches(ce) => advice
}
def toJson: JValue = JObject(
"error" -> compilationError.toJson,
"text" -> JString(advice),
"library" -> library.toJson
)
}
object Advice {
def fromJson(jvalue: JValue): Option[Advice] =
(for {
JObject(fields) <- jvalue
JField("error", errorJV) <- fields
error <- CompilationError.fromJson(errorJV).toList
JField("text", JString(text)) <- fields
JField("library", libraryJV) <- fields
library <- Library.fromJson(libraryJV).toList
} yield Advice(error, text, library)).headOption
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/Clippy.scala
================================================
package com.softwaremill.clippy
import org.json4s.JsonAST._
case class Clippy(version: String, advices: List[Advice], fatalWarnings: List[Warning]) {
def checkPluginVersion(ourVersion: String, logInfo: String => Unit) =
if (version != ourVersion) {
logInfo(s"New version of clippy plugin available: $version. Please update!")
}
def toJson: JValue = JObject(
"version" -> JString(version),
"advices" -> JArray(advices.map(_.toJson)),
"fatalWarnings" -> JArray(fatalWarnings.map(_.toJson))
)
}
object Clippy {
def fromJson(jvalue: JValue): Option[Clippy] =
(for {
JObject(fields) <- jvalue
JField("version", JString(version)) <- fields
} yield {
val advices = jvalue.findField {
case JField("advices", _) => true
case _ => false
} match {
case Some((_, JArray(advicesJV))) => advicesJV.flatMap(Advice.fromJson)
case _ => Nil
}
val fatalWarnings = fields.find { tpl =>
tpl._1 == "fatalWarnings"
} match {
case Some((_, JArray(fatalWarningsJV))) => fatalWarningsJV.flatMap(Warning.fromJson)
case _ => Nil
}
Clippy(version, advices, fatalWarnings)
}).headOption
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/CompilationError.scala
================================================
package com.softwaremill.clippy
import org.json4s.JValue
import org.json4s.JsonAST.{JArray, JField, JObject, JString}
import org.json4s.native.JsonMethods._
import CompilationError._
sealed trait CompilationError[T <: Template] {
def toJson: JValue
def toJsonString: String = compact(render(toJson))
def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT): Boolean
def asRegex(implicit ev: T =:= ExactT): CompilationError[RegexT]
}
case class TypeMismatchError[T <: Template](
found: T,
foundExpandsTo: Option[T],
required: T,
requiredExpandsTo: Option[T],
notes: Option[String]
) extends CompilationError[T] {
def notesAfterNewline = notes.fold("")(n => "\n" + n)
override def toString = {
def expandsTo(et: Option[T]): String = et.fold("")(e => s" (expands to: $e)")
s"Type mismatch error.\nFound: $found${expandsTo(foundExpandsTo)},\nrequired: $required${expandsTo(requiredExpandsTo)}$notesAfterNewline"
}
override def toJson =
JObject(
List(TypeField -> JString("typeMismatch"), "found" -> JString(found.v), "required" -> JString(required.v))
++ foundExpandsTo.fold[List[JField]](Nil)(e => List("foundExpandsTo" -> JString(e.v)))
++ requiredExpandsTo.fold[List[JField]](Nil)(e => List("requiredExpandsTo" -> JString(e.v)))
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case TypeMismatchError(f, fe, r, re, _) =>
def optMatches(t: Option[T], v: Option[ExactT]) =
(for {
tt <- t
vv <- v
} yield tt.matches(vv)).getOrElse(true)
found.matches(f) && optMatches(foundExpandsTo, fe) && required.matches(r) && optMatches(requiredExpandsTo, re)
case _ =>
false
}
def hasExpands: Boolean = foundExpandsTo.nonEmpty || requiredExpandsTo.nonEmpty
override def asRegex(implicit ev: T =:= ExactT) =
TypeMismatchError(
RegexT.fromPattern(found.v),
foundExpandsTo.map(fe => RegexT.fromPattern(fe.v)),
RegexT.fromPattern(required.v),
requiredExpandsTo.map(re => RegexT.fromPattern(re.v)),
notes
)
}
case class NotFoundError[T <: Template](what: T) extends CompilationError[T] {
override def toString = s"Not found error: $what"
override def toJson =
JObject(TypeField -> JString("notFound"), "what" -> JString(what.v))
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case NotFoundError(w) => what.matches(w)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) = NotFoundError(RegexT.fromPattern(what.v))
}
case class NotAMemberError[T <: Template](what: T, notAMemberOf: T) extends CompilationError[T] {
override def toString = s"Not a member error: $what isn't a member of $notAMemberOf"
override def toJson =
JObject(
TypeField -> JString("notAMember"),
"what" -> JString(what.v),
"notAMemberOf" -> JString(notAMemberOf.v)
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case NotAMemberError(w, n) => what.matches(w) && notAMemberOf.matches(n)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) =
NotAMemberError(RegexT.fromPattern(what.v), RegexT.fromPattern(notAMemberOf.v))
}
case class ImplicitNotFoundError[T <: Template](parameter: T, implicitType: T) extends CompilationError[T] {
override def toString = s"Implicit not found error: for parameter $parameter of type $implicitType"
override def toJson =
JObject(
TypeField -> JString("implicitNotFound"),
"parameter" -> JString(parameter.v),
"implicitType" -> JString(implicitType.v)
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case ImplicitNotFoundError(p, i) => parameter.matches(p) && implicitType.matches(i)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) =
ImplicitNotFoundError(RegexT.fromPattern(parameter.v), RegexT.fromPattern(implicitType.v))
}
case class TypeclassNotFoundError[T <: Template](typeclass: T, forType: T) extends CompilationError[T] {
override def toString = s"Implicit $typeclass typeclass not found error: for type $forType"
override def toJson =
JObject(
TypeField -> JString("typeclassNotFound"),
"typeclass" -> JString(typeclass.v),
"forType" -> JString(forType.v)
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case TypeclassNotFoundError(p, i) => typeclass.matches(p) && forType.matches(i)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) =
TypeclassNotFoundError(RegexT.fromPattern(typeclass.v), RegexT.fromPattern(forType.v))
}
case class DivergingImplicitExpansionError[T <: Template](forType: T, startingWith: T, in: T)
extends CompilationError[T] {
override def toString = s"Diverging implicit expansion error: for type $forType starting with $startingWith in $in"
override def toJson =
JObject(
TypeField -> JString("divergingImplicitExpansion"),
"forType" -> JString(forType.v),
"startingWith" -> JString(startingWith.v),
"in" -> JString(in.v)
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case DivergingImplicitExpansionError(f, s, i) => forType.matches(f) && startingWith.matches(s) && in.matches(i)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) =
DivergingImplicitExpansionError(
RegexT.fromPattern(forType.v),
RegexT.fromPattern(startingWith.v),
RegexT.fromPattern(in.v)
)
}
case class TypeArgumentsDoNotConformToOverloadedBoundsError[T <: Template](
typeArgs: T,
alternativesOf: T,
alternatives: Set[T]
) extends CompilationError[T] {
override def toString =
s"Type arguments: $typeArgs for overloaded: $alternativesOf do not conform to any bounds: ${alternatives.map(_.toString).mkString(" <or> ")}"
override def toJson =
JObject(
TypeField -> JString("typeArgumentsDoNotConformToOverloadedBounds"),
"typeArgs" -> JString(typeArgs.v),
"alternativesOf" -> JString(alternativesOf.v),
"alternatives" -> JArray(alternatives.map(a => JString(a.v)).toList)
)
override def matches(other: CompilationError[ExactT])(implicit ev: T =:= RegexT) = other match {
case TypeArgumentsDoNotConformToOverloadedBoundsError(t, af, a) =>
typeArgs.matches(t) &&
alternativesOf.matches(af) && RegexT.setMatches(alternatives.map(ev.apply), a)
case _ => false
}
override def asRegex(implicit ev: T =:= ExactT) =
TypeArgumentsDoNotConformToOverloadedBoundsError(
RegexT.fromPattern(typeArgs.v),
RegexT.fromPattern(alternativesOf.v),
alternatives.map(a => RegexT.fromPattern(a.v))
)
}
object CompilationError {
val TypeField = "type"
def fromJsonString(s: String): Option[CompilationError[RegexT]] = fromJson(parse(s))
def fromJson(jvalue: JValue): Option[CompilationError[RegexT]] = {
def regexTFromJson(fields: List[JField], name: String): Option[RegexT] =
(for {
JField(`name`, JString(v)) <- fields
} yield RegexT.fromRegex(v)).headOption
def multipleRegexTFromJson(fields: List[JField], name: String): Option[Set[RegexT]] =
(for {
JField(`name`, JArray(vv)) <- fields
} yield vv.collect { case JString(v) => RegexT.fromRegex(v) }.toSet).headOption
def extractWithType(typeValue: String, fields: List[JField]): Option[CompilationError[RegexT]] = typeValue match {
case "typeMismatch" =>
for {
found <- regexTFromJson(fields, "found")
foundExpandsTo = regexTFromJson(fields, "foundExpandsTo")
required <- regexTFromJson(fields, "required")
requiredExpandsTo = regexTFromJson(fields, "requiredExpandsTo")
} yield TypeMismatchError(found, foundExpandsTo, required, requiredExpandsTo, None)
case "notFound" =>
for {
what <- regexTFromJson(fields, "what")
} yield NotFoundError(what)
case "notAMember" =>
for {
what <- regexTFromJson(fields, "what")
notAMemberOf <- regexTFromJson(fields, "notAMemberOf")
} yield NotAMemberError(what, notAMemberOf)
case "implicitNotFound" =>
for {
parameter <- regexTFromJson(fields, "parameter")
implicitType <- regexTFromJson(fields, "implicitType")
} yield ImplicitNotFoundError(parameter, implicitType)
case "divergingImplicitExpansion" =>
for {
forType <- regexTFromJson(fields, "forType")
startingWith <- regexTFromJson(fields, "startingWith")
in <- regexTFromJson(fields, "in")
} yield DivergingImplicitExpansionError(forType, startingWith, in)
case "typeArgumentsDoNotConformToOverloadedBounds" =>
for {
typeArgs <- regexTFromJson(fields, "typeArgs")
alternativesOf <- regexTFromJson(fields, "alternativesOf")
alternatives <- multipleRegexTFromJson(fields, "alternatives")
} yield TypeArgumentsDoNotConformToOverloadedBoundsError(typeArgs, alternativesOf, alternatives)
case "typeclassNotFound" =>
for {
typeclass <- regexTFromJson(fields, "typeclass")
forType <- regexTFromJson(fields, "forType")
} yield TypeclassNotFoundError(typeclass, forType)
case _ => None
}
(for {
JObject(fields) <- jvalue
JField(TypeField, JString(typeValue)) <- fields
v <- extractWithType(typeValue, fields).toList
} yield v).headOption
}
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/CompilationErrorParser.scala
================================================
package com.softwaremill.clippy
import java.util.regex.Pattern
object CompilationErrorParser {
private val FoundRegexp = """found\s*:\s*([^\n]+)""".r
private val RequiredPrefixRegexp = """required\s*:""".r
private val AfterRequiredRegexp = """required\s*:\s*([^\n]+)""".r
private val WhichExpandsToRegexp = """\s*\(which expands to\)\s*([^\n]+)""".r
private val NotFoundRegexp = """not found\s*:\s*([^\n]+)""".r
private val NotAMemberRegexp = """:?\s*([^\n:]+) is not a member of""".r
private val NotAMemberOfRegexp = """is not a member of\s*([^\n]+)""".r
private val ImplicitNotFoundRegexp = """could not find implicit value for parameter\s*([^:]+):\s*([^\n]+)""".r
private val DivergingImplicitExpansionRegexp =
"""diverging implicit expansion for type\s*([^\s]+)\s*.*\s*starting with method\s*([^\s]+)\s*in\s*([^\n]+)""".r
private val TypeArgumentsDoNotConformToOverloadedBoundsRegexp =
"""type arguments \[([^\]]+)\] conform to the bounds of none of the overloaded alternatives of\s*([^:\n]+)[^:]*: ([^\n]+)""".r
private val TypeclassNotFoundRegexp = """No implicit (.*) defined for ([^\n]+)""".r
def parse(e: String): Option[CompilationError[ExactT]] = {
val error = e.replaceAll(Pattern.quote("[error]"), "")
if (error.contains("type mismatch")) {
RequiredPrefixRegexp.split(error).toList match {
case List(beforeReq, afterReq) =>
for {
found <- FoundRegexp.findFirstMatchIn(beforeReq)
foundExpandsTo = WhichExpandsToRegexp.findFirstMatchIn(beforeReq)
required <- AfterRequiredRegexp.findFirstMatchIn(error)
requiredExpandsTo = WhichExpandsToRegexp.findFirstMatchIn(afterReq)
} yield {
val notes = requiredExpandsTo match {
case Some(et) => getNotesFromIndex(afterReq, et.end)
case None => getNotesFromIndex(error, required.end)
}
TypeMismatchError[ExactT](
ExactT(found.group(1)),
foundExpandsTo.map(m => ExactT(m.group(1))),
ExactT(required.group(1)),
requiredExpandsTo.map(m => ExactT(m.group(1))),
notes
)
}
case _ =>
None
}
} else if (error.contains("not found")) {
for {
what <- NotFoundRegexp.findFirstMatchIn(error)
} yield NotFoundError[ExactT](ExactT(what.group(1)))
} else if (error.contains("is not a member of")) {
for {
what <- NotAMemberRegexp.findFirstMatchIn(error)
notAMemberOf <- NotAMemberOfRegexp.findFirstMatchIn(error)
} yield NotAMemberError[ExactT](ExactT(what.group(1)), ExactT(notAMemberOf.group(1)))
} else if (error.contains("could not find implicit value for parameter")) {
for {
inf <- ImplicitNotFoundRegexp.findFirstMatchIn(error)
} yield ImplicitNotFoundError[ExactT](ExactT(inf.group(1)), ExactT(inf.group(2)))
} else if (error.contains("diverging implicit expansion for type")) {
for {
inf <- DivergingImplicitExpansionRegexp.findFirstMatchIn(error)
} yield DivergingImplicitExpansionError[ExactT](ExactT(inf.group(1)), ExactT(inf.group(2)), ExactT(inf.group(3)))
} else if (error.contains("conform to the bounds of none of the overloaded alternatives")) {
for {
inf <- TypeArgumentsDoNotConformToOverloadedBoundsRegexp.findFirstMatchIn(error)
} yield
TypeArgumentsDoNotConformToOverloadedBoundsError[ExactT](
ExactT(inf.group(1)),
ExactT(inf.group(2)),
inf.group(3).split(Pattern.quote(" <and> ")).toSet.map(ExactT.apply)
)
} else if (error.contains("No implicit")) {
for {
inf <- TypeclassNotFoundRegexp.findFirstMatchIn(error)
group2 = inf.group(2)
} yield
TypeclassNotFoundError(
ExactT(inf.group(1)),
ExactT(if (group2.endsWith(".")) group2.substring(0, group2.length - 1) else group2)
)
} else None
}
private def getNotesFromIndex(msg: String, afterIdx: Int): Option[String] = {
val fromIdx = afterIdx + 1
if (msg.length >= fromIdx + 1) {
val notes = msg.substring(fromIdx).trim
if (notes == "") None else Some(notes)
} else None
}
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/Library.scala
================================================
package com.softwaremill.clippy
import org.json4s.JsonAST.{JField, JObject, JString, JValue}
case class Library(groupId: String, artifactId: String, version: String) {
def toJson: JValue = JObject(
"groupId" -> JString(groupId),
"artifactId" -> JString(artifactId),
"version" -> JString(version)
)
override def toString = s"$groupId:$artifactId:$version"
}
object Library {
def fromJson(jvalue: JValue): Option[Library] =
(for {
JObject(fields) <- jvalue
JField("groupId", JString(groupId)) <- fields
JField("artifactId", JString(artifactId)) <- fields
JField("version", JString(version)) <- fields
} yield Library(groupId, artifactId, version)).headOption
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/StringDiff.scala
================================================
package com.softwaremill.clippy
object StringDiff {
val separators = List(' ', ',', '(', ')', '[', ']', '#', '#', '=', '>', '{', '.')
def isSeparator(char: Char): Boolean = separators.contains(char)
}
class StringDiff(expected: String, actual: String, color: String => String) {
import StringDiff._
def diff(message: String): String =
if (this.expected == this.actual) format(message, this.expected, this.actual)
else format(message, markDiff(expected), markDiff(actual))
private def format(msg: String, expected: String, actual: String) = msg.format(expected, actual)
private def markDiff(source: String) = {
val prefix = findCommonPrefix()
val suffix = findCommonSuffix()
if (overlappingPrefixSuffix(source, prefix, suffix))
source
else {
val diff = color(source.substring(prefix.length, source.length - suffix.length))
prefix + diff + suffix
}
}
private def overlappingPrefixSuffix(source: String, prefix: String, suffix: String) =
prefix.length + suffix.length >= source.length
def findCommonPrefix(expectedStr: String = expected, actualStr: String = actual): String = {
val prefixChars = expectedStr.zip(actualStr).takeWhile(Function.tupled(_ == _)).map(_._1)
val lastSeparatorIndex = prefixChars.lastIndexWhere(isSeparator)
val prefixEndIndex = if (lastSeparatorIndex == -1) 0 else lastSeparatorIndex + 1
if (prefixChars.nonEmpty && prefixEndIndex < prefixChars.length)
prefixChars.mkString.substring(0, prefixEndIndex)
else {
prefixChars.mkString
}
}
def findCommonSuffix(): String =
findCommonPrefix(expected.reverse, actual.reverse).reverse
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/Template.scala
================================================
package com.softwaremill.clippy
import java.util.regex.Pattern
import scala.util.Try
import scala.util.matching.Regex
sealed trait Template {
def v: String
}
case class ExactT(v: String) extends Template {
override def toString = v
}
case class RegexT(v: String) extends Template {
lazy val regex = Try(new Regex(v)).getOrElse(new Regex("^$"))
def matches(e: ExactT): Boolean = regex.pattern.matcher(e.v).matches()
override def toString = v
}
object RegexT {
/**
* Patterns can include wildcards (`*`)
*/
def fromPattern(pattern: String): RegexT = {
val regexp = pattern
.split("\\*", -1)
.map(el => if (el != "") Pattern.quote(el) else el)
.flatMap(el => List(".*", el))
.tail
.filter(_.nonEmpty)
.mkString("")
RegexT.fromRegex(regexp)
}
def fromRegex(v: String): RegexT =
new RegexT(v)
def setMatches(rr: Set[RegexT], ee: Set[ExactT]): Boolean =
if (rr.size != ee.size) false
else {
rr.toList.forall { r =>
ee.exists(r.matches)
}
}
}
================================================
FILE: model/src/main/scala/com/softwaremill/clippy/Warning.scala
================================================
package com.softwaremill.clippy
import org.json4s.JsonAST._
final case class Warning(pattern: RegexT, text: Option[String]) {
def toJson: JValue =
JObject(
"pattern" -> JString(pattern.toString)
) ++ text.map(t => JObject("text" -> JString(t))).getOrElse(JNothing)
}
object Warning {
def fromJson(jvalue: JValue): Option[Warning] =
(for {
JObject(fields) <- jvalue
JField("pattern", JString(patternStr)) <- fields
pattern = RegexT.fromRegex(patternStr)
} yield {
val text = jvalue.findField {
case (("text", _)) => true
case _ => false
} match {
case Some((_, JString(textStr))) => Some(textStr)
case _ => None
}
Warning(pattern, text)
}).headOption
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/CompilationErrorParserTest.scala
================================================
package com.softwaremill.clippy
import org.scalatest.{FlatSpec, Matchers}
class CompilationErrorParserTest extends FlatSpec with Matchers {
it should "parse akka's route error message" in {
val e =
"""type mismatch;
| found : akka.http.scaladsl.server.StandardRoute
| required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT("akka.http.scaladsl.server.StandardRoute"),
None,
ExactT(
"akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]"
),
None,
None
)
)
)
}
it should "parse an error message with [error] prefix" in {
val e =
"""[error] /Users/adamw/projects/clippy/tests/src/main/scala/com/softwaremill/clippy/Working.scala:16: type mismatch;
|[error] found : akka.http.scaladsl.server.StandardRoute
|[error] required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT("akka.http.scaladsl.server.StandardRoute"),
None,
ExactT(
"akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]"
),
None,
None
)
)
)
}
it should "parse a type mismatch error with a single expands to section" in {
val e =
"""type mismatch;
|found : japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]
|required: japgolly.scalajs.react.CompState.AccessRD[?]
| (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT(
"japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]"
),
None,
ExactT("japgolly.scalajs.react.CompState.AccessRD[?]"),
Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")),
None
)
)
)
}
it should "parse a type mismatch error with two expands to sections" in {
val e =
"""type mismatch;
|found : japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]
| (which expands to) japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.FormField]
|required: japgolly.scalajs.react.CompState.AccessRD[?]
| (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT(
"japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.Contribute.Step2.State]#This[com.softwaremill.clippy.FormField]"
),
Some(
ExactT("japgolly.scalajs.react.CompState.ReadCallbackWriteCallbackOps[com.softwaremill.clippy.FormField]")
),
ExactT("japgolly.scalajs.react.CompState.AccessRD[?]"),
Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")),
None
)
)
)
}
it should "parse macwire's wire not found error message" in {
val e = "not found: value wire"
CompilationErrorParser.parse(e) should be(Some(NotFoundError(ExactT("value wire"))))
}
it should "parse not a member of message" in {
val e = "value call is not a member of scala.concurrent.Future[Unit]"
CompilationErrorParser.parse(e) should be(
Some(NotAMemberError(ExactT("value call"), ExactT("scala.concurrent.Future[Unit]")))
)
}
it should "parse not a member of message with extra text" in {
val e =
"[error] /Users/adamw/projects/clippy/ui-client/src/main/scala/com/softwaremill/clippy/Listing.scala:33: value call is not a member of scala.concurrent.Future[Unit]"
CompilationErrorParser.parse(e) should be(
Some(NotAMemberError(ExactT("value call"), ExactT("scala.concurrent.Future[Unit]")))
)
}
it should "parse an implicit not found" in {
val e =
"could not find implicit value for parameter marshaller: spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[String]]"
CompilationErrorParser.parse(e) should be(
Some(
ImplicitNotFoundError(
ExactT("marshaller"),
ExactT("spray.httpx.marshalling.ToResponseMarshaller[scala.concurrent.Future[String]]")
)
)
)
}
it should "parse a diverging implicit error " in {
val e =
"diverging implicit expansion for type io.circe.Decoder.Secondary[this.Out] starting with method decodeCaseClass in trait GenericInstances"
CompilationErrorParser.parse(e) should be(
Some(
DivergingImplicitExpansionError(
ExactT("io.circe.Decoder.Secondary[this.Out]"),
ExactT("decodeCaseClass"),
ExactT("trait GenericInstances")
)
)
)
}
it should "parse a diverging implicit error with extra text" in {
val e =
"""
|[error] /home/src/main/scala/Routes.scala:19: diverging implicit expansion for type io.circe.Decoder.Secondary[this.Out]
|[error] starting with method decodeCaseClass in trait GenericInstances
""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
DivergingImplicitExpansionError(
ExactT("io.circe.Decoder.Secondary[this.Out]"),
ExactT("decodeCaseClass"),
ExactT("trait GenericInstances")
)
)
)
}
it should "parse a type arguments do not conform to any overloaded bounds error" in {
val e =
"""
|[error] clippy/Working.scala:32: type arguments [org.softwaremill.clippy.User] conform to the bounds of none of the overloaded alternatives of
|value apply: [E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E] <and> [E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]
|protected val users = TableQuery[User]
""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeArgumentsDoNotConformToOverloadedBoundsError(
ExactT("org.softwaremill.clippy.User"),
ExactT("value apply"),
Set(
ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"),
ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]")
)
)
)
)
}
it should "parse a no implicit defined for" in {
val e =
"""
|[error] /Users/clippy/model/src/main/scala/com/softwaremill/clippy/CompilationErrorParser.scala:18: No implicit Ordering defined for java.time.LocalDate.
|[error] Seq(java.time.LocalDate.MIN, java.time.LocalDate.MAX).sorted
""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(TypeclassNotFoundError(ExactT("Ordering"), ExactT("java.time.LocalDate")))
)
}
it should "parse an error with notes" in {
val e =
"""
|type mismatch;
| found : org.softwaremill.clippy.ImplicitResolutionDiamond.C
| required: Array[String]
|Note that implicit conversions are not applicable because they are ambiguous:
| both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String]
| and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String]
| are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]
""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT("org.softwaremill.clippy.ImplicitResolutionDiamond.C"),
None,
ExactT("Array[String]"),
None,
Some(
"""Note that implicit conversions are not applicable because they are ambiguous:
| both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String]
| and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String]
| are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]""".stripMargin
)
)
)
)
}
it should "parse an error with expands to & notes" in {
val e =
"""
|type mismatch;
| found : org.softwaremill.clippy.ImplicitResolutionDiamond.C
| required: Array[String]
| (which expands to) japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]
|Note that implicit conversions are not applicable because they are ambiguous:
| both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String]
| and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String]
| are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]
""".stripMargin
CompilationErrorParser.parse(e) should be(
Some(
TypeMismatchError(
ExactT("org.softwaremill.clippy.ImplicitResolutionDiamond.C"),
None,
ExactT("Array[String]"),
Some(ExactT("japgolly.scalajs.react.CompState.ReadDirectWriteCallbackOps[?]")),
Some(
"""Note that implicit conversions are not applicable because they are ambiguous:
| both method toMessage in object B of type (b: org.softwaremill.clippy.ImplicitResolutionDiamond.B)Array[String]
| and method toMessage in object A of type (a: org.softwaremill.clippy.ImplicitResolutionDiamond.A)Array[String]
| are possible conversion functions from org.softwaremill.clippy.ImplicitResolutionDiamond.C to Array[String]""".stripMargin
)
)
)
)
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/CompilationErrorTest.scala
================================================
package com.softwaremill.clippy
import org.scalacheck.Prop._
import org.scalacheck.Properties
import org.scalatest.{FlatSpec, Matchers}
class CompilationErrorTest extends FlatSpec with Matchers {
it should "do not match different exact not found errors" in {
// given
val err = NotFoundError(RegexT.fromPattern("value wire[]"))
val nonMatchingErrs = List(
NotFoundError(ExactT("value wirex")),
NotFoundError(ExactT("value wir")),
NotFoundError(ExactT("avalue wire")),
NotFoundError(ExactT("hakuna matata"))
)
// then
nonMatchingErrs.foreach(err.matches(_) should be(false))
}
it should "match regex in not found errors" in {
// given
val err = NotFoundError(RegexT.fromPattern("value wi*"))
val matchingErrs = List(
NotFoundError(ExactT("value wire")),
NotFoundError(ExactT("value wirex")),
NotFoundError(ExactT("value wi")),
NotFoundError(ExactT("value wire55"))
)
val nonMatchingErrs = List(
NotFoundError(ExactT("avalue wire")),
NotFoundError(ExactT("avalue w5"))
)
// then
matchingErrs.foreach(err.matches(_) should be(true))
nonMatchingErrs.foreach(err.matches(_) should be(false))
}
it should "not match different exact type mismatch errors" in {
// given
val err = TypeMismatchError(
RegexT.fromPattern("com.softwaremill.String"),
None,
RegexT.fromPattern("com.softwaremill.RequiredType[String]"),
None,
None
)
val nonMatchingErrs = List(
TypeMismatchError(ExactT("com.softwaremill.String"), None, ExactT("com.softwaremill.OtherType"), None, None),
TypeMismatchError(ExactT("com.softwaremill.Int"), None, ExactT("com.softwaremill.OtherType"), None, None),
TypeMismatchError(
ExactT("com.softwaremill.Int"),
None,
ExactT("com.softwaremill.RequiredType[String]"),
None,
None
)
)
// then
nonMatchingErrs.foreach(err.matches(_) should be(false))
}
it should "match regex in type mismatch errors" in {
// given
val err = TypeMismatchError(
RegexT.fromPattern("slick.dbio.DBIOAction[*]"),
None,
RegexT.fromPattern("slick.lifted.Rep[Option[*]]"),
None,
None
)
val matchingErrs = List(
TypeMismatchError(
ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"),
None,
ExactT("slick.lifted.Rep[Option[?]]"),
None,
Some("notes")
),
TypeMismatchError(
ExactT("slick.dbio.DBIOAction[String,slick.dbio.NoStream,slick.dbio.Effect.Read]"),
None,
ExactT("slick.lifted.Rep[Option[Int]]"),
None,
None
),
TypeMismatchError(
ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read]"),
None,
ExactT("slick.lifted.Rep[Option[Option[Int]]"),
None,
None
)
)
val nonMatchingErrs = List(
TypeMismatchError(
ExactT("slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read]"),
None,
ExactT("String"),
None,
Some("notes")
),
TypeMismatchError(ExactT("String"), None, ExactT("slick.lifted.Rep[Option[?]]"), None, None),
TypeMismatchError(ExactT("String"), None, ExactT("com.softwaremill.AweSomeType"), None, None)
)
// then
matchingErrs.foreach(err.matches(_) should be(true))
nonMatchingErrs.foreach(err.matches(_) should be(false))
}
}
class CompilationErrorProperties extends Properties("CompilationError") {
property("obj -> json -> obj works for type mismatch error") = forAll {
(found: String, foundExpandsTo: Option[String], required: String, requiredExpandsTo: Option[String]) =>
val e = TypeMismatchError(
RegexT.fromPattern(found),
foundExpandsTo.map(RegexT.fromPattern),
RegexT.fromPattern(required),
requiredExpandsTo.map(RegexT.fromPattern),
None
)
CompilationError.fromJson(e.toJson).contains(e)
}
property("obj -> json -> obj works for not found error") = forAll { (what: String) =>
val e = NotFoundError(RegexT.fromPattern(what))
CompilationError.fromJson(e.toJson).contains(e)
}
property("obj -> json -> obj works for not a member error") = forAll { (what: String, notAMemberOf: String) =>
val e = NotAMemberError(RegexT.fromPattern(what), RegexT.fromPattern(notAMemberOf))
CompilationError.fromJson(e.toJson).contains(e)
}
property("obj -> json -> obj works for implicit not found") = forAll { (parameter: String, implicitType: String) =>
val e = ImplicitNotFoundError(RegexT.fromPattern(parameter), RegexT.fromPattern(implicitType))
CompilationError.fromJson(e.toJson).contains(e)
}
property("obj -> json -> obj works for diverging implicit expansions") = forAll {
(forType: String, startingWith: String, in: String) =>
val e = DivergingImplicitExpansionError(
RegexT.fromPattern(forType),
RegexT.fromPattern(startingWith),
RegexT.fromPattern(in)
)
CompilationError.fromJson(e.toJson).contains(e)
}
property("obj -> json -> obj works for type arguments do not conform to overloaded bounds") = forAll {
(typeArgs: String, alternativesOf: String, alternatives: Set[String]) =>
val e = TypeArgumentsDoNotConformToOverloadedBoundsError(
RegexT.fromPattern(typeArgs),
RegexT.fromPattern(alternativesOf),
alternatives.map(a => RegexT.fromPattern(a))
)
CompilationError.fromJson(e.toJson).contains(e)
}
property("match identical not found error") = forAll { (what: String) =>
NotFoundError(RegexT.fromPattern(what)).matches(NotFoundError(ExactT(what)))
}
property("matches identical type mismatch error") = forAll {
(found: String, required: String, notes: Option[String]) =>
TypeMismatchError(RegexT.fromPattern(found), None, RegexT.fromPattern(required), None, None)
.matches(TypeMismatchError(ExactT(found), None, ExactT(required), None, notes))
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/LibraryProperties.scala
================================================
package com.softwaremill.clippy
import org.scalacheck.Prop._
import org.scalacheck.Properties
class LibraryProperties extends Properties("Library") {
property("obj -> json -> obj") = forAll { (gid: String, aid: String, v: String) =>
val l = Library(gid, aid, v)
Library.fromJson(l.toJson).contains(l)
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/RegexTTest.scala
================================================
package com.softwaremill.clippy
import org.scalatest.{FlatSpec, Matchers}
class RegexTTest extends FlatSpec with Matchers {
val matchingTests = List(
("slick.dbio.DBIOAction[*]", "slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"),
("slick.dbio.DBIOAction[Unit,*,*]", "slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"),
(
"slick.dbio.DBIOAction[*,slick.dbio.NoStream,*]",
"slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Write]"
)
)
val nonMatchingTests = List(
("slick.dbio.DBIOAction[*]", "scala.concurrent.Future[Unit]"),
("slick.dbio.DBIOAction[Unit,*,*]", "slick.dbio.DBIOAction[String,slick.dbio.NoStream,slick.dbio.Effect.Write]"),
(
"slick.dbio.DBIOAction[*,slick.dbio.NoStream,*]",
"slick.dbio.DBIOAction[Unit,slick.dbio.Stream,slick.dbio.Effect.Write]"
)
)
for ((pattern, test) <- matchingTests) {
pattern should s"match $test" in {
RegexT.fromPattern(pattern).matches(ExactT(test)) should be(true)
}
}
for ((pattern, test) <- nonMatchingTests) {
pattern should s"not match $test" in {
RegexT.fromPattern(pattern).matches(ExactT(test)) should be(false)
}
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/StringDiffSpecification.scala
================================================
package com.softwaremill.clippy
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
class StringDiffSpecification extends Properties("StringDiff") with TypeNamesGenerators {
val S = "S"
val E = "E"
val AddSE = (s: String) => S + s + E
def innerTypeDiffsCorrectly(fourTypes: List[String]): Boolean = {
val List(x, y, v, z) = fourTypes
val expected = s"$x[$y[$z]]"
val actual = s"$x[$v[$z]]"
val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s")
msg == s"""expected: $x[$S$y$E[$z]] actual: $x[$S$v$E[$z]]"""
}
def twoTypesAreFullyDiff(twoTypes: List[String]): Boolean = {
val List(x, y) = twoTypes
new StringDiff(x, y, AddSE).diff("expected: %s actual: %s") == s"""expected: $S$x$E actual: $S$y$E"""
}
property("X[Y[Z]] vs X[V[Z]] always gives X[<diff>[Z]] excluding packages") =
forAll(different(singleTypeName)(4))(innerTypeDiffsCorrectly)
property("X[Y[Z]] vs X[V[Z]] always gives X[<diff>[Z]] if Y and V have common prefix") =
forAll(typesWithCommonPrefix(4))(innerTypeDiffsCorrectly)
property("X[Y[Z]] vs X[V[Z]] always gives X[<diff>[Z]] if Y and V have common suffix") =
forAll(typesWithCommonSuffix(4))(innerTypeDiffsCorrectly)
property("A[X] vs B[X] always marks outer as diff for A != B when A and B have common prefix") =
forAll(typesWithCommonPrefix(2), complexTypeName(maxDepth = 3)) { (outerTypes, x) =>
val List(a, b) = outerTypes
val expected = s"$a[$x]"
val actual = s"$b[$x]"
val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s")
msg == s"""expected: $S$a$E[$x] actual: $S$b$E[$x]"""
}
property("package.A[X] vs package.B[X] always gives package.<diff>A</diff>[X]") =
forAll(javaPackage, different(singleTypeName)(2), complexTypeName(maxDepth = 3)) { (pkg, outerTypes, x) =>
val List(a, b) = outerTypes
val expected = s"$pkg$a[$x]"
val actual = s"$pkg$b[$x]"
val msg = new StringDiff(expected, actual, AddSE).diff("expected: %s actual: %s")
msg == s"""expected: $pkg$S$a$E[$x] actual: $pkg$S$b$E[$x]"""
}
property("any complex X vs Y is a full diff when X and Y don't have common suffix nor prefix") =
forAll(different(complexTypeName(maxDepth = 4))(2).suchThat(noCommonPrefixSuffix))(twoTypesAreFullyDiff)
property("any single X vs Y is a full diff") = forAll(different(singleTypeName)(2))(twoTypesAreFullyDiff)
def noCommonPrefixSuffix(twoTypes: List[String]): Boolean = {
val List(x, y) = twoTypes
x.head != y.head && x.last != y.last
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/StringDiffTest.scala
================================================
package com.softwaremill.clippy
import org.scalatest.{FlatSpec, Matchers}
class StringDiffTest extends FlatSpec with Matchers {
val S = "S"
val E = "E"
val AddSE = (s: String) => S + s + E
val testData = List(
(
"Super[String, String]",
"Super[Option[String], String]",
"expected Super[" + S + "String" + E + ", String] but was Super[" + S + "Option[String]" + E + ", String]"
),
(
"Cool[String, String]",
"Super[Option[String], String]",
"expected " + S + "Cool[String" + E + ", String] but was " + S + "Super[Option[String]" + E + ", String]"
),
(
"(String, String)",
"Super[Option[String], String]",
"expected " + S + "(String, String)" + E + " but was " + S + "Super[Option[String], String]" + E
),
(
"Map[Long, Double]",
"Map[String, Double]",
"expected Map[" + S + "Long" + E + ", Double] but was Map[" + S + "String" + E + ", Double]"
),
(
"(Int, Int, Float, Int, Char)",
"(Int, Int, Int, Char)",
"expected (Int, Int, " + S + "Float" + E + ", Int, Char) but was (Int, Int, Int, Char)"
)
)
"StringDiff" should "diff" in {
for ((expected, actual, expectedDiff) <- testData) {
val diff = new StringDiff(expected, actual, AddSE).diff("expected %s but was %s")
diff should be(expectedDiff)
}
}
it should "find common prefix" in {
new StringDiff("Map[Long, Double]", "Map[String, Double]", AddSE).findCommonPrefix() should be("Map[")
}
it should "find common suffix" in {
new StringDiff("Map[Long, Double]", "Map[String, Double]", AddSE).findCommonSuffix() should be(", Double]")
}
}
================================================
FILE: model/src/test/scala/com/softwaremill/clippy/TypeNamesGenerators.scala
================================================
package com.softwaremill.clippy
import org.scalacheck.Gen
trait TypeNamesGenerators {
import Gen._
def typeNameChar = frequency((1, numChar), (3, Gen.const('_')), (14, alphaChar))
val specialTypeName = Gen.oneOf(List("Option", "Operation", "Op", "String", "Long", "Set", "Aux"))
def javaPackage: Gen[String] =
for {
depth <- Gen.choose(0, 3)
names <- Gen.listOfN(depth, packageIdentifer)
} yield {
val str = names.filter(_.nonEmpty).mkString(".")
if (str.nonEmpty)
str + "."
else
str
}
def packageIdentifer: Gen[String] =
for {
c <- alphaLowerChar
cs <- Gen.resize(7, listOf(alphaNumChar))
} yield {
(c :: cs).mkString
}
def randomTypeWithPackage: Gen[String] =
for {
p <- javaPackage
t <- randomTypeWithPackage
} yield p + t
def randomTypeName: Gen[String] =
for {
c <- alphaChar
cs <- Gen.resize(7, listOf(typeNameChar))
} yield (c :: cs).mkString
def singleTypeName = frequency((3, randomTypeName), (7, specialTypeName))
def functionalTypeName(maxResultDepth: Int): Gen[String] =
for {
argsMemberCount <- Gen.choose(2, 4)
args <- Gen.oneOf(singleTypeName, tupleTypeName(depth = 0, argsMemberCount))
result <- complexTypeName(maxResultDepth)
} yield {
s"$args => $result"
}
def genericTypeName(depth: Int, memberCount: Int): Gen[String] =
for {
name <- singleTypeName
innerNames <- Gen.listOfN(memberCount, if (depth == 0) singleTypeName else complexTypeName(depth - 1))
} yield s"$name[${innerNames.mkString(", ")}]"
def tupleTypeName(depth: Int, memberCount: Int): Gen[String] =
for {
innerNames <- Gen.listOfN(memberCount, if (depth == 0) singleTypeName else complexTypeName(depth - 1))
} yield s"(${innerNames.mkString(", ")})"
def different[T](gen: Gen[T]) =
(count: Int) =>
Gen.listOfN(count, gen).suchThat { list =>
list.distinct == list
}
def typesWithCommonPrefix =
(count: Int) =>
for {
types <- different(singleTypeName)(count)
commonPrefix <- singleTypeName
} yield types.map(commonPrefix + _)
def typesWithCommonSuffix =
(count: Int) =>
for {
types <- different(singleTypeName)(count)
commonSuffix <- singleTypeName
} yield types.map(_ + commonSuffix)
def flatTypeIdentifier =
for {
tupleMemberCount <- Gen.choose(2, 4)
generator <- Gen.oneOf(
Seq(singleTypeName, tupleTypeName(0, tupleMemberCount), innerTypeName(0), functionalTypeName(0))
)
typeStr <- generator
} yield typeStr
def deepTypeName(maxDepth: Int): Gen[String] =
for {
genericMemberCount <- Gen.choose(1, 3)
tupleMemberCount <- Gen.choose(2, 4)
generator <- Gen.oneOf(
Seq(
singleTypeName,
tupleTypeName(maxDepth, tupleMemberCount),
genericTypeName(maxDepth, genericMemberCount),
innerTypeName(maxDepth),
functionalTypeName(maxDepth)
)
)
typeStr <- generator
} yield typeStr
def complexTypeName(maxDepth: Int): Gen[String] =
if (maxDepth == 0)
flatTypeIdentifier
else
deepTypeName(maxDepth)
def innerTypeName(maxDepth: Int): Gen[String] =
for {
outerName <- singleTypeName
innerType <- complexTypeName(maxDepth)
} yield s"$outerName#$innerType"
}
================================================
FILE: package.json
================================================
{
"name": "scala-clippy-site",
"dependencies": {
"jsdom": "9.12.0"
}
}
================================================
FILE: plugin/src/main/resources/scalac-plugin.xml
================================================
<plugin>
<name>clippy</name>
<classname>com.softwaremill.clippy.ClippyPlugin</classname>
</plugin>
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/AdviceLoader.scala
================================================
package com.softwaremill.clippy
import java.io._
import java.net.{HttpURLConnection, URL}
import java.util.zip.GZIPInputStream
import com.softwaremill.clippy.Utils._
import scala.concurrent.{ExecutionContext, Future}
import scala.io.Source
import scala.tools.nsc.Global
import scala.util.{Failure, Success, Try}
import scala.collection.JavaConverters._
class AdviceLoader(
global: Global,
url: String,
localStoreDir: File,
projectAdviceFile: Option[File],
localAdviceFiles: List[URL]
)(implicit ec: ExecutionContext) {
private val OneDayMillis = 1000L * 60 * 60 * 24
private val localStore = new File(localStoreDir, "clippy.json.gz")
private lazy val resourcesAdvice: AdvicesAndWarnings =
localAdviceFiles.map(loadAdviceFromUrL).reduceOption(_ ++ _).getOrElse(AdvicesAndWarnings.empty)
private def loadAdviceFromUrL(url: URL): AdvicesAndWarnings =
TryWith(url.openStream())(inputStreamToClippy(_)) match {
case Success(clippyData) => AdvicesAndWarnings(clippyData.advices, clippyData.fatalWarnings)
case Failure(_) =>
global.inform(s"Cannot load advice from ${url.getPath} : Ignoring.")
AdvicesAndWarnings.empty
}
private lazy val projectAdvice: AdvicesAndWarnings =
projectAdviceFile.map(file => loadAdviceFromUrL(file.toURI.toURL)).getOrElse(AdvicesAndWarnings.empty)
def load(): Future[Clippy] = {
val localClippy = if (!localStore.exists()) {
fetchStoreParse()
} else {
val needsUpdate = System.currentTimeMillis() - localStore.lastModified() > OneDayMillis
// fetching in the background
val runningFetch = if (needsUpdate) {
Some(fetchStoreParseInBackground())
} else None
val localLoad = Try(loadLocally()) match {
case Success(v) => Future.successful(v)
case Failure(t) => Future.failed(t)
}
localLoad.map(bytes => inputStreamToClippy(decodeZippedBytes(bytes))).recoverWith {
case e: Exception =>
global.warning(s"Cannot load advice from local store: $localStore. Trying to fetch from server")
runningFetch.getOrElse(fetchStoreParse())
}
}
// Add in advice found in resources and project root
localClippy.map(
clippy =>
clippy.copy(
advices = (projectAdvice.advices ++ resourcesAdvice.advices ++ clippy.advices).distinct,
fatalWarnings =
(projectAdvice.fatalWarnings ++ resourcesAdvice.fatalWarnings ++ clippy.fatalWarnings).distinct
)
)
}
private def fetchStoreParse(): Future[Clippy] =
fetchCompressedJson()
.map { bytes =>
storeLocallyInBackground(bytes)
bytes
}
.map(bytes => inputStreamToClippy(decodeZippedBytes(bytes)))
.recover {
case e: Exception =>
global.inform(s"Unable to load/store local Clippy advice due to: ${e.getMessage}")
Clippy(ClippyBuildInfo.version, Nil, Nil)
}
.andThen { case Success(v) => v.checkPluginVersion(ClippyBuildInfo.version, println) }
private def fetchStoreParseInBackground(): Future[Clippy] = {
val f = fetchStoreParse()
f.onFailure {
case e: Exception => global.inform(s"Cannot fetch data from $url due to: $e")
}
f
}
private def fetchCompressedJson(): Future[Array[Byte]] = Future {
val u = new URL(url)
val conn = u.openConnection().asInstanceOf[HttpURLConnection]
try {
conn.setRequestMethod("GET")
inputStreamToBytes(conn.getInputStream)
} finally conn.disconnect()
}
private def decodeZippedBytes(bytes: Array[Byte]): GZIPInputStream = new GZIPInputStream(decodeUtf8Bytes(bytes))
private def decodeUtf8Bytes(bytes: Array[Byte]): ByteArrayInputStream = new ByteArrayInputStream(bytes)
private def inputStreamToClippy(byteStream: InputStream): Clippy = {
import org.json4s.native.JsonMethods._
val data = Source.fromInputStream(byteStream, "UTF-8").getLines().mkString("\n")
Clippy
.fromJson(parse(data))
.getOrElse(throw new IllegalArgumentException("Cannot deserialize Clippy data"))
}
private def storeLocally(bytes: Array[Byte]): Unit = {
if (!localStoreDir.isDirectory && !localStoreDir.mkdir()) {
throw new IOException(s"Cannot create directory $localStoreDir")
}
TryWith(new FileOutputStream(localStore))(_.write(bytes)).get
}
private def storeLocallyInBackground(bytes: Array[Byte]): Unit =
Future {
runNonDaemon {
storeLocally(bytes)
}
}.onFailure {
case e: Exception => global.inform(s"Cannot store data at $localStore due to: $e")
}
private def loadLocally(source: File = localStore): Array[Byte] = inputStreamToBytes(new FileInputStream(source))
}
object AdviceLoader {
val localFile = "clippy.json.gz"
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/ClippyPlugin.scala
================================================
package com.softwaremill.clippy
import java.io.File
import java.net.{URL, URLClassLoader}
import java.util.concurrent.TimeoutException
import scala.collection.JavaConverters._
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.reflect.internal.util.Position
import scala.tools.nsc.Global
import scala.tools.nsc.plugins.Plugin
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.util.PathResolver
final case class AdvicesAndWarnings(advices: List[Advice], fatalWarnings: List[Warning]) {
def ++(other: AdvicesAndWarnings): AdvicesAndWarnings =
copy(advices = advices ++ other.advices, fatalWarnings = fatalWarnings ++ other.fatalWarnings)
}
object AdvicesAndWarnings {
def empty: AdvicesAndWarnings = AdvicesAndWarnings(Nil, Nil)
}
class ClippyPlugin(val global: Global) extends Plugin {
override val name: String = "clippy"
override val description: String = "gives good advice"
var url: String = ""
var colorsConfig: ColorsConfig = ColorsConfig.Disabled
var testMode = false
val DefaultStoreDir = new File(System.getProperty("user.home"), ".clippy")
var localStoreDir = DefaultStoreDir
var projectRoot: Option[File] = None
var initialFatalWarnings: List[Warning] = Nil
lazy val localAdviceFiles = {
val classPathURLs = new PathResolver(global.settings).result.asURLs
val classLoader = new URLClassLoader(classPathURLs.toArray, getClass.getClassLoader)
classLoader.getResources("clippy.json").asScala.toList
}
lazy val advicesAndWarnings =
loadAdvicesAndWarnings(url, localStoreDir, projectRoot, localAdviceFiles)
def getFatalWarningAdvice(warningText: String): Option[Warning] =
advicesAndWarnings.fatalWarnings.find(warning => warning.pattern.matches(ExactT(warningText)))
def handleError(pos: Position, msg: String): String = {
val parsedMsg = CompilationErrorParser.parse(msg)
val matchers = advicesAndWarnings.advices.map(_.errMatching.lift)
val matches = matchers.flatMap(pf => parsedMsg.flatMap(pf)).distinct
matches.size match {
case 0 =>
(parsedMsg, colorsConfig) match {
case (Some(tme: TypeMismatchError[ExactT]), cc: ColorsConfig.Enabled) =>
prettyPrintTypeMismatchError(tme, cc)
case _ => msg
}
case 1 =>
matches.mkString(s"$msg\n Clippy advises: ", "", "")
case _ =>
matches.mkString(s"$msg\n Clippy advises you to try one of these solutions: \n ", "\n or\n ", "")
}
}
override def processOptions(options: List[String], error: (String) => Unit): Unit = {
colorsConfig = colorsFromOptions(options)
url = urlFromOptions(options)
testMode = testModeFromOptions(options)
localStoreDir = localStoreDirFromOptions(options)
projectRoot = projectRootFromOptions(options)
initialFatalWarnings = initialFatalWarningsFromOptions(options)
if (testMode) {
val r = global.reporter
global.reporter = new FailOnWarningsReporter(
new DelegatingReporter(r, handleError, colorsConfig),
getFatalWarningAdvice,
colorsConfig
)
}
}
override val components: List[PluginComponent] = {
List(
new InjectReporter(handleError, getFatalWarningAdvice, global) {
override def colorsConfig = ClippyPlugin.this.colorsConfig
override def isEnabled = !testMode
},
new RestoreReporter(global) {
override def isEnabled = !testMode
}
)
}
private def prettyPrintTypeMismatchError(tme: TypeMismatchError[ExactT], colors: ColorsConfig.Enabled): String = {
val colorDiff = (s: String) => colors.diff(s).toString
val plain = new StringDiff(tme.found.toString, tme.required.toString, colorDiff)
val expandsMsg = if (tme.hasExpands) {
val reqExpandsTo = tme.requiredExpandsTo.getOrElse(tme.required)
val foundExpandsTo = tme.foundExpandsTo.getOrElse(tme.found)
val expands = new StringDiff(foundExpandsTo.toString, reqExpandsTo.toString, colorDiff)
s"""${expands.diff("\nExpanded types:\nfound : %s\nrequired: %s\"")}"""
} else
""
s""" type mismatch;
| Clippy advises, pay attention to the marked parts:
| ${plain.diff("found : %s\n required: %s")}$expandsMsg${tme.notesAfterNewline}""".stripMargin
}
private def urlFromOptions(options: List[String]): String =
options.find(_.startsWith("url=")).map(_.substring(4)).getOrElse("https://www.scala-clippy.org") + "/api/advices"
private def colorsFromOptions(options: List[String]): ColorsConfig =
if (boolFromOptions(options, "colors")) {
def colorToFansi(color: String): fansi.Attrs = color match {
case "black" => fansi.Color.Black
case "light-gray" => fansi.Color.LightGray
case "dark-gray" => fansi.Color.DarkGray
case "red" => fansi.Color.Red
case "light-red" => fansi.Color.LightRed
case "green" => fansi.Color.Green
case "light-green" => fansi.Color.LightGreen
case "yellow" => fansi.Color.Yellow
case "light-yellow" => fansi.Color.LightYellow
case "blue" => fansi.Color.Blue
case "light-blue" => fansi.Color.LightBlue
case "magenta" => fansi.Color.Magenta
case "light-magenta" => fansi.Color.LightMagenta
case "cyan" => fansi.Color.Cyan
case "light-cyan" => fansi.Color.LightCyan
case "white" => fansi.Color.White
case "none" => fansi.Attrs.Empty
case x =>
global.warning("Unknown color: " + x)
fansi.Attrs.Empty
}
val partColorPattern = "colors-(.*)=(.*)".r
options.filter(_.startsWith("colors-")).foldLeft(ColorsConfig.defaultEnabled) {
case (current, partAndColor) =>
val partColorPattern(part, colorStr) = partAndColor
val color = colorToFansi(colorStr.trim.toLowerCase())
part.trim.toLowerCase match {
case "diff" => current.copy(diff = color)
case "comment" => current.copy(comment = color)
case "type" => current.copy(`type` = color)
case "literal" => current.copy(literal = color)
case "keyword" => current.copy(keyword = color)
case "reset" => current.copy(reset = color)
case x =>
global.warning("Unknown colored part: " + x)
current
}
}
} else ColorsConfig.Disabled
private def testModeFromOptions(options: List[String]): Boolean = boolFromOptions(options, "testmode")
private def boolFromOptions(options: List[String], option: String): Boolean =
options
.find(_.startsWith(s"$option="))
.map(_.substring(option.length + 1))
.getOrElse("false")
.toBoolean
private def projectRootFromOptions(options: List[String]): Option[File] =
options
.find(_.startsWith("projectRoot="))
.map(_.substring(12))
.map(new File(_, ".clippy.json"))
.filter(_.exists())
private def initialFatalWarningsFromOptions(options: List[String]): List[Warning] =
options
.find(_.startsWith("fatalWarnings="))
.map(_.substring(14))
.map { str =>
str.split('|').toList.map(str => Warning(RegexT(str), text = None))
}
.getOrElse(Nil)
private def localStoreDirFromOptions(options: List[String]): File =
options.find(_.startsWith("store=")).map(_.substring(6)).map(new File(_)).getOrElse(DefaultStoreDir)
private def loadAdvicesAndWarnings(
url: String,
localStoreDir: File,
projectAdviceFile: Option[File],
localAdviceFiles: List[URL]
): AdvicesAndWarnings = {
implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
try {
val clippyData = Await
.result(
new AdviceLoader(global, url, localStoreDir, projectAdviceFile, localAdviceFiles).load(),
10.seconds
)
AdvicesAndWarnings(clippyData.advices, clippyData.fatalWarnings ++ initialFatalWarnings)
} catch {
case e: TimeoutException =>
global.warning(s"Unable to read advices from $url and store to $localStoreDir within 10 seconds.")
AdvicesAndWarnings.empty
case e: Exception =>
global.warning(s"Exception when reading advices from $url and storing to $localStoreDir: $e")
AdvicesAndWarnings.empty
}
}
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/ColorsConfig.scala
================================================
package com.softwaremill.clippy
sealed trait ColorsConfig
object ColorsConfig {
case object Disabled extends ColorsConfig
case class Enabled(
diff: fansi.Attrs,
comment: fansi.Attrs,
`type`: fansi.Attrs,
literal: fansi.Attrs,
keyword: fansi.Attrs,
reset: fansi.Attrs
) extends ColorsConfig
val defaultEnabled = Enabled(
fansi.Color.Red,
fansi.Color.Blue,
fansi.Color.Green,
fansi.Color.Magenta,
fansi.Color.Yellow,
fansi.Attr.Reset
)
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/FailOnWarningsReporter.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.Position
import scala.tools.nsc.reporters.Reporter
class FailOnWarningsReporter(r: Reporter, warningMatcher: String => Option[Warning], colorsConfig: ColorsConfig)
extends Reporter {
override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
// cannot delegate to info0 as it's protected, hence special-casing on the possible severity values
if (severity == INFO) {
r.info(wrapped, msg, force)
} else if (severity == WARNING) {
warning(wrapped, msg)
} else if (severity == ERROR) {
error(wrapped, msg)
} else {
error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg")
}
}
override def echo(msg: String) = r.echo(msg)
override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def hasErrors = r.hasErrors || cancelled
override def reset() = {
cancelled = false
r.reset()
}
//
override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def errorCount = r.errorCount
override def warningCount = r.warningCount
override def hasWarnings = r.hasWarnings
override def flush() = r.flush()
override def count(severity: Severity): Int = r.count(conv(severity))
override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity))
//
private def conv(s: Severity): r.Severity = s match {
case INFO => r.INFO
case WARNING => r.WARNING
case ERROR => r.ERROR
}
//
override def warning(pos: Position, msg: String) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
warningMatcher(msg) match {
case Some(Warning(_, adviceOpt)) =>
val finalMsg = adviceOpt.map(advice => msg + s"\nClippy advises: $advice").getOrElse(msg)
r.error(wrapped, finalMsg)
case None =>
r.warning(wrapped, msg)
}
}
override def error(pos: Position, msg: String) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
r.error(wrapped, msg)
}
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/Highlighter.scala
================================================
package com.softwaremill.clippy
import fastparse.all._
import scalaparse.Scala._
import scalaparse.syntax.Identifiers._
/**
* Copied from https://github.com/lihaoyi/Ammonite/blob/master/amm/repl/src/main/scala/ammonite/repl/Highlighter.scala
*/
object Highlighter {
object BackTicked {
def unapplySeq(s: Any): Option[List[String]] =
"`([^`]+)`".r.unapplySeq(s.toString)
}
def flattenIndices(
boundedIndices: Seq[(Int, fansi.Attrs, Boolean)],
buffer: Vector[Char]
) =
boundedIndices
.sliding(2)
.map {
case Seq((s, c1, _), (e, c2, _)) =>
assert(e >= s, s"s: $s e: $e")
c1(fansi.Str(buffer.slice(s, e), errorMode = fansi.ErrorMode.Sanitize))
}
.reduce(_ ++ _)
.render
.toVector
def defaultHighlight(
buffer: Vector[Char],
comment: fansi.Attrs,
`type`: fansi.Attrs,
literal: fansi.Attrs,
keyword: fansi.Attrs,
reset: fansi.Attrs
) = {
val boundedIndices = defaultHighlightIndices(buffer, comment, `type`, literal, keyword, reset)
flattenIndices(boundedIndices, buffer)
}
def defaultHighlightIndices(
buffer: Vector[Char],
comment: fansi.Attrs,
`type`: fansi.Attrs,
literal: fansi.Attrs,
keyword: fansi.Attrs,
reset: fansi.Attrs
) = Highlighter.highlightIndices(
Parsers.Splitter,
buffer, {
case Literals.Expr.Interp | Literals.Pat.Interp => reset
case Literals.Comment => comment
case ExprLiteral => literal
case TypeId => `type`
case BackTicked(body) if alphaKeywords.contains(body) => keyword
},
reset
)
def highlightIndices[T](
parser: Parser[_],
buffer: Vector[Char],
ruleColors: PartialFunction[Parser[_], T],
endColor: T
): Seq[(Int, T, Boolean)] = {
val indices = {
var indices = collection.mutable.Buffer((0, endColor, false))
var done = false
val input = buffer.mkString
parser.parse(
input,
instrument = (rule, idx, res) => {
for (color <- ruleColors.lift(rule)) {
val closeColor = indices.last._2
val startIndex = indices.length
indices += ((idx, color, true))
res() match {
case s: Parsed.Success[_] =>
val prev = indices(startIndex - 1)._1
if (idx < prev && s.index <= prev) {
indices.remove(startIndex, indices.length - startIndex)
}
while (idx < indices.last._1 && s.index <= indices.last._1) {
indices.remove(indices.length - 1)
}
indices += ((s.index, closeColor, false))
if (s.index == buffer.length) done = true
case f: Parsed.Failure
if f.index == buffer.length
&& (WL ~ End).parse(input, idx).isInstanceOf[Parsed.Failure] =>
// EOF, stop all further parsing
done = true
case _ => // hard failure, or parsed nothing. Discard all progress
indices.remove(startIndex, indices.length - startIndex)
}
}
}
)
indices
}
// Make sure there's an index right at the start and right at the end! This
// resets the colors at the snippet's end so they don't bleed into later output
indices ++ Seq((999999999, endColor, false))
}
def highlight(
parser: Parser[_],
buffer: Vector[Char],
ruleColors: PartialFunction[Parser[_], fansi.Attrs],
endColor: fansi.Attrs
) = {
val boundedIndices = highlightIndices(parser, buffer, ruleColors, endColor)
flattenIndices(boundedIndices, buffer)
}
}
object Parsers {
import fastparse.noApi._
import scalaparse.Scala._
import WhitespaceApi._
val Prelude = P((Annot ~ OneNLMax).rep ~ (Mod ~/ Pass).rep)
val Statement =
P(scalaparse.Scala.TopPkgSeq | scalaparse.Scala.Import | Prelude ~ BlockDef | StatCtx.Expr)
def StatementBlock(blockSep: P0) =
P(Semis.? ~ (!blockSep ~ Statement ~~ WS ~~ (Semis | End)).!.repX)
val Splitter = P(StatementBlock(Fail) ~ WL ~ End)
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/InjectReporter.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.Position
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.{Global, Phase}
/**
* Responsible for replacing the global reporter with our custom Clippy reporter after the first phase of compilation.
*/
abstract class InjectReporter(
handleError: (Position, String) => String,
getFatalWarningAdvice: String => Option[Warning],
superGlobal: Global
) extends PluginComponent {
override val global = superGlobal
def colorsConfig: ColorsConfig
def isEnabled: Boolean
override val runsAfter = List[String]("parser")
override val runsBefore = List[String]("namer")
override val phaseName = "inject-clippy-reporter"
override def newPhase(prev: Phase) = new Phase(prev) {
override def name = phaseName
override def description = "Switches the reporter to Clippy's reporter chain"
override def run(): Unit =
if (isEnabled) {
val r = global.reporter
global.reporter = new FailOnWarningsReporter(
new DelegatingReporter(r, handleError, colorsConfig),
getFatalWarningAdvice,
colorsConfig
)
}
}
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/RestoreReporter.scala
================================================
package com.softwaremill.clippy
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.{Global, Phase}
/**
* Replaces global reporter back with the original global reporter. Sbt uses its own xsbt.DelegatingReporter
* which we cannot replace outside of Scala compilation phases. This component makes sure that before the compilation
* is over, original reporter gets reassigned to the global field.
*/
class RestoreReporter(val global: Global) extends PluginComponent {
val originalReporter = global.reporter
def isEnabled: Boolean = true
override val runsAfter = List[String]("jvm")
override val runsBefore = List[String]("terminal")
override val phaseName = "restore-original-reporter"
override def newPhase(prev: Phase) = new Phase(prev) {
override def name = phaseName
override def description = "Switches the reporter from Clippy's DelegatingReporter back to original one"
override def run(): Unit =
if (isEnabled)
global.reporter = originalReporter
}
}
================================================
FILE: plugin/src/main/scala/com/softwaremill/clippy/Utils.scala
================================================
package com.softwaremill.clippy
import java.io.{ByteArrayOutputStream, InputStream}
import java.io.Closeable
import scala.util.control.NonFatal
import scala.util.{Failure, Try}
object Utils {
/**
* All future callbacks will be running on a daemon thread pool which can be interrupted at any time if the JVM
* exits, if the compiler finished its job.
*
* Here we are trying to make as sure as possible (unless the JVM crashes) that we'll run the given code.
*/
def runNonDaemon(t: => Unit) = {
val shutdownHook = new Thread() {
private val lock = new Object
@volatile private var didRun = false
override def run() =
lock.synchronized {
if (!didRun) {
t
didRun = true
}
}
}
Runtime.getRuntime.addShutdownHook(shutdownHook)
try shutdownHook.run()
finally Runtime.getRuntime.removeShutdownHook(shutdownHook)
}
def inputStreamToBytes(is: InputStream): Array[Byte] =
try {
val baos = new ByteArrayOutputStream()
val buf = new Array[Byte](512)
var read = 0
while ({ read = is.read(buf, 0, buf.length); read } != -1) {
baos.write(buf, 0, read)
}
baos.toByteArray
} finally is.close()
object TryWith {
def apply[C <: Closeable, R](resource: => C)(f: C => R): Try[R] =
Try(resource).flatMap(resourceInstance => {
try {
val returnValue = f(resourceInstance)
Try(resourceInstance.close()).map(_ => returnValue)
} catch {
case NonFatal(exceptionInFunction) =>
try {
resourceInstance.close()
Failure(exceptionInFunction)
} catch {
case NonFatal(exceptionInClose) =>
exceptionInFunction.addSuppressed(exceptionInClose)
Failure(exceptionInFunction)
}
}
})
}
}
================================================
FILE: plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingPosition.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.{NoPosition, Position, SourceFile}
import scala.reflect.macros.Attachments
class DelegatingPosition(delegate: Position, colorsConfig: ColorsConfig) extends Position {
// used by scalac to report errors
override def showError(msg: String): String = highlight(delegate.showError(msg))
// used by sbt
override def lineContent: String = highlight(delegate.lineContent)
def highlight(str: String): String = colorsConfig match {
case e: ColorsConfig.Enabled =>
Highlighter
.defaultHighlight(
str.toVector,
e.comment,
e.`type`,
e.literal,
e.keyword,
e.reset
)
.mkString
case _ => str
}
// impl copied
override def fail(what: String): Nothing = throw new UnsupportedOperationException(s"Position.$what on $this")
// simple delegates with position wrapping when position is returned
@scala.deprecated("use `point`")
override def offset: Option[Int] = delegate.offset
override def all: Set[Any] = delegate.all
@scala.deprecated("use `focus`")
override def toSingleLine: Position = DelegatingPosition.wrap(delegate.toSingleLine, colorsConfig)
override def pos: Position = DelegatingPosition.wrap(delegate.pos, colorsConfig)
override def get[T](implicit evidence$2: ClassManifest[T]): Option[T] = delegate.get(evidence$2)
override def finalPosition: Pos = DelegatingPosition.wrap(delegate.finalPosition, colorsConfig)
override def withPos(newPos: Position): Attachments { type Pos = DelegatingPosition.this.Pos } =
delegate.withPos(newPos)
@scala.deprecated("use `line`")
override def safeLine: Int = delegate.safeLine
override def isTransparent: Boolean = delegate.isTransparent
override def contains[T](implicit evidence$3: ClassManifest[T]): Boolean = delegate.contains(evidence$3)
override def isOffset: Boolean = delegate.isOffset
@scala.deprecated("use `showDebug`")
override def dbgString: String = delegate.dbgString
override def isOpaqueRange: Boolean = delegate.isOpaqueRange
override def update[T](attachment: T)(
implicit evidence$4: ClassManifest[T]
): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.update(attachment)(evidence$4)
override def pointOrElse(alt: Int): Int = delegate.pointOrElse(alt)
@scala.deprecated("use `finalPosition`")
override def inUltimateSource(source: SourceFile): Position =
DelegatingPosition.wrap(delegate.inUltimateSource(source), colorsConfig)
override def isDefined: Boolean = delegate.isDefined
override def makeTransparent: Position = DelegatingPosition.wrap(delegate.makeTransparent, colorsConfig)
override def isRange: Boolean = delegate.isRange
override def source: SourceFile = delegate.source
override def remove[T](
implicit evidence$5: ClassManifest[T]
): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.remove(evidence$5)
override def withStart(start: Int): Position = DelegatingPosition.wrap(delegate.withStart(start), colorsConfig)
@scala.deprecated("use `lineCaret`")
override def lineWithCarat(maxWidth: Int): (String, String) = delegate.lineWithCarat(maxWidth)
override def start: Int = delegate.start
override def withPoint(point: Int): Position = DelegatingPosition.wrap(delegate.withPoint(point), colorsConfig)
override def point: Int = delegate.point
override def isEmpty: Boolean = delegate.isEmpty
override def end: Int = delegate.end
override def withEnd(end: Int): Position = DelegatingPosition.wrap(delegate.withEnd(end), colorsConfig)
@scala.deprecated("Use `withSource(source)` and `withShift`")
override def withSource(source: SourceFile, shift: Int): Position =
DelegatingPosition.wrap(delegate.withSource(source, shift), colorsConfig)
override def withSource(source: SourceFile): Position =
DelegatingPosition.wrap(delegate.withSource(source), colorsConfig)
@scala.deprecated("Use `start` instead")
override def startOrPoint: Int = delegate.startOrPoint
override def withShift(shift: Int): Position = DelegatingPosition.wrap(delegate.withShift(shift), colorsConfig)
@scala.deprecated("Use `end` instead")
override def endOrPoint: Int = delegate.endOrPoint
override def focusStart: Position = DelegatingPosition.wrap(delegate.focusStart, colorsConfig)
override def focus: Position = DelegatingPosition.wrap(delegate.focus, colorsConfig)
override def focusEnd: Position = DelegatingPosition.wrap(delegate.focusEnd, colorsConfig)
override def |(that: Position, poses: Position*): Position =
DelegatingPosition.wrap(delegate.|(that, poses: _*), colorsConfig)
override def |(that: Position): Position = DelegatingPosition.wrap(delegate.|(that), colorsConfig)
override def ^(point: Int): Position = DelegatingPosition.wrap(delegate.^(point), colorsConfig)
override def |^(that: Position): Position = DelegatingPosition.wrap(delegate.|^(that), colorsConfig)
override def ^|(that: Position): Position = DelegatingPosition.wrap(delegate.^|(that), colorsConfig)
override def union(pos: Position): Position = DelegatingPosition.wrap(delegate.union(pos), colorsConfig)
override def includes(pos: Position): Boolean = delegate.includes(pos)
override def properlyIncludes(pos: Position): Boolean = delegate.properlyIncludes(pos)
override def precedes(pos: Position): Boolean = delegate.precedes(pos)
override def properlyPrecedes(pos: Position): Boolean = delegate.properlyPrecedes(pos)
override def sameRange(pos: Position): Boolean = delegate.sameRange(pos)
override def overlaps(pos: Position): Boolean = delegate.overlaps(pos)
override def line: Int = delegate.line
override def column: Int = delegate.column
override def lineCaret: String = delegate.lineCaret
@scala.deprecated("use `lineCaret`")
override def lineCarat: String = delegate.lineCarat
override def showDebug: String = delegate.showDebug
override def show: String = delegate.show
}
object DelegatingPosition {
def wrap(pos: Position, colorsConfig: ColorsConfig): Position =
pos match {
case NoPosition => pos
case wrapped: DelegatingPosition => wrapped
case _ => new DelegatingPosition(pos, colorsConfig)
}
}
================================================
FILE: plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingReporter.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.Position
import scala.tools.nsc.reporters.Reporter
class DelegatingReporter(r: Reporter, handleError: (Position, String) => String, colorsConfig: ColorsConfig)
extends Reporter {
override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
// cannot delegate to info0 as it's protected, hence special-casing on the possible severity values
if (severity == INFO) {
r.info(wrapped, msg, force)
} else if (severity == WARNING) {
warning(wrapped, msg)
} else if (severity == ERROR) {
error(wrapped, msg)
} else {
error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg")
}
}
override def echo(msg: String) = r.echo(msg)
override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def hasErrors = r.hasErrors || cancelled
override def reset() = {
cancelled = false
r.reset()
}
//
override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def warning(pos: Position, msg: String) = r.warning(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def errorCount = r.errorCount
override def warningCount = r.warningCount
override def hasWarnings = r.hasWarnings
override def flush() = r.flush()
override def count(severity: Severity): Int = r.count(conv(severity))
override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity))
//
private def conv(s: Severity): r.Severity = s match {
case INFO => r.INFO
case WARNING => r.WARNING
case ERROR => r.ERROR
}
//
override def error(pos: Position, msg: String) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
r.error(wrapped, handleError(wrapped, msg))
}
}
================================================
FILE: plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingPosition.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.{NoPosition, Position, SourceFile}
import scala.reflect.macros.Attachments
class DelegatingPosition(delegate: Position, colorsConfig: ColorsConfig) extends Position {
// used by scalac to report errors
override def showError(msg: String): String = highlight(delegate.showError(msg))
// used by sbt
override def lineContent: String = highlight(delegate.lineContent)
def highlight(str: String): String = colorsConfig match {
case e: ColorsConfig.Enabled =>
Highlighter
.defaultHighlight(
str.toVector,
e.comment,
e.`type`,
e.literal,
e.keyword,
e.reset
)
.mkString
case _ => str
}
// impl copied
override def fail(what: String): Nothing = throw new UnsupportedOperationException(s"Position.$what on $this")
// simple delegates with position wrapping when position is returned
@scala.deprecated("use `point`")
override def offset: Option[Int] = delegate.offset
override def all: Set[Any] = delegate.all
@scala.deprecated("use `focus`")
override def toSingleLine: Position = DelegatingPosition.wrap(delegate.toSingleLine, colorsConfig)
override def pos: Position = DelegatingPosition.wrap(delegate.pos, colorsConfig)
override def get[T](implicit evidence$2: ClassManifest[T]): Option[T] = delegate.get(evidence$2)
override def finalPosition: Pos = DelegatingPosition.wrap(delegate.finalPosition, colorsConfig)
override def withPos(newPos: Position): Attachments { type Pos = DelegatingPosition.this.Pos } =
delegate.withPos(newPos)
@scala.deprecated("use `line`")
override def safeLine: Int = delegate.safeLine
override def isTransparent: Boolean = delegate.isTransparent
override def contains[T](implicit evidence$3: ClassManifest[T]): Boolean = delegate.contains(evidence$3)
override def isOffset: Boolean = delegate.isOffset
@scala.deprecated("use `showDebug`")
override def dbgString: String = delegate.dbgString
override def isOpaqueRange: Boolean = delegate.isOpaqueRange
override def update[T](attachment: T)(
implicit evidence$4: ClassManifest[T]
): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.update(attachment)(evidence$4)
override def pointOrElse(alt: Int): Int = delegate.pointOrElse(alt)
@scala.deprecated("use `finalPosition`")
override def inUltimateSource(source: SourceFile): Position =
DelegatingPosition.wrap(delegate.inUltimateSource(source), colorsConfig)
override def isDefined: Boolean = delegate.isDefined
override def makeTransparent: Position = DelegatingPosition.wrap(delegate.makeTransparent, colorsConfig)
override def isRange: Boolean = delegate.isRange
override def source: SourceFile = delegate.source
override def remove[T](
implicit evidence$5: ClassManifest[T]
): Attachments { type Pos = DelegatingPosition.this.Pos } = delegate.remove(evidence$5)
override def withStart(start: Int): Position = DelegatingPosition.wrap(delegate.withStart(start), colorsConfig)
@scala.deprecated("use `lineCaret`")
override def lineWithCarat(maxWidth: Int): (String, String) = delegate.lineWithCarat(maxWidth)
override def start: Int = delegate.start
override def withPoint(point: Int): Position = DelegatingPosition.wrap(delegate.withPoint(point), colorsConfig)
override def point: Int = delegate.point
override def isEmpty: Boolean = delegate.isEmpty
override def end: Int = delegate.end
override def withEnd(end: Int): Position = DelegatingPosition.wrap(delegate.withEnd(end), colorsConfig)
@scala.deprecated("Use `withSource(source)` and `withShift`")
override def withSource(source: SourceFile, shift: Int): Position =
DelegatingPosition.wrap(delegate.withSource(source, shift), colorsConfig)
override def withSource(source: SourceFile): Position =
DelegatingPosition.wrap(delegate.withSource(source), colorsConfig)
@scala.deprecated("Use `start` instead")
override def startOrPoint: Int = delegate.startOrPoint
override def withShift(shift: Int): Position = DelegatingPosition.wrap(delegate.withShift(shift), colorsConfig)
@scala.deprecated("Use `end` instead")
override def endOrPoint: Int = delegate.endOrPoint
override def focusStart: Position = DelegatingPosition.wrap(delegate.focusStart, colorsConfig)
override def focus: Position = DelegatingPosition.wrap(delegate.focus, colorsConfig)
override def focusEnd: Position = DelegatingPosition.wrap(delegate.focusEnd, colorsConfig)
override def |(that: Position, poses: Position*): Position =
DelegatingPosition.wrap(delegate.|(that, poses: _*), colorsConfig)
override def |(that: Position): Position = DelegatingPosition.wrap(delegate.|(that), colorsConfig)
override def ^(point: Int): Position = DelegatingPosition.wrap(delegate.^(point), colorsConfig)
override def |^(that: Position): Position = DelegatingPosition.wrap(delegate.|^(that), colorsConfig)
override def ^|(that: Position): Position = DelegatingPosition.wrap(delegate.^|(that), colorsConfig)
override def union(pos: Position): Position = DelegatingPosition.wrap(delegate.union(pos), colorsConfig)
override def includes(pos: Position): Boolean = delegate.includes(pos)
override def properlyIncludes(pos: Position): Boolean = delegate.properlyIncludes(pos)
override def precedes(pos: Position): Boolean = delegate.precedes(pos)
override def properlyPrecedes(pos: Position): Boolean = delegate.properlyPrecedes(pos)
override def sameRange(pos: Position): Boolean = delegate.sameRange(pos)
override def overlaps(pos: Position): Boolean = delegate.overlaps(pos)
override def line: Int = delegate.line
override def column: Int = delegate.column
override def lineCaret: String = delegate.lineCaret
@scala.deprecated("use `lineCaret`")
override def lineCarat: String = delegate.lineCarat
override def showDebug: String = delegate.showDebug
override def show: String = delegate.show
}
object DelegatingPosition {
def wrap(pos: Position, colorsConfig: ColorsConfig): Position =
pos match {
case NoPosition => pos
case wrapped: DelegatingPosition => wrapped
case _ => new DelegatingPosition(pos, colorsConfig)
}
}
================================================
FILE: plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingReporter.scala
================================================
package com.softwaremill.clippy
import scala.reflect.internal.util.Position
import scala.tools.nsc.reporters.Reporter
class DelegatingReporter(r: Reporter, handleError: (Position, String) => String, colorsConfig: ColorsConfig)
extends Reporter {
override protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
// cannot delegate to info0 as it's protected, hence special-casing on the possible severity values
if (severity == INFO) {
r.info(wrapped, msg, force)
} else if (severity == WARNING) {
warning(wrapped, msg)
} else if (severity == ERROR) {
error(wrapped, msg)
} else {
error(wrapped, s"UNKNOWN SEVERITY: $severity\n$msg")
}
}
override def echo(msg: String) = r.echo(msg)
override def comment(pos: Position, msg: String) = r.comment(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def hasErrors = r.hasErrors || cancelled
override def reset() = {
cancelled = false
r.reset()
}
//
override def echo(pos: Position, msg: String) = r.echo(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def warning(pos: Position, msg: String) = r.warning(DelegatingPosition.wrap(pos, colorsConfig), msg)
override def errorCount = r.errorCount
override def warningCount = r.warningCount
override def hasWarnings = r.hasWarnings
override def flush() = r.flush()
override def count(severity: Severity): Int = r.count(conv(severity))
override def resetCount(severity: Severity): Unit = r.resetCount(conv(severity))
//
private def conv(s: Severity): r.Severity = s match {
case INFO => r.INFO
case WARNING => r.WARNING
case ERROR => r.ERROR
}
//
override def error(pos: Position, msg: String) = {
val wrapped = DelegatingPosition.wrap(pos, colorsConfig)
r.error(wrapped, handleError(wrapped, msg))
}
}
================================================
FILE: plugin-sbt/src/main/scala/com/softwaremill/clippy/ClippySbtPlugin.scala
================================================
package com.softwaremill.clippy
import sbt._
import sbt.Keys._
import scala.collection.mutable.ListBuffer
object ClippySbtPlugin extends AutoPlugin {
object ClippyColor extends Enumeration {
val Black = Value("black")
val LightGray = Value("light-gray")
val DarkGray = Value("dark-gray")
val Red = Value("red")
val LightRed = Value("light-red")
val Green = Value("green")
val LightGreen = Value("light-green")
val Yellow = Value("yellow")
val LightYellow = Value("light-yellow")
val Blue = Value("blue")
val LightBlue = Value("light-blue")
val Magenta = Value("magenta")
val LightMagenta = Value("light-magenta")
val Cyan = Value("cyan")
val LightCyan = Value("light-cyan")
val White = Value("white")
val None = Value("none")
}
object WarningPatterns {
val NonExhaustiveMatch = "match may not be exhaustive[\\s\\S]*"
}
object autoImport {
val clippyColorsEnabled = settingKey[Boolean]("Should Clippy color type mismatch diffs and highlight syntax")
val clippyColorDiff = settingKey[Option[ClippyColor.Value]]("The color to use for diffs, if other than default")
val clippyColorComment =
settingKey[Option[ClippyColor.Value]]("The color to use for comments, if other than default")
val clippyColorType = settingKey[Option[ClippyColor.Value]]("The color to use for types, if other than default")
val clippyColorLiteral =
settingKey[Option[ClippyColor.Value]]("The color to use for literals, if other than default")
val clippyColorKeyword =
settingKey[Option[ClippyColor.Value]]("The color to use for keywords, if other than default")
val clippyColorReset =
settingKey[Option[ClippyColor.Value]]("The color to use for resetting to neutral, if other than default")
val clippyUrl = settingKey[Option[String]]("Url from which to fetch advice, if other than default")
val clippyLocalStoreDir =
settingKey[Option[String]]("Directory where cached advice data should be stored, if other than default")
val clippyProjectRoot =
settingKey[Option[String]]("Project root in which project-specific advice is stored, if any")
val clippyFatalWarnings =
settingKey[List[String]]("Regular expressions of warning messages which should fail compilation")
val NonExhaustiveMatch = "match may not be exhaustive[\\s\\S]*"
}
// in ~/.sbt auto import doesn't work, so providing aliases here for convenience
val clippyColorsEnabled = autoImport.clippyColorsEnabled
val clippyColorDiff = autoImport.clippyColorDiff
val clippyColorComment = autoImport.clippyColorComment
val clippyColorType = autoImport.clippyColorType
val clippyColorLiteral = autoImport.clippyColorLiteral
val clippyColorKeyword = autoImport.clippyColorKeyword
val clippyColorReset = autoImport.clippyColorReset
val clippyUrl = autoImport.clippyUrl
val clippyLocalStoreDir = autoImport.clippyLocalStoreDir
val clippyProjectRoot = autoImport.clippyProjectRoot
val clippyFatalWarnings = autoImport.clippyFatalWarnings
override def projectSettings = Seq(
clippyColorsEnabled := false,
clippyColorDiff := None,
clippyColorComment := None,
clippyColorType := None,
clippyColorLiteral := None,
clippyColorKeyword := None,
clippyColorReset := None,
clippyUrl := None,
clippyLocalStoreDir := None,
clippyProjectRoot := None,
clippyFatalWarnings := Nil,
addCompilerPlugin("com.softwaremill.clippy" %% "plugin" % ClippyBuildInfo.version classifier "bundle"),
scalacOptions := {
val result = ListBuffer(scalacOptions.value: _*)
if (clippyColorsEnabled.value) result += "-P:clippy:colors=true"
clippyColorDiff.value.foreach(c => result += s"-P:clippy:colors-diff=$c")
clippyColorComment.value.foreach(c => result += s"-P:clippy:colors-comment=$c")
clippyColorType.value.foreach(c => result += s"-P:clippy:colors-type=$c")
clippyColorLiteral.value.foreach(c => result += s"-P:clippy:colors-literal=$c")
clippyColorKeyword.value.foreach(c => result += s"-P:clippy:colors-keyword=$c")
clippyColorReset.value.foreach(c => result += s"-P:clippy:colors-reset=$c")
clippyUrl.value.foreach(c => result += s"-P:clippy:url=$c")
clippyLocalStoreDir.value.foreach(c => result += s"-P:clippy:store=$c")
clippyProjectRoot.value.foreach(c => result += s"-P:clippy:projectRoot=$c")
if (clippyFatalWarnings.value.nonEmpty)
result += s"-P:clippy:fatalWarnings=${clippyFatalWarnings.value.mkString("|")}"
result.toList
}
)
override def trigger = allRequirements
}
================================================
FILE: project/build.properties
================================================
sbt.version=0.13.17
================================================
FILE: project/plugins.sbt
================================================
resolvers += Resolver.typesafeRepo("releases")
// Workaround for the bug: https://github.com/sbt/sbt-assembly/issues/236
resolvers += "JBoss" at "https://repository.jboss.org"
addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.4.0")
addSbtPlugin("com.updateimpact" % "updateimpact-sbt-plugin" % "2.1.1")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.4.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1")
addSbtPlugin("com.heroku" % "sbt-heroku" % "0.5.4")
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.6.8")
// The Play plugin
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.6")
// web plugins
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0")
// scalajs
addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.3")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.15")
================================================
FILE: tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala
================================================
package org.softwaremill.clippy
import java.io.{File, FileOutputStream}
import java.util.zip.GZIPOutputStream
import scala.reflect.runtime.currentMirror
import com.softwaremill.clippy._
import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers}
import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBoxError
class CompileTests extends FlatSpec with Matchers with BeforeAndAfterAll {
val localStoreDir = new File(System.getProperty("user.home"), ".clippy")
val localStore = new File(localStoreDir, "clippy.json.gz")
val localStore2 = new File(localStoreDir, "clippy2.json.gz")
/**
* Writing test json data to where the plugin will expect to have it cached.
*/
override protected def beforeAll() = {
super.beforeAll()
localStoreDir.mkdirs()
if (localStore.exists()) {
localStore.renameTo(localStore2)
}
val advices = List(
Advice(
TypeMismatchError(ExactT("slick.dbio.DBIOAction[*]"), None, ExactT("slick.lifted.Rep[Option[*]]"), None, None).asRegex,
"Perhaps you forgot to call .result on your Rep[]? This will give you a DBIOAction that you can compose with other DBIOActions.",
Library("com.typesafe.slick", "slick", "3.1.0")
),
Advice(
TypeMismatchError(
ExactT("akka.http.scaladsl.server.StandardRoute"),
None,
ExactT(
"akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]"
),
None,
None
).asRegex,
"did you forget to define an implicit akka.stream.ActorMaterializer? It allows routes to be converted into a flow. You can read more at http://doc.akka.io/docs/akka-stream-and-http-experimental/2.0/scala/http/routing-dsl/index.html",
Library("com.typesafe.akka", "akka-http-experimental", "2.0.0")
),
Advice(
NotFoundError(ExactT("value wire")).asRegex,
"you need to import com.softwaremill.macwire._",
Library("com.softwaremill.macwire", "macros", "2.0.0")
),
Advice(
NotFoundError(ExactT("value wire")).asRegex,
"If you need further help check out the macwire readme at https://github.com/adamw/macwire",
Library("com.softwaremill.macwire", "macros", "2.0.0")
),
Advice(
TypeArgumentsDoNotConformToOverloadedBoundsError(
ExactT("*"),
ExactT("value apply"),
Set(
ExactT("[E <: slick.lifted.AbstractTable[_]]=> slick.lifted.TableQuery[E]"),
ExactT("[E <: slick.lifted.AbstractTable[_]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]")
)
).asRegex,
"incorrect class name passed to TableQuery",
Library("com.typesafe.slick", "slick", "3.1.1")
),
Advice(
TypeclassNotFoundError(
ExactT("Ordering"),
ExactT("java.time.LocalDate")
).asRegex,
"implicit val localDateOrdering: Ordering[java.time.LocalDate] = Ordering.by(_.toEpochDay)",
Library("java-lang", "time", "8+")
)
)
import org.json4s.native.JsonMethods._
val data = compact(render(Clippy("0.1", advices, Nil).toJson))
val os = new GZIPOutputStream(new FileOutputStream(localStore))
try os.write(data.getBytes("UTF-8"))
finally os.close()
}
override protected def afterAll() = {
localStore.delete()
if (localStore2.exists()) {
localStore2.renameTo(localStore)
}
super.afterAll()
}
val snippets = Map(
"akka http" ->
"""
|import akka.actor.ActorSystem
|import akka.http.scaladsl.Http
|
|import akka.http.scaladsl.server.Directives._
|
|implicit val system = ActorSystem()
|
|val r = complete("ok")
|
|Http().bindAndHandle(r, "localhost", 8080)
""".stripMargin,
"macwire" ->
"""
|class A()
|val a = wire[A]
""".stripMargin,
"slick" ->
"""
|case class User(id1: Long, id2: Long)
|trait TestSchema {
|
| val db: slick.jdbc.JdbcBackend#DatabaseDef
| val driver: slick.driver.JdbcProfile
|
| import driver.api._
|
| protected val users = TableQuery[User]
|
| protected class Users(tag: Tag) extends Table[User](tag, "users") {
| def id1 = column[Long]("id")
| def id2 = column[Long]("id")
|
| def * = (id1, id2) <> (User.tupled, User.unapply)
| }
|}
""".stripMargin,
"Type mismatch pretty diff" ->
"""
|class Test {
|
| type Cool = (String, String, Int, Option[String], Long)
| type Bool = (String, String, Int, String, Long)
|
| def test(cool: Cool): Bool = cool
|
|}
""".stripMargin
)
val tb = {
val cpp = sys.env("CLIPPY_PLUGIN_PATH")
currentMirror.mkToolBox(
options = s"-Xplugin:$cpp -Xplugin-require:clippy -P:clippy:colors=true -P:clippy:testmode=true"
)
}
def tryCompile(snippet: String) = tb.compile(tb.parse(snippet))
for ((name, s) <- snippets) {
name should "compile with errors" in {
(the[ToolBoxError] thrownBy tryCompile(s)).message should include("Clippy advises")
}
}
"Clippy" should "return all matching advice" in {
(the[ToolBoxError] thrownBy tryCompile(snippets("macwire"))).message should include(
"Clippy advises you to try one of these"
)
}
}
================================================
FILE: ui/app/ClippyApplicationLoader.scala
================================================
import api.UiApiImpl
import com.softwaremill.id.DefaultIdGenerator
import controllers._
import dal.AdvicesRepository
import play.api.ApplicationLoader.Context
import play.api._
import play.api.i18n.I18nComponents
import play.api.mvc.EssentialFilter
import router.Routes
import util.{DatabaseConfig, SqlDatabase}
import util.email.{DummyEmailService, SendgridEmailService}
class ClippyApplicationLoader extends ApplicationLoader {
def load(context: Context) = {
Logger.configure(context.environment)
val c = new ClippyComponents(context)
c.database.updateSchema()
c.application
}
}
class ClippyComponents(context: Context) extends BuiltInComponentsFromContext(context) with I18nComponents {
implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
lazy val router =
new Routes(httpErrorHandler, applicationController, assets, webJarAssets, advicesController, autowireController)
lazy val contactEmail = configuration.getString("email.contact").getOrElse("?")
lazy val emailService = SendgridEmailService
.createFromEnv(contactEmail)
.getOrElse(new DummyEmailService)
lazy val webJarAssets = new WebJarAssets(httpErrorHandler, configuration, environment)
lazy val assets = new controllers.Assets(httpErrorHandler)
lazy val idGenerator = new DefaultIdGenerator()
lazy val applicationController = new ApplicationController()
lazy val database = SqlDatabase.create(new DatabaseConfig { override val rootConfig = configuration.underlying })
lazy val advicesRepository = new AdvicesRepository(database, idGenerator)
lazy val uiApiImpl = new UiApiImpl(advicesRepository, emailService, contactEmail)
lazy val autowireController = new AutowireController(uiApiImpl)
lazy val advicesController = new AdvicesController(advicesRepository)
override lazy val httpFilters: Seq[EssentialFilter] = List(new HttpsFilter())
}
================================================
FILE: ui/app/api/UiApiImpl.scala
================================================
package api
import com.softwaremill.clippy._
import dal.AdvicesRepository
import util.email.EmailService
import scala.concurrent.{ExecutionContext, Future}
class UiApiImpl(
advicesRepository: AdvicesRepository,
emailService: EmailService,
contactEmail: String
)(implicit ec: ExecutionContext)
extends UiApi {
override def sendCannotParse(errorText: String, contributorEmail: String) =
emailService.send(
contactEmail,
"Clippy: unparseable message",
s"""
|Contributor email: $contributorEmail
|
|Error text:
|$errorText
""".stripMargin
)
override def sendAdviceProposal(ap: AdviceProposal): Future[Unit] =
advicesRepository
.store(
ap.errorTextRaw,
ap.patternRaw,
ap.compilationError,
ap.advice,
AdviceState.Pending,
ap.library,
ap.contributor,
ap.comment
)
.flatMap { a =>
emailService.send(contactEmail, "Clippy: new advice proposal", s"""
|Advice proposal:
|$a
|""".stripMargin)
}
override def listAccepted() =
advicesRepository.findAll().map(_.map(_.toAdviceListing))
override def sendSuggestEdit(text: String, contactEmail: String, adviceListing: AdviceListing) =
emailService.send(
contactEmail,
"Clippy: edit suggestion",
s"""
|Edit suggestion for: $adviceListing
|Contact email: $contactEmail
|
|Suggestion:
|$text
""".stripMargin
)
override def feedback(text: String, contactEmail: String) =
emailService.send(contactEmail, "Clippy: feedback", s"""
|Contact email: $contactEmail
|
|Feedback:
|$text
""".stripMargin)
}
================================================
FILE: ui/app/assets/stylesheets/main.less
================================================
#reactmain {
padding-top: 60px;
}
html {
position: relative;
min-height: 100%;
}
.cout {
width: 100%;
padding-top: 5px;
margin-bottom: 5px;
}
body {
margin-bottom: 70px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
background-color: #f5f5f5;
margin-top: 10px;
}
.glyphicon-edit {
cursor: pointer;
}
.advice-listing {
table-layout: fixed;
word-wrap: break-word;
}
================================================
FILE: ui/app/controllers/AdvicesController.scala
================================================
package controllers
import com.softwaremill.clippy.{Advice, Clippy}
import dal.AdvicesRepository
import play.api.mvc.{Action, Controller}
import util.{ClippyBuildInfo, Zip}
import scala.concurrent.{ExecutionContext, Future}
class AdvicesController(advicesRepository: AdvicesRepository)(implicit ec: ExecutionContext) extends Controller {
def get = Action.async {
gzippedAdvices.map(a => Ok(a).withHeaders("Content-Encoding" -> "gzip"))
}
def gzippedAdvices: Future[Array[Byte]] =
advicesRepository.findAll().map { storedAdvices =>
// TODO, once there's an admin: filter out not accepted advice
val advices = storedAdvices
//.filter(_.accepted)
.map(_.toAdvice)
Zip.compress(toJsonString(advices.toList))
}
private def toJsonString(advices: List[Advice]): String = {
import org.json4s.native.JsonMethods.{render => r, compact}
compact(r(Clippy(ClippyBuildInfo.version, advices, Nil).toJson))
}
}
================================================
FILE: ui/app/controllers/ApplicationController.scala
================================================
package controllers
import play.api.mvc.{Action, Controller}
class ApplicationController extends Controller {
def index = Action {
Ok(views.html.index())
}
}
================================================
FILE: ui/app/controllers/AutowireController.scala
================================================
package controllers
import com.softwaremill.clippy.UiApi
import play.api.mvc.{Action, Controller}
import upickle.default._
import upickle.Js
import scala.concurrent.ExecutionContext
class AutowireController(uiApi: UiApi)(implicit ec: ExecutionContext) extends Controller {
def autowireApi(path: String) = Action.async { implicit request =>
val b = request.body.asText.getOrElse("")
AutowireServer
.route[UiApi](uiApi)(
autowire.Core.Request(
path.split("/"),
upickle.json.read(b).asInstanceOf[Js.Obj].value.toMap
)
)
.map(jsv => Ok(upickle.json.write(jsv)))
}
}
object AutowireServer extends autowire.Server[Js.Value, Reader, Writer] {
def read[Result: Reader](p: Js.Value) = upickle.default.readJs[Result](p)
def write[Result: Writer](r: Result) = upickle.default.writeJs(r)
}
================================================
FILE: ui/app/controllers/HttpsFilter.scala
================================================
package controllers
import play.api.http.Status
import play.api.mvc.{Filter, RequestHeader, Result, Results}
import scala.concurrent.Future
class HttpsFilter extends Filter {
def apply(nextFilter: (RequestHeader) => Future[Result])(requestHeader: RequestHeader): Future[Result] =
requestHeader.headers.get("x-forwarded-proto") match {
case Some(header) =>
if (header == "https") {
nextFilter(requestHeader)
} else {
Future.successful(
Results.Redirect("https://" + requestHeader.host + requestHeader.uri, Status.MOVED_PERMANENTLY)
)
}
case None => nextFilter(requestHeader)
}
}
================================================
FILE: ui/app/dal/AdvicesRepository.scala
================================================
package dal
import com.softwaremill.clippy.AdviceState.AdviceState
import com.softwaremill.clippy._
import com.softwaremill.id.IdGenerator
import util.SqlDatabase
import scala.concurrent.{ExecutionContext, Future}
class AdvicesRepository(database: SqlDatabase, idGenerator: IdGenerator)(implicit ec: ExecutionContext) {
import database._
import database.driver.api._
private class AdvicesTable(tag: Tag) extends Table[StoredAdvice](tag, "advices") {
def id = column[Long]("id", O.PrimaryKey)
def errorTextRaw = column[String]("error_text_raw")
def patternRaw = column[String]("pattern_raw")
def compilationError = column[String]("compilation_error")
def advice = column[String]("advice")
def state = column[Int]("state")
def libraryGroupId = column[String]("library_group_id")
def libraryArtifactId = column[String]("library_artifact_id")
def libraryVersion = column[String]("library_version")
def contributorEmail = column[Option[String]]("contributor_email")
def contributorTwitter = column[Option[String]]("contributor_twitter")
def contributorGithub = column[Option[String]]("contributor_github")
def comment = column[Option[String]]("comment")
def * =
(
id,
errorTextRaw,
patternRaw,
compilationError,
advice,
state,
(libraryGroupId, libraryArtifactId, libraryVersion),
(contributorEmail, contributorGithub, contributorTwitter),
comment
).shaped <> ({ t =>
StoredAdvice(
t._1,
t._2,
t._3,
CompilationError.fromJsonString(t._4).get,
t._5,
AdviceState(t._6),
(Library.apply _).tupled(t._7),
Contributor.tupled(t._8),
t._9
)
}, { (a: StoredAdvice) =>
Some(
(
a.id,
a.errorTextRaw,
a.patternRaw,
a.compilationError.toJsonString,
a.advice,
a.state.id,
Library.unapply(a.library).get,
Contributor.unapply(a.contributor).get,
a.comment
)
)
})
}
private val advices = TableQuery[AdvicesTable]
def store(
errorTextRaw: String,
patternRaw: String,
compilationError: CompilationError[RegexT],
advice: String,
state: AdviceState,
library: Library,
contributor: Contributor,
comment: Option[String]
): Future[StoredAdvice] = {
val a = StoredAdvice(
idGenerator.nextId(),
errorTextRaw,
patternRaw,
compilationError,
advice,
state,
library,
contributor,
comment
)
db.run(advices += a).map(_ => a)
}
def findAll(): Future[Seq[StoredAdvice]] =
db.run(advices.result)
}
case class StoredAdvice(
id: Long,
errorTextRaw: String,
patternRaw: String,
compilationError: CompilationError[RegexT],
advice: String,
state: AdviceState,
library: Library,
contributor: Contributor,
comment: Option[String]
) {
def toAdvice = Advice(compilationError, advice, library)
def toAdviceListing =
AdviceListing(id, compilationError, advice, library, ContributorListing(contributor.github, contributor.twitter))
}
================================================
FILE: ui/app/util/ConfigWithDefault.scala
================================================
package util
import java.util.concurrent.TimeUnit
import com.typesafe.config.Config
trait ConfigWithDefault {
def rootConfig: Config
def getBoolean(path: String, default: Boolean) = ifHasPath(path, default) { _.getBoolean(path) }
def getString(path: String, default: String) = ifHasPath(path, default) { _.getString(path) }
def getInt(path: String, default: Int) = ifHasPath(path, default) { _.getInt(path) }
def getConfig(path: String, default: Config) = ifHasPath(path, default) { _.getConfig(path) }
def getMilliseconds(path: String, default: Long) = ifHasPath(path, default) {
_.getDuration(path, TimeUnit.MILLISECONDS)
}
def getOptionalString(path: String, default: Option[String] = None) = getOptional(path) { _.getString(path) }
private def ifHasPath[T](path: String, default: T)(get: Config => T): T =
if (rootConfig.hasPath(path)) get(rootConfig) else default
private def getOptional[T](fullPath: String, default: Option[T] = None)(get: Config => T) =
if (rootConfig.hasPath(fullPath)) {
Some(get(rootConfig))
} else {
default
}
}
================================================
FILE: ui/app/util/DatabaseConfig.scala
================================================
package util
import com.typesafe.config.Config
trait DatabaseConfig extends ConfigWithDefault {
def rootConfig: Config
import DatabaseConfig._
lazy val dbH2Url = getString(s"db.h2.properties.url", "jdbc:h2:file:./data")
lazy val dbPostgresServerName = getString(PostgresServerNameKey, "")
lazy val dbPostgresPort = getString(PostgresPortKey, "5432")
lazy val dbPostgresDbName = getString(PostgresDbNameKey, "")
lazy val dbPostgresUsername = getString(PostgresUsernameKey, "")
lazy val dbPostgresPassword = getString(PostgresPasswordKey, "")
}
object DatabaseConfig {
val PostgresDSClass = "db.postgres.dataSourceClass"
val PostgresServerNameKey = "db.postgres.properties.serverName"
val PostgresPortKey = "db.postgres.properties.portNumber"
val PostgresDbNameKey = "db.postgres.properties.databaseName"
val PostgresUsernameKey = "db.postgres.properties.user"
val PostgresPasswordKey = "db.postgres.properties.password"
}
================================================
FILE: ui/app/util/SqlDatabase.scala
================================================
package util
import java.net.URI
import com.typesafe.config.ConfigValueFactory._
import com.typesafe.config.{Config, ConfigFactory}
import com.typesafe.scalalogging.StrictLogging
import org.flywaydb.core.Flyway
import slick.driver.JdbcProfile
import slick.jdbc.JdbcBackend._
case class SqlDatabase(
db: slick.jdbc.JdbcBackend#Database,
driver: JdbcProfile,
connectionString: JdbcConnectionString
) {
def updateSchema() {
val flyway = new Flyway()
flyway.setDataSource(connectionString.url, connectionString.username, connectionString.password)
flyway.migrate()
}
def close() {
db.close()
}
}
case class JdbcConnectionString(url: String, username: String = "", password: String = "")
object SqlDatabase extends StrictLogging {
def create(config: DatabaseConfig): SqlDatabase = {
val envDatabaseUrl = System.getenv("DATABASE_URL")
if (config.dbPostgresServerName.length > 0)
createPostgresFromConfig(config)
else if (envDatabaseUrl != null)
createPostgresFromEnv(envDatabaseUrl)
else
createEmbedded(config)
}
def createEmbedded(connectionString: String): SqlDatabase = {
val db = Database.forURL(connectionString)
SqlDatabase(db, slick.driver.H2Driver, JdbcConnectionString(connectionString))
}
private def createPostgresFromEnv(envDatabaseUrl: String) = {
import DatabaseConfig._
/*
The DATABASE_URL is set by Heroku (if deploying there) and must be converted to a proper object
of type Config (for Slick). Expected format:
postgres://<username>:<password>@<host>:<port>/<dbname>
*/
val dbUri = new URI(envDatabaseUrl)
val username = dbUri.getUserInfo.split(":")(0)
val password = dbUri.getUserInfo.split(":")(1)
val intermediaryConfig = new DatabaseConfig {
override def rootConfig: Config =
ConfigFactory
.empty()
.withValue(PostgresDSClass, fromAnyRef("org.postgresql.ds.PGSimpleDataSource"))
.withValue(PostgresServerNameKey, fromAnyRef(dbUri.getHost))
.withValue(PostgresPortKey, fromAnyRef(dbUri.getPort))
.withValue(PostgresDbNameKey, fromAnyRef(dbUri.getPath.tail))
.withValue(PostgresUsernameKey, fromAnyRef(username))
.withValue(PostgresPasswordKey, fromAnyRef(password))
.withFallback(ConfigFactory.load())
}
createPostgresFromConfig(intermediaryConfig)
}
private def postgresUrl(host: String, port: String, dbName: String) =
s"jdbc:postgresql://$host:$port/$dbName"
private def postgresConnectionString(config: DatabaseConfig) = {
val host = config.dbPostgresServerName
val port = config.dbPostgresPort
val dbName = config.dbPostgresDbName
val username = config.dbPostgresUsername
val password = config.dbPostgresPassword
JdbcConnectionString(postgresUrl(host, port, dbName), username, password)
}
private def createPostgresFromConfig(config: DatabaseConfig) = {
val db = Database.forConfig("db.postgres", config.rootConfig)
SqlDatabase(db, slick.driver.PostgresDriver, postgresConnectionString(config))
}
private def createEmbedded(config: DatabaseConfig): SqlDatabase = {
val db = Database.forConfig("db.h2")
SqlDatabase(db, slick.driver.H2Driver, JdbcConnectionString(embeddedConnectionStringFromConfig(config)))
}
private def embeddedConnectionStringFromConfig(config: DatabaseConfig): String = {
val url = config.dbH2Url
val fullPath = url.split(":")(3)
logger.info(s"Using an embedded database, with data files located at: $fullPath")
url
}
}
================================================
FILE: ui/app/util/Zip.scala
================================================
package util
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.util.zip.{GZIPInputStream, GZIPOutputStream}
object Zip {
private val BufferSize = 512
def compress(string: String): Array[Byte] = {
val os = new ByteArrayOutputStream(string.length() / 5)
val gos = new GZIPOutputStream(os)
gos.write(string.getBytes("UTF-8"))
gos.close()
os.close()
os.toByteArray
}
def decompress(compressed: Array[Byte]): String = {
val is = new ByteArrayInputStream(compressed)
val gis = new GZIPInputStream(is, BufferSize)
val string = new StringBuilder()
val data = new Array[Byte](BufferSize)
var bytesRead = gis.read(data)
while (bytesRead != -1) {
string.append(new String(data, 0, bytesRead, "UTF-8"))
bytesRead = gis.read(data)
}
gis.close()
is.close()
string.toString()
}
}
================================================
FILE: ui/app/util/email/DummyEmailService.scala
================================================
package util.email
import com.typesafe.scalalogging.StrictLogging
import scala.collection.mutable.ListBuffer
import scala.concurrent.Future
class DummyEmailService extends EmailService with StrictLogging {
private val sentEmails: ListBuffer[(String, String, String)] = ListBuffer()
logger.info("Using dummy email service")
def reset() {
sentEmails.clear()
}
override def send(to: String, subject: String, body: String) = {
this.synchronized {
sentEmails.+=((to, subject, body))
}
logger.info(s"Would send email to $to, with subject: $subject, body: $body")
Future.successful(())
}
def wasEmailSent(to: String, subject: String): Boolean =
sentEmails.exists(email => email._1.contains(to) && email._2 == subject)
}
================================================
FILE: ui/app/util/email/EmailService.scala
================================================
package util.email
import scala.concurrent.Future
trait EmailService {
def send(to: String, subject: String, body: String): Future[Unit]
}
================================================
FILE: ui/app/util/email/SendgridEmailService.scala
================================================
package util.email
import com.sendgrid.SendGrid
import com.typesafe.scalalogging.StrictLogging
import scala.concurrent.Future
import scala.util.Properties
class SendgridEmailService(sendgridUsername: String, sendgridPassword: String, emailFrom: String)
extends EmailService
with StrictLogging {
private lazy val sendgrid = new SendGrid(sendgridUsername, sendgridPassword)
override def send(to: String, subject: String, body: String) = {
val email = new SendGrid.Email()
email.addTo(to)
email.setFrom(emailFrom)
email.setSubject(subject)
email.setText(body)
val response = sendgrid.send(email)
if (response.getStatus) {
logger.info(s"Email to $to sent")
} else {
logger.error(
s"Email to $to, subject: $subject, body: $body, not sent: " +
s"${response.getCode}/${response.getMessage}"
)
}
Future.successful(())
}
}
object SendgridEmailService extends StrictLogging {
def createFromEnv(emailFrom: String): Option[SendgridEmailService] =
for {
u <- Properties.envOrNone("SENDGRID_USERNAME")
p <- Properties.envOrNone("SENDGRID_PASSWORD")
} yield {
logger.info("Using SendGrid email service")
new SendgridEmailService(u, p, emailFrom)
}
}
================================================
FILE: ui/app/views/index.scala.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scala Clippy</title>
<link rel='stylesheet' href='@routes.WebJarAssets.at(WebJarAssets.locate("css/bootstrap.min.css"))'>
<link rel="stylesheet" href="@routes.Assets.versioned("stylesheets/main.css")"/>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")"/>
<link rel="canonical" href="https://scala-clippy.org">
</head>
<body>
<!-- http://stackoverflow.com/questions/21324395/bootstrap-3-flush-footer-to-bottom-not-fixed -->
<div class="container">
<div id="reactmain"></div>
<div id="use" style="display: none">@use()</div>
<script type='text/javascript' src='@routes.WebJarAssets.at(WebJarAssets.locate("jquery.min.js"))'></script>
<script type='text/javascript' src='@routes.WebJarAssets.at(WebJarAssets.locate("bootstrap.min.js"))'></script>
@playscalajs.html.scripts("uiclient")
<a href="https://github.com/softwaremill/scala-clippy"><img style="position: absolute; top: 51px; right: 0; border: 0;" src="https://camo.githubusercontent.com/365986a132ccd6a44c23a9169022c0b5c890c387/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f7265645f6161303030302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png"></a>
</div>
<footer class="footer">
<div> </div>
<div class="text-center">
Copyright 2016. Created & maintained by
<a href="http://softwaremill.com">SoftwareMill</a>.
</div>
</footer>
<!-- twitter -->
<script>window.twttr = (function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0],
t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
t._e = [];
t.ready = function(f) {
t._e.push(f);
};
return t;
}(document, "script", "twitter-wjs"));</script>
<!-- google analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-11235106-14', 'auto');
ga('send', 'pageview');
</script>
</body>
</html>
================================================
FILE: ui/app/views/use.scala.html
================================================
<h2>
Scala Clippy
<a class="twitter-share-button" href="https://twitter.com/intent/tweet?text=Scala%20Clippy%3A%20programmer-friendly%20compiler%20errors%20">Tweet</a>
<a href="https://gitter.im/softwaremill/scala-clippy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge">
<img src="https://badges.gitter.im/softwaremill/scala-clippy.svg" alt="Join the chat at https://gitter.im/softwaremill/scala-clippy" style="max-width:100%; margin-bottom:5px;" />
</a>
</h2>
<p>
Did you ever see a Scala compiler error such as:
</p>
<img class="cout" src="assets/img/clippy-akka-err.png">
<p>
and had no idea what to do next? Well in this case, you need to provide an implicit instance of an <code>ActorMaterializer</code>,
but the compiler isn't smart enough to be able to tell you that. Luckily, <strong>Scala Clippy is here to help</strong>!
</p>
<p>
Just add the Scala Clippy compiler plugin, and you'll see this additional helpful message, with optional additional colors:
</p>
<img class="cout" src="assets/img/clippy-akka-err-rich.png">
<h2>Adding the plugin</h2>
<p>
The easiest to use Clippy is via an SBT plugin. If you'd like Clippy to be enabled for all projects, without
the need to modify each project's build, add the following to <code>~/.sbt/0.13/plugins/build.sbt</code>:
</p>
<pre>
addSbtPlugin("com.softwaremill.clippy" % "plugin-sbt" % "0.6.1")
</pre>
<p>
Upon first use, the plugin will download the advice dataset from <code>https://scala-clippy.org</code> and store it in the
<code>$HOME/.clippy</code> directory. The dataset will be updated at most once a day, in the background. You can customize the
dataset URL and local store by setting the <code>clippyUrl</code> and <code>clippyLocalStoreDir</code> sbt options to non-<code>None</code>-values.
</p>
<p>
Note: to customize a global sbt plugin (a plugin which is added via `~/.sbt/0.13/plugins/build.sbt`) keep in mind
that:
</p>
<ul>
<li>customize the plugin settings in <code>~/.sbt/0.13/build.sbt</code> (one directory up!). These settings will be
automatically added to all of your projects.</li>
<li>you'll need to add <code>import com.softwaremill.clippy.ClippySbtPlugin._</code> to access the setting names as auto-imports
don't work in the global settings</li>
<li>the clippy sbt settings are just a convenient syntax for adding compiler options (e.g., enabling colors is same
as <code>scalacOptions += "-P:clippy:colors=true"</code>)</li>
</ul>
<h2>(NEW!) Turning selected compilation warnings into errors</h2>
<p>
From version 0.6.0 you can selectively define regexes for warnings which will be treated as fatal compilation errors. To do so with sbt,
use the <code>clippyFatalWarnings</code> setting, for example:
</p>
<pre>
import com.softwaremill.clippy.ClippySbtPlugin.WarningPatterns._
// ...
settings(
clippyFatalWarnings ++= List(
NonExhaustiveMatch,
".*\\[wartremover:.*\\].*[\\s\\S]*"
)
)
</pre>
<p>
You can also define fatal warnings in your .clippy.json file (see further sections for examples).
</p>
<h2>Enabling syntax and type mismatch diffs highlighting</h2>
<p>
Clippy can highlight:
</p>
<ul>
<li>
in type mismatch errors, the diff between expected and actual types. This may be especially helpful for long type
signatures
</li>
<li>
syntax when displaying code fragments with errors. Example:
<img class="cout" src="assets/img/clippy-syntax-highlight.png">
</li>
</ul>
<p>
If you'd like to enable this feature in sbt globally, add the following to `~/.sbt/0.13/build.sbt`: (see also notes
above)
</p>
<pre>
import com.softwaremill.clippy.ClippySbtPlugin._ // needed in global configuration only
clippyColorsEnabled := true
</pre>
<p>
To customize the colors, set any of `clippyColorDiff`, `clippyColorComment`,
`clippyColorType`, `clippyColorLiteral`, `clippyColorKeyword` to `Some(ClippyColor.[name])`, where `[name]` can be:
`Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `White` or `None`.
</p>
<p>
You can of course add clippy on a per-project basis as well.
</p>
<h3>Windows users</h3>
<p>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.</p>
<h2>Contributing advice</h2>
<p>
Help others users by submitting an advice for a compilation error that you have encountered!
Just click "contribute" above and paste in your error!
</p>
<p>
Alternatively, create an issue on <a href="https://github.com/softwaremill/scala-clippy">GitHub</a> or chat with
us on <a href="https://gitter.im/softwaremill/scala-clippy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge">Gitter</a> in case of any doubts.
</p>
<p>
Speaking of GitHub, you are also welcome to check out (and improve!) the plugin's & website's
<a href="https://github.com/softwaremill/scala-clippy">source code</a>.
</p>
<h2>Project-specific advices and fatal warnings</h2>
<p>If you have advice that you feel is too specific to be worth sharing publicly, you can add
it to your project specific advice file. First set your project root:
</p>
<pre>
clippyProjectRoot := Some((baseDirectory in ThisBuild).value)
</pre>
Then create a file named .clippy.json in the root of your project directory and add the advice json in the format illustrated below:
<pre>
{
"version": "1",
"advices": [
{
"error": {
"type": "typeMismatch",
"found": "scala\\.concurrent\\.Future\\[Int\\]",
"required": "Int"
},
"text": "Maybe you used map where you should have used flatMap?",
"library": {
"groupId": "scala.lang",
"artifactId": "Future",
"version": "1.0"
}
}
],
"fatalWarnings": [
{
"pattern": "match may not be exhaustive[\\s\\S]*",
"text": "Additional optional text to be displayed"
}
]
}
</pre>
<h2>Library-specific advice</h2>
If you have advice that is specific to a library or library version you can also bundle the advice with your library.
If your users have Scala-Clippy installed they will see your advice if your library is inclued in their project.
This can be helpful in the common case where users of your library need specific imports to be able to use your functionality.
To bundle clippy advice with your library just put it in a file named clippy.json in your resources directory.
<h2>Alternative ways to use Clippy</h2>
<p>
You can also use Clippy directly as a compiler plugin. If you use SBT, add the following setting to your
project's <code>.sbt</code> file:
</p>
<pre>
addCompilerPlugin("com.softwaremill.clippy" %% "plugin" % "0.6.1" classifier "bundle")
</pre>
<p>
If you are using <code>scalac</code> directly, add the following option:
</p>
<pre>
-Xplugin:clippy-plugin_2.11-0.6.1-bundle.jar
</pre>
<p>
See clippy <a href="https://github.com/softwaremill/scala-clippy">README</a> for more details.</p>
</p>
================================================
FILE: ui/conf/application.conf
================================================
# This is the main configuration file for the application.
# ~~~~~
play.application.loader = "ClippyApplicationLoader"
# Secret key
# ~~~~~
# The secret key is used to secure cryptographics functions.
#
# This must be changed for production, but we recommend not changing it in this file.
#
# See http://www.playframework.com/documentation/latest/ApplicationSecret for more details.
play.crypto.secret = "changeme"
play.crypto.secret = ${?PLAY_CRYPTO_SECRET}
# The application languages
# ~~~~~
play.i18n.langs = [ "en" ]
# Router
# ~~~~~
# Define the Router object to use for this application.
# This router will be looked up first when the application is starting up,
# so make sure this is the entry point.
# Furthermore, it's assumed your route file is named properly.
# So for an application router like `my.application.Router`,
# you may need to define a router file `conf/my.application.routes`.
# Default to Routes in the root package (and conf/routes)
# play.http.router = my.application.Routes
db {
h2 {
dataSourceClass = "org.h2.jdbcx.JdbcDataSource"
properties = {
url = "jdbc:h2:file:./data/clippy"
}
}
postgres {
dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
maxConnections = 16
properties = {
serverName = ""
portNumber = "5432"
databaseName = ""
user = ""
password = ""
}
}
}
email.contact="clippy@scala-clippy.org"
play.server.http.port=${?PORT}
================================================
FILE: ui/conf/db/migration/V1__create_schema.sql
================================================
create table "advices" (
"id" bigint not null primary key,
"error_text_raw" varchar not null,
"compilation_error" varchar not null,
"advice" varchar not null,
"state" smallint not null,
"library_group_id" varchar not null,
"library_artifact_id" varchar not null,
"library_version" varchar not null,
"contributor_email" varchar,
"contributor_twitter" varchar,
"contributor_github" varchar,
"comment" varchar
);
================================================
FILE: ui/conf/db/migration/V2__add_pattern.sql
================================================
alter table "advices" add column "pattern_raw" varchar not null default '';
================================================
FILE: ui/conf/logback.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n%rEx</pattern>
</encoder>
</appender>
<logger name="application" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
================================================
FILE: ui/conf/routes
================================================
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.ApplicationController.index
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
# Webjars
GET /webjars/*file controllers.WebJarAssets.at(file)
GET /api/advices controllers.AdvicesController.get
# Autowire calls
POST /ui-api/*path controllers.AutowireController.autowireApi(path: String)
================================================
FILE: ui/test/dal/AdvicesRepositoryTest.scala
================================================
package dal
import com.softwaremill.clippy._
import com.softwaremill.id.DefaultIdGenerator
import util.BaseSqlSpec
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.concurrent.IntegrationPatience
class AdvicesRepositoryTest extends BaseSqlSpec with ScalaFutures with IntegrationPatience {
it should "store & read an advice" in {
// given
val ar = new AdvicesRepository(database, new DefaultIdGenerator())
// when
val stored = ar
.store(
"zzz",
"yyy",
TypeMismatchError[RegexT](RegexT("x"), None, RegexT("y"), None, None),
"z",
AdviceState.Pending,
Library("g", "a", "1"),
Contributor(None, None, Some("t")),
Some("c")
)
.futureValue
// then
val r = ar.findAll().futureValue
r should have size (1)
val found = r.head
stored should be(found)
found.errorTextRaw should be("zzz")
found.patternRaw should be("yyy")
found.compilationError should be(TypeMismatchError(RegexT("x"), None, RegexT("y"), None, None))
found.advice should be("z")
found.state should be(AdviceState.Pending)
found.library should be(Library("g", "a", "1"))
found.contributor should be(Contributor(None, None, Some("t")))
found.comment should be(Some("c"))
}
}
================================================
FILE: ui/test/util/BaseSqlSpec.scala
================================================
package util
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FlatSpec, Matchers}
import scala.concurrent.ExecutionContext
trait BaseSqlSpec extends FlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with ScalaFutures {
private val connectionString = "jdbc:h2:mem:clippy_test" + this.getClass.getSimpleName + ";DB_CLOSE_DELAY=-1"
lazy val database = SqlDatabase.createEmbedded(connectionString)
override protected def beforeAll() {
super.beforeAll()
createAll()
}
override protected def afterAll() {
super.afterAll()
dropAll()
database.close()
}
private def dropAll() {
import database.driver.api._
database.db.run(sqlu"DROP ALL OBJECTS").futureValue
}
private def createAll() {
database.updateSchema()
}
override protected def afterEach() {
try {
dropAll()
createAll()
} catch {
case e: Exception => e.printStackTrace()
}
super.afterEach()
}
implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/App.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
import autowire._
import japgolly.scalajs.react.vdom.prefix_<^._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Success}
object App {
sealed trait Page
case object UsePage extends Page
case object ContributeStep1InputError extends Page
case class ContributeParseError(errorText: String) extends Page
case class ContributeStep2EditPattern(errorTextRaw: String, ce: CompilationError[ExactT]) extends Page
case class ContributeStep3SubmitAdvice(
errorTextRaw: String,
patternText: String,
ceFromPattern: CompilationError[ExactT]
) extends Page
case object ListingPage extends Page
case object FeedbackPage extends Page
case class State(page: Page, errorMsgs: List[String], infoMsgs: List[String])
class Backend($ : BackendScope[Unit, State]) {
private val handleReset: Callback = clearMsgs >> $.modState(_.copy(page = ContributeStep1InputError))
private def handleErrorTextSubmitted(errorText: String): Callback =
CompilationErrorParser.parse(errorText) match {
case None => clearMsgs >> $.modState(_.copy(page = ContributeParseError(errorText)))
case Some(ce) => clearMsgs >> $.modState(_.copy(page = ContributeStep2EditPattern(errorText, ce)))
}
private def handleSendParseError(errorText: String)(email: String): Callback =
handleFuture(
AutowireClient[UiApi].sendCannotParse(errorText, email).call(),
Some("Error submitted successfully! We'll get in touch soon."),
Some((_: Unit) => $.modState(s => s.copy(page = ContributeStep1InputError)))
)
private def handlePatternSubmitted(
errorTextRaw: String,
patternText: String,
ceFromPattern: CompilationError[ExactT]
): Callback =
$.modState(s => s.copy(page = ContributeStep3SubmitAdvice(errorTextRaw, patternText, ceFromPattern)))
private def handleSendAdviceProposal(ap: AdviceProposal): Callback =
handleFuture(
AutowireClient[UiApi].sendAdviceProposal(ap).call(),
Some("Advice submitted successfully!"),
Some((_: Unit) => $.modState(s => s.copy(page = ContributeStep1InputError)))
)
private lazy val handleFuture = new HandleFuture {
override def apply[T](f: Future[T], successMsg: Option[String], successCallback: Option[(T) => Callback]) =
CallbackTo(f onComplete {
case Success(v) =>
val msgCallback = successMsg map handleShowInfo
val vCallback = successCallback map (_(v))
(msgCallback.getOrElse(Callback.empty) >> vCallback.getOrElse(Callback.empty)).runNow()
case Failure(e) => handleShowError("Error communicating with the server").runNow()
})
}
private def handleShowError(error: String): Callback =
clearMsgs >> $.modState(s => s.copy(errorMsgs = error :: s.errorMsgs))
private def handleShowInfo(info: String): Callback =
clearMsgs >> $.modState(s => s.copy(infoMsgs = info :: s.infoMsgs))
private def clearMsgs = $.modState(_.copy(errorMsgs = Nil, infoMsgs = Nil))
private def handleSwitchPage(newPage: Page): Callback =
clearMsgs >> $.modState { s =>
def isContribute(p: Page) = p != UsePage && p != ListingPage && p != FeedbackPage
if (s.page == newPage || (isContribute(s.page) && isContribute(newPage))) s else s.copy(page = newPage)
}
private def showMsgs(s: State) = <.span(
s.infoMsgs.map(m => <.div(^.cls := "alert alert-success", ^.role := "alert")(m)) ++
s.errorMsgs.map(m => <.div(^.cls := "alert alert-danger", ^.role := "alert")(m)): _*
)
private def showPage(s: State) = s.page match {
case UsePage =>
Use.component()
case ContributeStep1InputError =>
Contribute.Step1InputError.component(
Contribute.Step1InputError.Props(handleErrorTextSubmitted, handleShowError)
)
case ContributeParseError(et) =>
Contribute.ParseError.component(
Contribute.ParseError.Props(handleReset, handleSendParseError(et), handleShowError)
)
case ContributeStep2EditPattern(errorTextRaw, ce) =>
Contribute.Step2EditPattern.component(
Contribute.Step2EditPattern.Props(errorTextRaw, ce, handleReset, handlePatternSubmitted, handleShowError)
)
case ContributeStep3SubmitAdvice(errorTextRaw, pattern, ceFromPattern) =>
Contribute.Step3SubmitAdvice.component(
Contribute.Step3SubmitAdvice
.Props(errorTextRaw, pattern, ceFromPattern, handleReset, handleSendAdviceProposal, handleShowError)
)
case ListingPage =>
Listing.component(Listing.Props(handleShowError, clearMsgs, handleFuture))
case FeedbackPage =>
Feedback.component(Feedback.Props(handleShowError, clearMsgs, handleFuture))
}
def render(s: State) = <.span(
Menu.component((s.page, handleSwitchPage)),
<.div(^.cls := "container")(
showMsgs(s),
showPage(s)
)
)
}
}
trait HandleFuture {
def apply[T](f: Future[T], successMsg: Option[String], successCallback: Option[T => Callback]): Callback
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/AutowireClient.scala
================================================
package com.softwaremill.clippy
import org.scalajs.dom
import scala.concurrent.Future
import scalajs.concurrent.JSExecutionContext.Implicits.queue
import upickle.default._
import upickle.Js
object AutowireClient extends autowire.Client[Js.Value, Reader, Writer] {
override def doCall(req: Request): Future[Js.Value] =
dom.ext.Ajax
.post(
url = "/ui-api/" + req.path.mkString("/"),
data = upickle.json.write(Js.Obj(req.args.toSeq: _*))
)
.map(_.responseText)
.map(upickle.json.read)
def read[Result: Reader](p: Js.Value) = readJs[Result](p)
def write[Result: Writer](r: Result) = writeJs(r)
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/BsUtils.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.ExternalVar
import japgolly.scalajs.react.vdom.prefix_<^._
object BsUtils {
def bsPanel(body: TagMod*) = <.div(^.cls := "panel panel-default") {
<.div(^.cls := "panel-body")(body)
}
// Maybe this could be a component? But a function works for now
def bsFormEl(ev: ExternalVar[FormField])(body: Seq[TagMod] => ReactTag) = {
val formField = ev.value
val elId = Utils.randomString(8)
<.div(^.cls := "form-group", formField.error ?= (^.cls := "has-error"))(
<.label(^.htmlFor := elId, if (formField.required) <.strong(formField.label) else formField.label),
body(
Seq(
^.id := elId,
formField.required ?= (^.required := "required"),
^.value := formField.v,
^.onChange ==> ((e: ReactEventI) => ev.set(formField.copy(v = e.target.value)))
)
)
)
}
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Contribute.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import BsUtils._
import Utils._
import monocle.macros.Lenses
object Contribute {
object Step1InputError {
case class Props(next: String => Callback, showError: String => Callback)
@Lenses
case class State(errorText: FormField)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(errorText = s.errorText.validated)
override def fields(s: State) = List(s.errorText)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = <.div(
bsPanel(
<.p(
"Scala Clippy is only as good as its advice database. Help other users by submitting an advice for a compilation error that you have encountered!"
),
<.p("First, paste in the error and we'll see if we can parse it. Only the error message is needed, e.g.:"),
<.pre(
"""[error] type mismatch;
|[error] found : akka.http.scaladsl.server.StandardRoute
|[error] required: akka.stream.scaladsl.Flow[akka.http.scaladsl.model.HttpRequest,akka.http.scaladsl.model.HttpResponse,Any]""".stripMargin
),
<.p("You will be able to edit the pattern that will be used when matching errors later.")
),
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(s => p.next(s.errorText.v)),
bsFormEl(externalVar($, s, State.errorText))(mods => <.textarea(^.cls := "form-control", ^.rows := 5)(mods)),
<.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Next")
)
)
}
val component = ReactComponentB[Props]("Step1InputError")
.initialState(State(FormField("Error text", required = true)))
.renderBackend[Backend]
.build
}
object Step2EditPattern {
case class Props(
errorTextRaw: String,
ce: CompilationError[ExactT],
reset: Callback,
next: (String, String, CompilationError[ExactT]) => Callback,
showError: String => Callback
)
@Lenses
case class State(patternText: FormField)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(patternText = s.patternText.validated)
override def fields(s: State) = List(s.patternText)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = {
val parsedPattern = CompilationErrorParser.parse(s.patternText.v)
val errorMatchesPattern = parsedPattern.map(_.asRegex).map(_.matches(p.ce))
val alert = errorMatchesPattern match {
case None =>
<.div(^.cls := "alert alert-danger", ^.role := "alert")("Cannot parse the error")
case Some(false) =>
<.div(^.cls := "alert alert-danger", ^.role := "alert")("The pattern doesn't match the original error")
case Some(true) =>
<.div(^.cls := "alert alert-success", ^.role := "alert")("The pattern matches the original error")
}
val canProceed = errorMatchesPattern.getOrElse(false)
val nextCallback = parsedPattern match {
case None => Callback.empty
case Some(ceFromPattern) => p.next(p.errorTextRaw, s.patternText.v, ceFromPattern)
}
<.div(
bsPanel(
<.p("Parsing successfull! Here's what we've found:"),
<.pre(p.ce.toString),
<.p(
"You can now create a pattern for matching errors using a wildcard character: *. For example, you can generalize a pattern by replacing concrete type parameters with *:"
),
<.pre("cats.Monad[List] -> cats.Monad[*]"),
<.p("Or, you can just click next and leave the submitted error to be an exact pattern.")
),
alert,
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(_ => nextCallback),
bsFormEl(externalVar($, s, State.patternText))(
mods => <.textarea(^.cls := "form-control", ^.rows := 5)(mods)
),
<.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"),
<.span(" "),
<.button(^.`type` := "submit", ^.cls := "btn btn-primary", ^.disabled := !canProceed)("Next")
)
)
}
}
val component = ReactComponentB[Props]("Step2EditPattern")
.initialState_P { p =>
State(FormField("Pattern", required = true, p.errorTextRaw, error = false))
}
.renderBackend[Backend]
.build
}
object Step3SubmitAdvice {
case class Props(
errorTextRaw: String,
patternRaw: String,
ceFromPattern: CompilationError[ExactT],
reset: Callback,
send: AdviceProposal => Callback,
showError: String => Callback
)
@Lenses
case class State(
advice: FormField,
libraryGroupId: FormField,
libraryArtifactId: FormField,
libraryVersion: FormField,
email: FormField,
twitter: FormField,
github: FormField,
comment: FormField
)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(
advice = s.advice.validated,
libraryGroupId = s.libraryGroupId.validated,
libraryArtifactId = s.libraryArtifactId.validated,
libraryVersion = s.libraryVersion.validated,
email = s.email.validated,
twitter = s.twitter.validated,
github = s.github.validated,
comment = s.comment.validated
)
override def fields(s: State) =
List(
s.advice,
s.libraryGroupId,
s.libraryArtifactId,
s.libraryVersion,
s.email,
s.twitter,
s.github,
s.comment
)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = <.div(
bsPanel(
<.p("You can now define the advice associated with the error. Here's the pattern you have entered:"),
<.pre(p.ceFromPattern.toString),
<.p(
"You can optionally leave an e-mail so that we can let you know when your proposal is accepted, and a twitter/github handle so that we can add proper attribution, visible in the advice browser."
)
),
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(
s =>
p.send(
AdviceProposal(
p.errorTextRaw,
p.patternRaw,
p.ceFromPattern.asRegex,
s.advice.v,
Library(s.libraryGroupId.v, s.libraryArtifactId.v, s.libraryVersion.v),
Contributor(s.email.vOpt, s.twitter.vOpt, s.github.vOpt),
s.comment.vOpt
)
)
),
bsFormEl(externalVar($, s, State.advice))(mods => <.textarea(^.cls := "form-control", ^.rows := 3)(mods)),
<.hr,
bsFormEl(externalVar($, s, State.libraryGroupId))(
mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "org.scala")(mods)
),
bsFormEl(externalVar($, s, State.libraryArtifactId))(
mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "scala-lang")(mods)
),
bsFormEl(externalVar($, s, State.libraryVersion))(
mods => <.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "2.11-M3")(mods)
),
<.hr,
bsFormEl(externalVar($, s, State.email))(
mods =>
<.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods)
),
bsFormEl(externalVar($, s, State.twitter))(
mods =>
<.div(^.cls := "input-group")(
<.div(^.cls := "input-group-addon")("@"),
<.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "twitter")(mods)
)
),
bsFormEl(externalVar($, s, State.github))(
mods =>
<.div(^.cls := "input-group")(
<.div(^.cls := "input-group-addon")("@"),
<.input(^.`type` := "text", ^.cls := "form-control", ^.placeholder := "github")(mods)
)
),
<.hr,
bsFormEl(externalVar($, s, State.comment))(mods => <.textarea(^.cls := "form-control", ^.rows := 3)(mods)),
<.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"),
<.span(" "),
<.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Send")
)
)
}
val component = ReactComponentB[Props]("Step3SubmitAdvice")
.initialState(
State(
FormField("Advice", required = true),
FormField("Library group id", required = true),
FormField("Library artifact id", required = true),
FormField("Library version", required = true),
FormField("E-mail (optional)", required = false),
FormField("Twitter handle (optional)", required = false),
FormField("Github handle (optional)", required = false),
FormField("Comment (optional)", required = false)
)
)
.renderBackend[Backend]
.build
}
object ParseError {
case class Props(reset: Callback, send: String => Callback, showError: String => Callback)
@Lenses
case class State(email: FormField)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(email = s.email.validated)
override def fields(s: State) = List(s.email)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = <.div(
bsPanel(
<.p(
"Unfortunately we cannot parse the error. Let us know how to contact you, we'll try to find out what's wrong and get back to you."
)
),
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(s => p.send(s.email.v)),
bsFormEl(externalVar($, s, State.email))(
mods =>
<.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods)
),
<.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> p.reset)("Reset"),
<.span(" "),
<.button(^.`type` := "submit", ^.cls := "btn btn-primary")("Send")
)
)
}
val component = ReactComponentB[Props]("ParseError")
.initialState(State(FormField("Email", required = true)))
.renderBackend[Backend]
.build
}
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Feedback.scala
================================================
package com.softwaremill.clippy
import autowire._
import com.softwaremill.clippy.BsUtils._
import com.softwaremill.clippy.Utils._
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import monocle.macros.Lenses
import scala.concurrent.ExecutionContext.Implicits.global
object Feedback {
case class Props(showError: String => Callback, clearMsgs: Callback, handleFuture: HandleFuture)
@Lenses
case class State(contact: FormField, feedback: FormField)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(
contact = s.contact.validated,
feedback = s.feedback.validated
)
override def fields(s: State) = List(s.contact, s.feedback)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = {
def sendFeedbackCallback() =
p.handleFuture(
AutowireClient[UiApi].feedback(s.feedback.v, s.contact.v).call(),
Some("Feedback sent, thank you!"),
Some(
(_: Unit) => $.modState(s => s.copy(contact = s.contact.copy(v = ""), feedback = s.feedback.copy(v = "")))
)
)
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(s => sendFeedbackCallback()),
bsFormEl(externalVar($, s, State.contact))(
mods =>
<.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods)
),
bsFormEl(externalVar($, s, State.feedback))(mods => <.textarea(^.cls := "form-control", ^.rows := "3")(mods)),
<.button(^.`type` := "send", ^.cls := "btn btn-primary")("Send")
)
}
}
val component = ReactComponentB[Props]("Use")
.initialState(
State(
FormField("Contact email", required = true),
FormField("Feedback", required = true)
)
)
.renderBackend[Backend]
.build
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/FormField.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
case class FormField(label: String, required: Boolean, v: String, error: Boolean) {
def validated =
if (required) {
if (v.isEmpty) copy(error = true) else copy(error = false)
} else this
def vOpt: Option[String] = if (v.isEmpty) None else Some(v)
}
object FormField {
def apply(label: String, required: Boolean): FormField = FormField(label, required, "", error = false)
def errorMsgIfAny(fields: Seq[FormField]): Option[String] =
fields.find(_.error).map(ff => s"${ff.label} is required") // required is the only type of error there could be
def submitValidated[P, S: Validatable](
$ : BackendScope[P, S],
showError: String => Callback
)(submit: S => Callback)(e: ReactEventI): Callback =
for {
_ <- e.preventDefaultCB
props <- $.props
s <- $.state
v = implicitly[Validatable[S]]
s2 = v.validated(s)
_ <- $.setState(s2)
fields = v.fields(s2)
em = errorMsgIfAny(fields)
_ <- em.fold(submit(s2))(showError)
} yield ()
}
trait Validatable[S] {
def validated(s: S): S
def fields(s: S): Seq[FormField]
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Listing.scala
================================================
package com.softwaremill.clippy
import com.softwaremill.clippy.BsUtils._
import com.softwaremill.clippy.Utils._
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import autowire._
import monocle.macros.Lenses
import scala.concurrent.ExecutionContext.Implicits.global
object Listing {
case class Props(showError: String => Callback, clearMsgs: Callback, handleFuture: HandleFuture)
@Lenses
case class State(
advices: Seq[AdviceListing],
suggestEditId: Option[Long],
suggestContact: FormField,
suggestText: FormField
)
implicit val stateVal = new Validatable[State] {
override def validated(s: State) = s.copy(
suggestContact = s.suggestContact.validated,
suggestText = s.suggestText.validated
)
override def fields(s: State) = List(s.suggestContact, s.suggestText)
}
class Backend($ : BackendScope[Props, State]) {
def render(s: State, p: Props) = {
def suggestEditCallback(a: AdviceListing) =
p.clearMsgs >> $.modState(s => s.copy(suggestEditId = Some(a.id), suggestText = s.suggestText.copy(v = "")))
def cancelSuggestEditCallback(a: AdviceListing) = $.modState(_.copy(suggestEditId = None))
def sendSuggestEditCallback(a: AdviceListing) =
cancelSuggestEditCallback(a) >> p.handleFuture(
AutowireClient[UiApi].sendSuggestEdit(s.suggestText.v, s.suggestContact.v, a).call(),
Some("Suggestion sent, thank you!"),
None
)
def rowForAdvice(a: AdviceListing) = <.tr(
<.td(<.pre(a.compilationError.toString)),
<.td(a.advice),
<.td(a.library.toString),
<.td(<.span(^.cls := "glyphicon glyphicon-edit", ^.onClick --> suggestEditCallback(a)))
)
def suggestEdit(a: AdviceListing) = <.tr(
<.td(^.colSpan := 4)(
<.form(
^.onSubmit ==> FormField.submitValidated($, p.showError)(s => sendSuggestEditCallback(a)),
bsFormEl(externalVar($, s, State.suggestContact))(
mods =>
<.input(^.`type` := "email", ^.cls := "form-control", ^.placeholder := "scalacoder@company.com")(mods)
),
bsFormEl(externalVar($, s, State.suggestText))(
mods => <.textarea(^.cls := "form-control", ^.rows := "3")(mods)
),
<.button(^.`type` := "reset", ^.cls := "btn btn-default", ^.onClick --> cancelSuggestEditCallback(a))(
"Cancel"
),
<.span(" "),
<.button(^.`type` := "send", ^.cls := "btn btn-primary")("Send")
)
)
)
<.table(^.cls := "table table-striped advice-listing")(
<.thead(
<.tr(
<.th("Compilation error (as regex)"),
<.th(^.width := "25%")("Advice"),
<.th(^.width := "20%")("Library"),
<.th(^.width := "8%")("Suggest edit")
)
),
<.tbody(
s.advices.flatMap(
a => List(rowForAdvice(a)) ++ s.suggestEditId.filter(_ == a.id).map(_ => suggestEdit(a)).toList
): _*
)
)
}
def initAdvices(p: Props): Callback =
p.handleFuture(
AutowireClient[UiApi].listAccepted().call(),
None,
Some((s: Seq[AdviceListing]) => $.modState(_.copy(advices = s)))
)
}
val component = ReactComponentB[Props]("Use")
.initialState(
State(
Nil,
None,
FormField("Contact email (optional)", required = false),
FormField("Suggestion", required = true)
)
)
.renderBackend[Backend]
.componentDidMount(ctx => ctx.backend.initAdvices(ctx.props))
.build
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Main.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
import org.scalajs.jquery._
import scala.scalajs.js
import japgolly.scalajs.react.vdom.prefix_<^._
object Main extends js.JSApp {
type HtmlId = String
def main(): Unit =
jQuery(setupUI _)
def setupUI(): Unit = {
val mountNode = org.scalajs.dom.document.getElementById("reactmain")
val app = ReactComponentB[Unit]("App")
.initialState(App.State(App.UsePage, Nil, Nil))
.renderBackend[App.Backend]
.build
ReactDOM.render(app(), mountNode)
}
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Menu.scala
================================================
package com.softwaremill.clippy
import com.softwaremill.clippy.App._
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
object Menu {
val component = ReactComponentB[(Page, Page => Callback)]("Menu").render { $ =>
val isUsePage = $.props._1 == UsePage
val isListingPage = $.props._1 == ListingPage
val isFeedbackPage = $.props._1 == FeedbackPage
val isContributePage = !isUsePage && !isListingPage && !isFeedbackPage
def switchTo(p: Page)(e: ReactEventI) = e.preventDefaultCB >> $.props._2(p)
<.nav(^.cls := "navbar navbar-inverse navbar-fixed-top")(
<.div(^.cls := "container")(
<.div(^.cls := "navbar-header")(
<.button(
^.`type` := "button",
^.cls := "navbar-toggle collapsed",
"data-toggle".reactAttr := "collapse",
"data-target".reactAttr := "navbar",
"aria-expanded".reactAttr := "false",
"aria-controls".reactAttr := "navbar"
)(
<.span(^.cls := "sr-only")("Toggle navigation"),
<.span(^.cls := "icon-bar"),
<.span(^.cls := "icon-bar"),
<.span(^.cls := "icon-bar")
),
<.a(^.cls := "navbar-brand", ^.onClick ==> switchTo(UsePage), ^.href := "#")("Scala Clippy")
),
<.div(^.id := "navbar", ^.cls := "collapse navbar-collapse")(
<.ul(^.cls := "nav navbar-nav")(
<.li(isUsePage ?= (^.cls := "active"))(
<.a("Use", ^.onClick ==> switchTo(UsePage), ^.href := "#")
),
<.li(isContributePage ?= (^.cls := "active"))(
<.a("Contribute", ^.onClick ==> switchTo(ContributeStep1InputError), ^.href := "#")
),
<.li(isListingPage ?= (^.cls := "active"))(
<.a("Browse", ^.onClick ==> switchTo(ListingPage), ^.href := "#")
),
<.li(isFeedbackPage ?= (^.cls := "active"))(
<.a("Send feedback", ^.onClick ==> switchTo(FeedbackPage), ^.href := "#")
)
)
)
)
)
}.build
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Use.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
object Use {
val component = ReactComponentB[Unit]("Use").render { $ =>
val html = org.scalajs.dom.document.getElementById("use").innerHTML
<.span(^.dangerouslySetInnerHtml(html))
}.build
}
================================================
FILE: ui-client/src/main/scala/com/softwaremill/clippy/Utils.scala
================================================
package com.softwaremill.clippy
import japgolly.scalajs.react.BackendScope
import japgolly.scalajs.react.extra.ExternalVar
import monocle._
import scala.util.Random
object Utils {
def randomString(length: Int) = Random.alphanumeric take length mkString ""
// ExternalVar companion has only methods for creating a var from AccessRD (read direct), here we are reading
// through callbacks, so we need that extra method
def externalVar[S, A]($ : BackendScope[_,
gitextract_gwq_0d06/
├── .gitignore
├── .scalafmt.conf
├── .travis.yml
├── LICENSE.txt
├── README.md
├── build.sbt
├── model/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── Advice.scala
│ │ ├── Clippy.scala
│ │ ├── CompilationError.scala
│ │ ├── CompilationErrorParser.scala
│ │ ├── Library.scala
│ │ ├── StringDiff.scala
│ │ ├── Template.scala
│ │ └── Warning.scala
│ └── test/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── CompilationErrorParserTest.scala
│ ├── CompilationErrorTest.scala
│ ├── LibraryProperties.scala
│ ├── RegexTTest.scala
│ ├── StringDiffSpecification.scala
│ ├── StringDiffTest.scala
│ └── TypeNamesGenerators.scala
├── package.json
├── plugin/
│ └── src/
│ └── main/
│ ├── resources/
│ │ └── scalac-plugin.xml
│ ├── scala/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── AdviceLoader.scala
│ │ ├── ClippyPlugin.scala
│ │ ├── ColorsConfig.scala
│ │ ├── FailOnWarningsReporter.scala
│ │ ├── Highlighter.scala
│ │ ├── InjectReporter.scala
│ │ ├── RestoreReporter.scala
│ │ └── Utils.scala
│ ├── scala-2.11/
│ │ └── com/
│ │ └── softwaremill/
│ │ └── clippy/
│ │ ├── DelegatingPosition.scala
│ │ └── DelegatingReporter.scala
│ └── scala-2.12/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── DelegatingPosition.scala
│ └── DelegatingReporter.scala
├── plugin-sbt/
│ └── src/
│ └── main/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ └── ClippySbtPlugin.scala
├── project/
│ ├── build.properties
│ └── plugins.sbt
├── tests/
│ └── src/
│ └── test/
│ └── scala/
│ └── org/
│ └── softwaremill/
│ └── clippy/
│ └── CompileTests.scala
├── ui/
│ ├── app/
│ │ ├── ClippyApplicationLoader.scala
│ │ ├── api/
│ │ │ └── UiApiImpl.scala
│ │ ├── assets/
│ │ │ └── stylesheets/
│ │ │ └── main.less
│ │ ├── controllers/
│ │ │ ├── AdvicesController.scala
│ │ │ ├── ApplicationController.scala
│ │ │ ├── AutowireController.scala
│ │ │ └── HttpsFilter.scala
│ │ ├── dal/
│ │ │ └── AdvicesRepository.scala
│ │ ├── util/
│ │ │ ├── ConfigWithDefault.scala
│ │ │ ├── DatabaseConfig.scala
│ │ │ ├── SqlDatabase.scala
│ │ │ ├── Zip.scala
│ │ │ └── email/
│ │ │ ├── DummyEmailService.scala
│ │ │ ├── EmailService.scala
│ │ │ └── SendgridEmailService.scala
│ │ └── views/
│ │ ├── index.scala.html
│ │ └── use.scala.html
│ ├── conf/
│ │ ├── application.conf
│ │ ├── db/
│ │ │ └── migration/
│ │ │ ├── V1__create_schema.sql
│ │ │ └── V2__add_pattern.sql
│ │ ├── logback.xml
│ │ └── routes
│ └── test/
│ ├── dal/
│ │ └── AdvicesRepositoryTest.scala
│ └── util/
│ └── BaseSqlSpec.scala
├── ui-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── com/
│ └── softwaremill/
│ └── clippy/
│ ├── App.scala
│ ├── AutowireClient.scala
│ ├── BsUtils.scala
│ ├── Contribute.scala
│ ├── Feedback.scala
│ ├── FormField.scala
│ ├── Listing.scala
│ ├── Main.scala
│ ├── Menu.scala
│ ├── Use.scala
│ └── Utils.scala
└── ui-shared/
└── src/
└── main/
└── scala/
└── com/
└── softwaremill/
└── clippy/
├── AdviceState.scala
├── Contributor.scala
└── UiApi.scala
SYMBOL INDEX (1 symbols across 1 files) FILE: ui/conf/db/migration/V1__create_schema.sql type "advices" (line 1) | create table "advices" (
Condensed preview — 77 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (204K chars).
[
{
"path": ".gitignore",
"chars": 78,
"preview": "node_modules\n*.iml\n*.ipr\n*.iws\n.idea/\ntarget/\ndata/\n*.log\n.DS_Store\nlocal.sbt\n"
},
{
"path": ".scalafmt.conf",
"chars": 315,
"preview": "style = defaultWithAlign\nmaxColumn = 120\nalign.openParenCallSite = false\nalign.openParenDefnSite = false\ndanglingParenth"
},
{
"path": ".travis.yml",
"chars": 964,
"preview": "sudo: false\nlanguage: scala\njdk:\n- oraclejdk8\nscala:\n- 2.11.8\ninstall:\n- \". $HOME/.nvm/nvm.sh\"\n- nvm install stable\n- nv"
},
{
"path": "LICENSE.txt",
"chars": 11348,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 2975,
"preview": "# Scala clippy\n\n[', '"
},
{
"path": "model/src/main/scala/com/softwaremill/clippy/Template.scala",
"chars": 1080,
"preview": "package com.softwaremill.clippy\n\nimport java.util.regex.Pattern\n\nimport scala.util.Try\nimport scala.util.matching.Regex\n"
},
{
"path": "model/src/main/scala/com/softwaremill/clippy/Warning.scala",
"chars": 821,
"preview": "package com.softwaremill.clippy\n\nimport org.json4s.JsonAST._\n\nfinal case class Warning(pattern: RegexT, text: Option[Str"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/CompilationErrorParserTest.scala",
"chars": 10669,
"preview": "package com.softwaremill.clippy\n\nimport org.scalatest.{FlatSpec, Matchers}\n\nclass CompilationErrorParserTest extends Fla"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/CompilationErrorTest.scala",
"chars": 6102,
"preview": "package com.softwaremill.clippy\n\nimport org.scalacheck.Prop._\nimport org.scalacheck.Properties\nimport org.scalatest.{Fla"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/LibraryProperties.scala",
"chars": 319,
"preview": "package com.softwaremill.clippy\n\nimport org.scalacheck.Prop._\nimport org.scalacheck.Properties\n\nclass LibraryProperties "
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/RegexTTest.scala",
"chars": 1238,
"preview": "package com.softwaremill.clippy\n\nimport org.scalatest.{FlatSpec, Matchers}\n\nclass RegexTTest extends FlatSpec with Match"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/StringDiffSpecification.scala",
"chars": 2668,
"preview": "package com.softwaremill.clippy\n\nimport org.scalacheck.Prop.forAll\nimport org.scalacheck.Properties\n\nclass StringDiffSpe"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/StringDiffTest.scala",
"chars": 1682,
"preview": "package com.softwaremill.clippy\n\nimport org.scalatest.{FlatSpec, Matchers}\n\nclass StringDiffTest extends FlatSpec with M"
},
{
"path": "model/src/test/scala/com/softwaremill/clippy/TypeNamesGenerators.scala",
"chars": 3494,
"preview": "package com.softwaremill.clippy\n\nimport org.scalacheck.Gen\n\ntrait TypeNamesGenerators {\n\n import Gen._\n\n def typeNameC"
},
{
"path": "package.json",
"chars": 88,
"preview": "{\n \"name\": \"scala-clippy-site\",\n \"dependencies\": {\n \"jsdom\": \"9.12.0\"\n }\n\n}\n"
},
{
"path": "plugin/src/main/resources/scalac-plugin.xml",
"chars": 106,
"preview": "<plugin>\n <name>clippy</name>\n <classname>com.softwaremill.clippy.ClippyPlugin</classname>\n</plugin>"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/AdviceLoader.scala",
"chars": 4797,
"preview": "package com.softwaremill.clippy\n\nimport java.io._\nimport java.net.{HttpURLConnection, URL}\nimport java.util.zip.GZIPInpu"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/ClippyPlugin.scala",
"chars": 8600,
"preview": "package com.softwaremill.clippy\n\nimport java.io.File\nimport java.net.{URL, URLClassLoader}\nimport java.util.concurrent.T"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/ColorsConfig.scala",
"chars": 510,
"preview": "package com.softwaremill.clippy\n\nsealed trait ColorsConfig\n\nobject ColorsConfig {\n case object Disabled extends ColorsC"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/FailOnWarningsReporter.scala",
"chars": 2351,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.Position\nimport scala.tools.nsc.reporters.Reporter\n\n"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/Highlighter.scala",
"chars": 4302,
"preview": "package com.softwaremill.clippy\n\nimport fastparse.all._\n\nimport scalaparse.Scala._\nimport scalaparse.syntax.Identifiers."
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/InjectReporter.scala",
"chars": 1188,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.Position\nimport scala.tools.nsc.plugins.PluginCompon"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/RestoreReporter.scala",
"chars": 1035,
"preview": "package com.softwaremill.clippy\n\nimport scala.tools.nsc.plugins.PluginComponent\nimport scala.tools.nsc.{Global, Phase}\n\n"
},
{
"path": "plugin/src/main/scala/com/softwaremill/clippy/Utils.scala",
"chars": 1926,
"preview": "package com.softwaremill.clippy\n\nimport java.io.{ByteArrayOutputStream, InputStream}\nimport java.io.Closeable\nimport sca"
},
{
"path": "plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingPosition.scala",
"chars": 6346,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.{NoPosition, Position, SourceFile}\nimport scala.refl"
},
{
"path": "plugin/src/main/scala-2.11/com/softwaremill/clippy/DelegatingReporter.scala",
"chars": 2098,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.Position\nimport scala.tools.nsc.reporters.Reporter\n\n"
},
{
"path": "plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingPosition.scala",
"chars": 6346,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.{NoPosition, Position, SourceFile}\nimport scala.refl"
},
{
"path": "plugin/src/main/scala-2.12/com/softwaremill/clippy/DelegatingReporter.scala",
"chars": 2098,
"preview": "package com.softwaremill.clippy\n\nimport scala.reflect.internal.util.Position\nimport scala.tools.nsc.reporters.Reporter\n\n"
},
{
"path": "plugin-sbt/src/main/scala/com/softwaremill/clippy/ClippySbtPlugin.scala",
"chars": 4742,
"preview": "package com.softwaremill.clippy\n\nimport sbt._\nimport sbt.Keys._\n\nimport scala.collection.mutable.ListBuffer\n\nobject Clip"
},
{
"path": "project/build.properties",
"chars": 20,
"preview": "sbt.version=0.13.17\n"
},
{
"path": "project/plugins.sbt",
"chars": 997,
"preview": "resolvers += Resolver.typesafeRepo(\"releases\")\n// Workaround for the bug: https://github.com/sbt/sbt-assembly/issues/236"
},
{
"path": "tests/src/test/scala/org/softwaremill/clippy/CompileTests.scala",
"chars": 5544,
"preview": "package org.softwaremill.clippy\n\nimport java.io.{File, FileOutputStream}\nimport java.util.zip.GZIPOutputStream\nimport sc"
},
{
"path": "ui/app/ClippyApplicationLoader.scala",
"chars": 1913,
"preview": "import api.UiApiImpl\nimport com.softwaremill.id.DefaultIdGenerator\nimport controllers._\nimport dal.AdvicesRepository\nimp"
},
{
"path": "ui/app/api/UiApiImpl.scala",
"chars": 1797,
"preview": "package api\n\nimport com.softwaremill.clippy._\nimport dal.AdvicesRepository\nimport util.email.EmailService\n\nimport scala."
},
{
"path": "ui/app/assets/stylesheets/main.less",
"chars": 420,
"preview": "#reactmain {\n padding-top: 60px;\n}\n\nhtml {\n position: relative;\n min-height: 100%;\n}\n.cout {\n width: 100%;\n padding"
},
{
"path": "ui/app/controllers/AdvicesController.scala",
"chars": 962,
"preview": "package controllers\n\nimport com.softwaremill.clippy.{Advice, Clippy}\nimport dal.AdvicesRepository\nimport play.api.mvc.{A"
},
{
"path": "ui/app/controllers/ApplicationController.scala",
"chars": 169,
"preview": "package controllers\n\nimport play.api.mvc.{Action, Controller}\n\nclass ApplicationController extends Controller {\n\n def i"
},
{
"path": "ui/app/controllers/AutowireController.scala",
"chars": 856,
"preview": "package controllers\n\nimport com.softwaremill.clippy.UiApi\nimport play.api.mvc.{Action, Controller}\n\nimport upickle.defau"
},
{
"path": "ui/app/controllers/HttpsFilter.scala",
"chars": 669,
"preview": "package controllers\n\nimport play.api.http.Status\nimport play.api.mvc.{Filter, RequestHeader, Result, Results}\n\nimport sc"
},
{
"path": "ui/app/dal/AdvicesRepository.scala",
"chars": 3355,
"preview": "package dal\n\nimport com.softwaremill.clippy.AdviceState.AdviceState\nimport com.softwaremill.clippy._\nimport com.software"
},
{
"path": "ui/app/util/ConfigWithDefault.scala",
"chars": 1112,
"preview": "package util\n\nimport java.util.concurrent.TimeUnit\n\nimport com.typesafe.config.Config\n\ntrait ConfigWithDefault {\n\n def "
},
{
"path": "ui/app/util/DatabaseConfig.scala",
"chars": 1001,
"preview": "package util\n\nimport com.typesafe.config.Config\n\ntrait DatabaseConfig extends ConfigWithDefault {\n def rootConfig: Conf"
},
{
"path": "ui/app/util/SqlDatabase.scala",
"chars": 3610,
"preview": "package util\n\nimport java.net.URI\n\nimport com.typesafe.config.ConfigValueFactory._\nimport com.typesafe.config.{Config, C"
},
{
"path": "ui/app/util/Zip.scala",
"chars": 900,
"preview": "package util\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream}\nimport java.util.zip.{GZIPInputStream, GZIPOu"
},
{
"path": "ui/app/util/email/DummyEmailService.scala",
"chars": 765,
"preview": "package util.email\n\nimport com.typesafe.scalalogging.StrictLogging\n\nimport scala.collection.mutable.ListBuffer\nimport sc"
},
{
"path": "ui/app/util/email/EmailService.scala",
"chars": 143,
"preview": "package util.email\n\nimport scala.concurrent.Future\n\ntrait EmailService {\n def send(to: String, subject: String, body: S"
},
{
"path": "ui/app/util/email/SendgridEmailService.scala",
"chars": 1271,
"preview": "package util.email\n\nimport com.sendgrid.SendGrid\nimport com.typesafe.scalalogging.StrictLogging\n\nimport scala.concurrent"
},
{
"path": "ui/app/views/index.scala.html",
"chars": 3015,
"preview": "<!DOCTYPE html>\n\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=e"
},
{
"path": "ui/app/views/use.scala.html",
"chars": 7246,
"preview": "<h2>\n Scala Clippy\n <a class=\"twitter-share-button\" href=\"https://twitter.com/intent/tweet?text=Scala%20Clippy%3A%"
},
{
"path": "ui/conf/application.conf",
"chars": 1452,
"preview": "# This is the main configuration file for the application.\n# ~~~~~\n\nplay.application.loader = \"ClippyApplicationLoader\"\n"
},
{
"path": "ui/conf/db/migration/V1__create_schema.sql",
"chars": 434,
"preview": "create table \"advices\" (\n \"id\" bigint not null primary key,\n \"error_text_raw\" varchar not null,\n \"compilation_error\" "
},
{
"path": "ui/conf/db/migration/V2__add_pattern.sql",
"chars": 76,
"preview": "alter table \"advices\" add column \"pattern_raw\" varchar not null default '';\n"
},
{
"path": "ui/conf/logback.xml",
"chars": 608,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<configuration>\n\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleA"
},
{
"path": "ui/conf/routes",
"chars": 607,
"preview": "# Routes\n# This file defines all application routes (Higher priority routes first)\n# ~~~~\n\n# Home page\nGET / "
},
{
"path": "ui/test/dal/AdvicesRepositoryTest.scala",
"chars": 1304,
"preview": "package dal\n\nimport com.softwaremill.clippy._\nimport com.softwaremill.id.DefaultIdGenerator\nimport util.BaseSqlSpec\nimpo"
},
{
"path": "ui/test/util/BaseSqlSpec.scala",
"chars": 1112,
"preview": "package util\n\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, "
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/App.scala",
"chars": 5425,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\nimport autowire._\nimport japgolly.scalajs.react.vdom.pr"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/AutowireClient.scala",
"chars": 646,
"preview": "package com.softwaremill.clippy\n\nimport org.scalajs.dom\nimport scala.concurrent.Future\nimport scalajs.concurrent.JSExecu"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/BsUtils.scala",
"chars": 955,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\nimport japgolly.scalajs.react.extra.ExternalVar\nimport "
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Contribute.scala",
"chars": 10911,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\nimport japgolly.scalajs.react.vdom.prefix_<^._\nimport B"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Feedback.scala",
"chars": 1912,
"preview": "package com.softwaremill.clippy\n\nimport autowire._\nimport com.softwaremill.clippy.BsUtils._\nimport com.softwaremill.clip"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/FormField.scala",
"chars": 1189,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\n\ncase class FormField(label: String, required: Boolean,"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Listing.scala",
"chars": 3676,
"preview": "package com.softwaremill.clippy\n\nimport com.softwaremill.clippy.BsUtils._\nimport com.softwaremill.clippy.Utils._\nimport "
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Main.scala",
"chars": 553,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\nimport org.scalajs.jquery._\nimport scala.scalajs.js\nimp"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Menu.scala",
"chars": 2172,
"preview": "package com.softwaremill.clippy\n\nimport com.softwaremill.clippy.App._\nimport japgolly.scalajs.react._\nimport japgolly.sc"
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Use.scala",
"chars": 315,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react._\nimport japgolly.scalajs.react.vdom.prefix_<^._\n\nobject "
},
{
"path": "ui-client/src/main/scala/com/softwaremill/clippy/Utils.scala",
"chars": 569,
"preview": "package com.softwaremill.clippy\n\nimport japgolly.scalajs.react.BackendScope\nimport japgolly.scalajs.react.extra.External"
},
{
"path": "ui-shared/src/main/scala/com/softwaremill/clippy/AdviceState.scala",
"chars": 145,
"preview": "package com.softwaremill.clippy\n\nobject AdviceState extends Enumeration {\n type AdviceState = Value\n val Pending, Acce"
},
{
"path": "ui-shared/src/main/scala/com/softwaremill/clippy/Contributor.scala",
"chars": 128,
"preview": "package com.softwaremill.clippy\n\ncase class Contributor(email: Option[String], twitter: Option[String], github: Option[S"
},
{
"path": "ui-shared/src/main/scala/com/softwaremill/clippy/UiApi.scala",
"chars": 1047,
"preview": "package com.softwaremill.clippy\n\nimport scala.concurrent.Future\n\ntrait UiApi extends ContributeApi with ListingApi with "
}
]
About this extraction
This page contains the full source code of the softwaremill/scala-clippy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 77 files (185.5 KB), approximately 50.1k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.