-
Notifications
You must be signed in to change notification settings - Fork 4
Update scalastyle, switch to Scala-based parsing, adopt new Engines requirements #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aa8e486
3dbb299
e335af5
a838046
3b5d1eb
f64faae
5992173
4460a78
afd8d35
4038b75
4d825b2
e8efde9
a33f906
e278d18
bfc11b6
04844cd
0b75954
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
--- | ||
engines: | ||
scalastyle: | ||
enabled: true | ||
config: | ||
config: src/main/resources/scalastyle_project.xml | ||
exclude_paths: | ||
- "**/src/test/" | ||
- "**/target/" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
target | ||
.idea | ||
|
||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,19 @@ | ||
FROM java | ||
FROM openjdk:alpine | ||
|
||
RUN apt-get update | ||
RUN apt-get install -y ruby ruby-nokogiri | ||
LABEL maintainer "Ivan Luzyanin <[email protected]>" | ||
LABEL maintainer "Jeff Sippel <[email protected]>" | ||
|
||
RUN adduser --uid 9000 --disabled-password --quiet --gecos "" app | ||
USER app | ||
RUN apk update && apk upgrade | ||
|
||
WORKDIR /home/app | ||
RUN addgroup -g 9000 -S code && \ | ||
adduser -S -G code app | ||
USER app | ||
|
||
COPY scalastyle_config.xml /home/app/ | ||
COPY scalastyle_2.11-0.6.0-batch.jar /home/app/ | ||
COPY codeclimate-scalastyle-assembly-0.1.0.jar /usr/src/app/engine-core.jar | ||
COPY src/main/resources/docker /usr/src/app | ||
COPY src/main/resources/docker/engine.json / | ||
|
||
COPY . /home/app | ||
WORKDIR /code | ||
VOLUME /code | ||
|
||
CMD ["/home/app/bin/scalastyle"] | ||
CMD ["/usr/src/app/bin/scalastyle"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,15 @@ | |
|
||
`scalastyle` is a configurable style linter for Scala code. | ||
|
||
### Building | ||
1. Install [sbt](http://www.scala-sbt.org/) | ||
2. Run `sbt docker` | ||
|
||
|
||
### Building release docker image | ||
1. Run `sbt assembly && cp target/scala-2.12/codeclimate-scalastyle-assembly-<version>.jar ./`. | ||
This will create assembled jar with all dependencies. | ||
2. Run `docker build -t codeclimate/codeclimate-scalastyle .` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine for now, but it would be great to have this step integrated within the docker image build process, maybe via a multi-stage Dockerfile: https://docs.docker.com/engine/userguide/eng-image/multistage-build There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand SBT build tool is not supported thus this is the only workaround I could think of. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are quite a few SBT images out there. Docker definitely can run it. Or have I misunderstood your words? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I get what you are saying. Basically use a docker image with sbt to build and assembly package and copy it into the final image. I'll do that. |
||
|
||
### Installation | ||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
name := "codeclimate-scalastyle" | ||
organization in ThisBuild := "codeclimate" | ||
version in ThisBuild := "0.1.0" | ||
scalaVersion in ThisBuild := "2.12.4" | ||
|
||
concurrentRestrictions in Global += Tags.limit(Tags.Test, 1) | ||
parallelExecution in Global := false | ||
|
||
lazy val `engine-core` = project settings ( | ||
libraryDependencies ++= Seq( | ||
"org.scalastyle" %% "scalastyle" % "1.0.0", | ||
"io.circe" %% "circe-parser" % "0.8.0", | ||
"io.circe" %% "circe-generic" % "0.8.0", | ||
"com.github.scopt" %% "scopt" % "3.7.0", | ||
"org.scalactic" %% "scalactic" % "3.0.4", | ||
"org.scalatest" %% "scalatest" % "3.0.4" % "test" | ||
) | ||
) | ||
|
||
lazy val `codeclimate-scalastyle` = project in file(".") dependsOn `engine-core` | ||
|
||
resolvers in ThisBuild ++= Seq( | ||
Resolver.sonatypeRepo("snapshots"), | ||
Resolver.sonatypeRepo("releases") | ||
) | ||
|
||
enablePlugins(sbtdocker.DockerPlugin) | ||
|
||
imageNames in docker := Seq( | ||
// Sets the latest tag | ||
ImageName(s"codeclimate/${name.value}:latest"), | ||
|
||
// Sets a name with a tag that contains the project version | ||
ImageName( | ||
namespace = Some("codeclimate"), | ||
repository = name.value, | ||
tag = Some(version.value) | ||
) | ||
) | ||
|
||
dockerfile in docker := { | ||
val dockerFiles = { | ||
val resources = (unmanagedResources in Runtime).value | ||
val dockerFilesDir = resources.find(_.getPath.endsWith("/docker")).get | ||
resources.filter(_.getPath.contains("/docker/")).map { r => | ||
(dockerFilesDir.toURI.relativize(r.toURI).getPath, r) | ||
}.toMap | ||
} | ||
|
||
new Dockerfile { | ||
from("openjdk:alpine") | ||
|
||
// add all dependencies to docker image instead of assembly (layers the dependencies instead of huge assembly) | ||
val dependencies = { | ||
((dependencyClasspath in Runtime) in `engine-core`).value | ||
}.map(_.data).toSet + ((packageBin in Compile) in `engine-core`).value | ||
|
||
maintainer("Jeff Sippel", "[email protected]") | ||
maintainer("Ivan Luzyanin", "[email protected]") | ||
|
||
add(dependencies.toSeq, "/usr/src/app/dependencies/") | ||
add(((packageBin in Compile) in `engine-core`).value, "/usr/src/app/engine-core.jar") | ||
add(dockerFiles("scalastyle_config.xml"), "/usr/src/app/") | ||
add(dockerFiles("engine.json"), "/") | ||
add(dockerFiles("bin/scalastyle"), "/usr/src/app/bin/") | ||
|
||
runRaw("apk update && apk upgrade") | ||
|
||
runRaw("addgroup -g 9000 -S code && adduser -S -G code app") | ||
|
||
user("app") | ||
|
||
workDir("/code") | ||
volume("/code") | ||
|
||
cmd("/usr/src/app/bin/scalastyle") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package org.codeclimate.scalastyle | ||
|
||
import java.io.File | ||
import java.nio.charset.Charset | ||
import java.nio.file.Files | ||
|
||
import io.circe.parser.parse | ||
import org.scalastyle._ | ||
|
||
import scala.collection.JavaConverters._ | ||
import scala.util.Try | ||
|
||
object CodeClimateEngine extends App { | ||
case class ProgramArgs(config_file_path: String, workspace_path: String) | ||
|
||
val argsParser = new scopt.OptionParser[ProgramArgs]("scopt") { | ||
opt[String]("config_file_path").action { case (path, conf) => | ||
conf.copy(config_file_path = path) | ||
} | ||
|
||
opt[String]("workspace_path").action { case (path, conf) => | ||
conf.copy(workspace_path = path) | ||
} | ||
} | ||
|
||
val defaultStyleConfigurationPath = "/usr/src/app/scalastyle_config.xml" | ||
|
||
argsParser.parse(args, ProgramArgs("/config.json", "/code")) match { | ||
case Some(programArgs) => | ||
val configFile = new File(programArgs.config_file_path) | ||
val configJson = Try { | ||
Files.readAllLines(configFile.toPath, Charset.defaultCharset()).asScala.toSeq.mkString("\n") | ||
}.toEither | ||
|
||
val providedConfig = configJson.right.flatMap(parse).right | ||
.map(config => config.hcursor) | ||
|
||
val includePaths = providedConfig.right.flatMap(_.downField("include_paths").as[Seq[String]]).toOption.getOrElse(Seq.empty) | ||
val configPath = providedConfig.right.flatMap(_.downField("config").downField("config").as[String]).toOption.getOrElse(defaultStyleConfigurationPath) | ||
|
||
val config = ScalastyleCodeClimateConfiguration(configPath, includePaths, Seq.empty) | ||
|
||
val ccPrinter = new CodeClimateIssuePrinter(programArgs.workspace_path, Console.out) | ||
|
||
ScalaStyleRunner.runCheckstyle(programArgs.workspace_path, config) foreach ccPrinter.printIssue | ||
case None => // it will print the error | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package org.codeclimate.scalastyle | ||
|
||
import java.io.{File, PrintStream} | ||
|
||
import com.typesafe.config.ConfigFactory | ||
import io.circe.Printer | ||
import io.circe.generic.auto._ | ||
import io.circe.syntax._ | ||
import org.scalastyle.{FileSpec, Message, MessageHelper, StyleError} | ||
|
||
import scala.collection.JavaConverters._ | ||
|
||
class CodeClimateIssuePrinter(workspacePath: String, ps: PrintStream) { | ||
private val basePath = new File(workspacePath).toPath | ||
private val printer = Printer.noSpaces.copy(dropNullKeys = true) | ||
|
||
private val messageHelper = new MessageHelper(ConfigFactory.load()) | ||
|
||
def printIssue[T <: FileSpec](msg: Message[T]): Unit = msg match { | ||
case se: StyleError[FileSpec] => | ||
val errPosition = Position(se.lineNumber.getOrElse(0), se.column.getOrElse(0)) | ||
val filePath = Option(se.fileSpec.name) | ||
.map(pathname => basePath.relativize(new File(pathname).toPath)) | ||
.map(_.toString) | ||
.getOrElse(se.fileSpec.name) | ||
|
||
val location = Location(path = filePath, positions = LinePosition( | ||
errPosition, errPosition | ||
)) | ||
val msg: String = se.customMessage.orElse { | ||
Some(messageHelper.message(se.key, se.args)) | ||
}.getOrElse("Error message not provided") | ||
val issue = Issue(location = location, | ||
description = String.format(msg, se.args.asJava), | ||
check_name = Some(se.clazz.getName), | ||
categories = Seq("Style"), | ||
severity = Some("major") | ||
) | ||
val jsonStr = printer.pretty(issue.asJson) | ||
ps.print(jsonStr) | ||
ps.print("\0") | ||
case _ => // ignore | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package org.codeclimate.scalastyle | ||
|
||
import java.io.File | ||
|
||
import org.scalastyle._ | ||
|
||
/** | ||
* Computes files and run ScalastyleChecker against them. | ||
*/ | ||
private object ScalaStyleRunner { | ||
def runCheckstyle(workspacePath: String, ccConfig: ScalastyleCodeClimateConfiguration): Seq[Message[FileSpec]] = { | ||
val paths = if (ccConfig.include_paths.isEmpty) { | ||
Seq(workspacePath) | ||
} else { | ||
ccConfig.include_paths.map(include => s"$workspacePath/$include") | ||
} | ||
val files = Directory.getFiles(None, paths.map(new File(_)), excludedFiles = ccConfig.exclude_paths) | ||
|
||
val scalastyleConfig = ScalastyleConfiguration.readFromXml(ccConfig.config) | ||
new ScalastyleChecker(None).checkFiles(scalastyleConfig, files) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package org.codeclimate | ||
|
||
package object scalastyle { | ||
private[this] val DEFAULT_REMEDIAION_POINTS = 50000 | ||
|
||
case class ScalastyleCodeClimateConfiguration( | ||
config: String, | ||
include_paths: Seq[String] = Seq.empty, | ||
exclude_paths: Seq[String] = Seq.empty | ||
) | ||
|
||
sealed trait IssueSchema extends Product with Serializable | ||
case class Position(line: Int, column: Int) extends IssueSchema | ||
case class LinePosition(begin: Position, end: Position) extends IssueSchema | ||
case class Location(path: String, positions: LinePosition) extends IssueSchema | ||
case class Issue(location: Location, description: String, check_name: Option[String] = None, | ||
severity: Option[String] = None, remediation_points: Option[Int] = Some(DEFAULT_REMEDIAION_POINTS), | ||
fingerprint: Option[String] = None, `type`: String = "issue", categories: Seq[String] = Seq.empty | ||
) extends IssueSchema | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package apackage | ||
|
||
class TestFile { | ||
val MyVariable = "" | ||
|
||
def foobar(s: String) = { | ||
var a = 1 | ||
lazy var b = a | ||
a = 2 | ||
b | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package apackage | ||
|
||
class TestFileToIgnore { | ||
val pi = Some(3.14) | ||
} // it should at least report missing new line |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package org.codeclimate.scalastyle | ||
|
||
import org.scalastyle.StyleError | ||
import org.scalatest.Matchers | ||
|
||
class CodeClimateRunnerTest extends org.scalatest.FreeSpec with Matchers { | ||
val workspacePath = "engine-core" | ||
|
||
val codeClimateConfiguration = ScalastyleCodeClimateConfiguration( | ||
config = "engine-core/src/test/resources/scalastyle_config.xml", | ||
include_paths = Seq("src/test/resources") | ||
) | ||
|
||
"CodeClimateEngine" - { | ||
"should call sclacheck and produce style errors for both files in apackage" in { | ||
val msgs = ScalaStyleRunner.runCheckstyle(workspacePath, codeClimateConfiguration) | ||
|
||
msgs should not be empty | ||
|
||
val styleErrors = msgs.flatMap { | ||
case se: StyleError[_] => Seq(se) | ||
case _ => Seq.empty | ||
} | ||
|
||
styleErrors should have size 3 // pre-computed number of issues | ||
} | ||
|
||
"should ignore files specified in `exclude_paths`" in { | ||
val msgs = ScalaStyleRunner.runCheckstyle(workspacePath, codeClimateConfiguration.copy( | ||
exclude_paths = Seq("TestFileToIgnore")) | ||
) | ||
|
||
val files = msgs.flatMap { | ||
case se: StyleError[_] => Seq(se.fileSpec.name) | ||
case _ => Seq.empty | ||
} | ||
|
||
files filter(_.contains("TestFileToIgnore")) shouldBe empty | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This repo might benefit from a
.dockerignore
file as well.I don't think the
build.sbt
orcodeclimate-scalastyle-assembly-0.1.0.jar
files need to be within the final image.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
codeclimate-scalastyle-assembly-0.1.0.jar
is required - it has all the logic for the engine. It is copied as "/usr/src/app/engine-core.jar".Only files in "src/main/resources/docker" and this jar file are included in the docker image.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think once it's COPY'd in, as
/usr/src/app/engine-core.jar
you can leave the original jar out of the final image.So if a
.dockerignore
file includedcodeclimate-scalastyle-assembly-0.1.0.jar
, that would remove the duplicate file, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dockerfile only copies these files:
Thus
codeclimate-scalastyle-assembly-0.1.0.jar
is included only once in the docker image.Adding it to
.dockerignore
will break the build process because Docker wouldn't be able to find this jar.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I see. You're right. Thought there was a broader
COPY
further down, but after looking again, there isn't. LGTM.