diff --git a/.gitignore b/.gitignore
index c58d83b..eacd57f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,8 @@
*.log
# sbt specific
-.cache
-.history
+.cache/
+.history/
.lib/
dist/*
target/
@@ -15,3 +15,13 @@ project/plugins/project/
# Scala-IDE specific
.scala_dependencies
.worksheet
+.classpath
+.project
+.settings
+.cache
+
+# Intellij IDEA Specific
+.idea/*
+*.iml
+*.iws
+*.ipr
diff --git a/README.md b/README.md
index 0cbaae2..4274630 100644
--- a/README.md
+++ b/README.md
@@ -39,3 +39,6 @@ Here is a list of the main ones.
[SonarSource/sonar-examples](https://github.com/SonarSource/sonar-examples)
[NCR-CoDE/sonar-scalastyle](https://github.com/NCR-CoDE/sonar-scalastyle)
+
+# Integration
+Sonar-scala integrates the latest code from the [Sonar Scalastyle Plugin](https://github.com/NCR-CoDE/sonar-scalastyle). As both project are licensed under LGPL3 this shouldn't be a problem.
diff --git a/pom.xml b/pom.xml
index a06c3d7..9aea572 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
${line.line.trim}\n")
+
+ case (out @ Out(false, true, text), line) =>
+ if (line.empty) out.copy(appended = false, text = text.trim + "${line.line}\n")
+ else Out(pre = false, appended = true, text + s"
${line.line}\n") + else Out(pre = false, appended = true, text + s"${line.line.trim}\n") + + case (out @ Out(true, false, text), line) => + if (line.empty) out.copy(text = text + "\n") + else if (line.pre) Out(pre = true, appended = true, text + s"${line.line}\n") + else Out(pre = false, appended = true, text.trim + s"\n
${line.line.trim}\n") + + case (out @ Out(true, true, text), line) => + if (line.empty) out.copy(appended = false, text = text + "\n") + else if (line.pre) Out(pre = true, appended = true, text + s"${line.line}\n") + else Out(pre = false, appended = true, text + s"
\n${line.line.trim}\n") + + } match { + case Out(true, _, text) => + text.trim + "
" + case Out(false, true, text) => + text.trim + "" + case Out(false, false, text) => + text.trim + } + } + +} diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunner.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunner.scala new file mode 100755 index 0000000..df5bd20 --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunner.scala @@ -0,0 +1,73 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ + +package com.ncredinburgh.sonar.scalastyle + +import java.io.File + +import org.scalastyle.StyleError +import org.scalastyle.StyleException +import org.scalastyle.Message +import org.scalastyle.FileSpec +import org.scalastyle.Directory +import org.scalastyle.ScalastyleChecker +import org.scalastyle.ErrorLevel +import org.scalastyle.ScalastyleConfiguration +import org.scalastyle.ConfigurationChecker +import org.slf4j.LoggerFactory +import org.sonar.api.profiles.RulesProfile +import org.sonar.api.rules.ActiveRule +import scala.collection.JavaConversions._ + +/** + * Runs Scalastyle based on active rules in the given RulesProfile + */ +class ScalastyleRunner(rp: RulesProfile) { + private val log = LoggerFactory.getLogger(classOf[ScalastyleRunner]) + + def run(encoding: String, files: java.util.List[File]): List[Message[FileSpec]] = { + log.debug("Using config " + config) + + val fileSpecs = Directory.getFilesAsJava(Some(encoding), files) + val messages = new ScalastyleChecker[FileSpec]().checkFiles(config, fileSpecs) + + // only errors and exceptions are of interest + messages.collect { _ match { + case e: StyleError[_] => e + case ex: StyleException[_] => ex + }} + + } + + def config: ScalastyleConfiguration = { + val sonarRules = rp.getActiveRulesByRepository(Constants.RepositoryKey) + val checkers = sonarRules.map(ruleToChecker).toList + new ScalastyleConfiguration("sonar", true, checkers) + } + + private def ruleToChecker(activeRule: ActiveRule): ConfigurationChecker = { + val sonarParams = activeRule.getActiveRuleParams.map(p => (p.getKey, p.getValue)).toMap + + val checkerParams = sonarParams.filterNot(keyVal => keyVal._1 == Constants.ClazzParam) + val className = sonarParams(Constants.ClazzParam) + val sonarKey = activeRule.getRuleKey + + ConfigurationChecker(className, ErrorLevel, true, sonarParams, None, Some(sonarKey)) + } +} diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensor.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensor.scala new file mode 100755 index 0000000..a46e2fb --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensor.scala @@ -0,0 +1,120 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import org.scalastyle._ +import org.slf4j.LoggerFactory +import org.sonar.api.batch.fs.{InputFile, FileSystem} +import org.sonar.api.batch.{Sensor, SensorContext} +import org.sonar.api.component.ResourcePerspectives +import org.sonar.api.issue.{Issuable, Issue} +import org.sonar.api.profiles.RulesProfile +import org.sonar.api.resources.Project +import org.sonar.api.rule.RuleKey +import org.sonar.api.rules.{Rule, RuleFinder, RuleQuery} + + +import scala.collection.JavaConversions._ + + +/** + * Main sensor for return Scalastyle issues to Sonar. + */ +class ScalastyleSensor(resourcePerspectives: ResourcePerspectives, + runner: ScalastyleRunner, + fileSystem: FileSystem, + ruleFinder: RuleFinder) + extends Sensor { + + def this(resourcePerspectives: ResourcePerspectives, + rulesProfile: RulesProfile, + fileSystem: FileSystem, + ruleFinder: RuleFinder) = this(resourcePerspectives, new ScalastyleRunner(rulesProfile), fileSystem, ruleFinder) + + + private val log = LoggerFactory.getLogger(classOf[ScalastyleSensor]) + + private def predicates = fileSystem.predicates() + + private def scalaFilesPredicate = predicates.and(predicates.hasType(InputFile.Type.MAIN), predicates.hasLanguage(Constants.ScalaKey)) + + + override def shouldExecuteOnProject(project: Project): Boolean = { + fileSystem.files(scalaFilesPredicate).nonEmpty + } + + override def analyse(project: Project, context: SensorContext): Unit = { + val files = fileSystem.files(scalaFilesPredicate) + val encoding = fileSystem.encoding.name + val messages = runner.run(encoding, files.toList) + + messages foreach (processMessage(_)) + } + + private def processMessage(message: Message[FileSpec]): Unit = message match { + case error: StyleError[FileSpec] => processError(error) + case exception: StyleException[FileSpec] => processException(exception) + case _ => Unit + } + + private def processError(error: StyleError[FileSpec]): Unit = { + log.debug("Error message for rule " + error.clazz.getName) + + val inputFile = fileSystem.inputFile(predicates.hasPath(error.fileSpec.name)) + val issuable = Option(resourcePerspectives.as(classOf[Issuable], inputFile)) + val rule = findSonarRuleForError(error) + + log.debug("Matched to sonar rule " + rule) + + if (issuable.isDefined) { + addIssue(issuable.get, error, rule) + } else { + log.error("issuable is null, cannot add issue") + } + } + + private def addIssue(issuable: Issuable, error: StyleError[FileSpec], rule: Rule): Unit = { + val lineNum = sanitiseLineNum(error.lineNumber) + val messageStr = error.customMessage getOrElse rule.getName + + val issue: Issue = issuable.newIssueBuilder.ruleKey(rule.ruleKey) + .line(lineNum).message(messageStr).build + issuable.addIssue(issue) + } + + private def findSonarRuleForError(error: StyleError[FileSpec]): Rule = { + val key = Constants.RepositoryKey + val errorKey = error.key // == scalastyle ConfigurationChecker.customId + log.debug("Looking for sonar rule for " + errorKey) + ruleFinder.find(RuleQuery.create.withKey(errorKey).withRepositoryKey(key)) + } + + private def processException(exception: StyleException[FileSpec]): Unit = { + log.error("Got exception message from Scalastyle. " + + "Check you have valid parameters configured for all rules. Exception message was: " + exception.message) + } + + // sonar claims to accept null or a non zero lines, however if it is passed + // null it blows up at runtime complaining it was passed 0 + private def sanitiseLineNum(maybeLine: Option[Int]) = if ((maybeLine getOrElse 0) != 0) { + maybeLine.get + } else { + 1 + } +} diff --git a/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala index d5f0310..a1c346b 100644 --- a/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala +++ b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala @@ -9,6 +9,9 @@ import org.sonar.api.resources.AbstractLanguage import org.sonar.api.SonarPlugin import scalariform.lexer.ScalaLexer import scalariform.lexer.Token +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository +import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile +import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor /** * Defines Scala as a language for SonarQube. @@ -34,7 +37,10 @@ class ScalaPlugin extends SonarPlugin { override def getExtensions: java.util.List[Class[_]] = ListBuffer[Class[_]] ( classOf[Scala], - classOf[ScalaSensor] + classOf[ScalaSensor], + classOf[ScalastyleRepository], + classOf[ScalastyleQualityProfile], + classOf[ScalastyleSensor] ) override val toString = getClass.getSimpleName diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleAdaptedQualityProfileSpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleAdaptedQualityProfileSpec.scala new file mode 100644 index 0000000..219293d --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleAdaptedQualityProfileSpec.scala @@ -0,0 +1,73 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinderWithTemplates +import org.scalatest._ +import org.scalatest.mock.MockitoSugar +import org.sonar.api.profiles.RulesProfile +import org.sonar.api.utils.ValidationMessages + +import scala.collection.JavaConversions._ + +/** + * Tests an adapted ScalastyleQualityProfile, assuming the user instantiated all templates once + */ +class ScalastyleQualityProfileSpec extends FlatSpec with Matchers with MockitoSugar { + trait Fixture { + val validationMessages = ValidationMessages.create + val testee = new ScalastyleQualityProfile(TestRuleFinderWithTemplates) + } + + val rulesCount = 38 + val parametersCount = 21 + + "a scalastyle quality profile" should "create a default profile" in new Fixture { + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getClass shouldEqual classOf[RulesProfile] + rulesProfile.getName shouldEqual Constants.ProfileName + rulesProfile.getLanguage shouldEqual Constants.ScalaKey + } + + "the default quality profile" should "have all the rules in default config" in new Fixture { + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getActiveRules.size shouldBe rulesCount + } + + it should "have all the parameters in default config" in new Fixture { + val totalParameters = parametersCount + (rulesCount * 1) + + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getActiveRules.flatMap(_.getActiveRuleParams).size shouldBe totalParameters + } + + it should "have correct values for parameters" in new Fixture { + val ruleKey = "scalastyle_NumberOfMethodsInTypeChecker" + + val rulesProfile = testee.createProfile(validationMessages) + val rule = rulesProfile.getActiveRule(Constants.RepositoryKey, ruleKey) + val param = rule.getActiveRuleParams.head + + param.getKey shouldBe "maxMethods" + param.getValue shouldBe "30" + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleDefaultQualityProfileSpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleDefaultQualityProfileSpec.scala new file mode 100644 index 0000000..c61a253 --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleDefaultQualityProfileSpec.scala @@ -0,0 +1,61 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinderWithTemplates +import org.scalatest._ +import org.scalatest.mock.MockitoSugar +import org.sonar.api.profiles.RulesProfile +import org.sonar.api.utils.ValidationMessages +import scala.collection.JavaConversions._ +import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinder + +/** + * Tests the default ScalastyleQualityProfile, only rules without parameters, no templates + */ +class ScalastyleDefaultQualityProfileSpec extends FlatSpec with Matchers with MockitoSugar { + trait Fixture { + val validationMessages = ValidationMessages.create + val testee = new ScalastyleQualityProfile(TestRuleFinder) + } + + val rulesCount = 19 // rules without templates + + "a scalastyle quality profile" should "create a default profile" in new Fixture { + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getClass shouldEqual classOf[RulesProfile] + rulesProfile.getName shouldEqual Constants.ProfileName + rulesProfile.getLanguage shouldEqual Constants.ScalaKey + } + + "the default quality profile" should "have all the rules in default config" in new Fixture { + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getActiveRules.size shouldBe rulesCount + } + + it should "have all the parameters in default config" in new Fixture { + val totalParameters = (rulesCount * 1) + + val rulesProfile = testee.createProfile(validationMessages) + + rulesProfile.getActiveRules.flatMap(_.getActiveRuleParams).size shouldBe totalParameters + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepositorySpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepositorySpec.scala new file mode 100644 index 0000000..db5c63b --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepositorySpec.scala @@ -0,0 +1,126 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import org.scalatest._ +import org.sonar.api.rules.RulePriority +import org.sonar.api.server.rule.{RuleParamType, RulesDefinition} + + +import scala.collection.JavaConversions._ + +/** + * Tests ScalastyleRepository + */ +class ScalastyleRepositorySpec extends FlatSpec with Matchers with Inspectors with BeforeAndAfterAll { + + val testee = new ScalastyleRepository + val ctx = new RulesDefinition.Context() + + def rules = ctx.repository(Constants.RepositoryKey).rules() + + override def beforeAll() { + testee.define(ctx) + } + + "a scalastyle repository" should "return a list of rules" in { + rules should not be empty + } + + it should "use the same repository for all rules" in { + forAll(rules) { + r: RulesDefinition.Rule => + r.repository().key() shouldEqual Constants.RepositoryKey + r.repository().name() shouldEqual Constants.RepositoryName + } + } + + it should "consist of 63 rules" in { + rules.size() shouldEqual 63 + } + + it should "set default severity to major" in { + forAll(rules) {r: RulesDefinition.Rule => r.severity() shouldEqual RulePriority.MAJOR.name()} + } + + it should "give a name to every rule" in { + rules.filter(_.name == null) should be(empty) + } + + it should "set the ClazzParam for every rule" in { + rules.filter(_.param(Constants.ClazzParam) == null) should be(empty) + } + + it should "name the rule properly" in { + val rule = rules.find(_.key == "scalastyle_MagicNumberChecker") + rule.get.name shouldEqual "Magic Number" + } + + it should "describe the rule properly" in { + val rule = rules.find(_.key == "scalastyle_MagicNumberChecker") + rule.get.htmlDescription shouldEqual + "Replacing a magic number with a named constant can make code easier to read and understand," + + " and can avoid some subtle bugs.
\n" + + "A simple assignment to a val is not considered to be a magic number, for example:
\n" + + "val foo = 4\n
is not a magic number, but
\n" + + "var foo = 4\n
is considered to be a magic number.
" + } + + it should "determine the parameter of a rule with a parameter" in { + val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") + rule.get.params map (_.key) shouldEqual List("maxParameters", Constants.ClazzParam) + } + + it should "determine parameters of a rule with multiple parameters" in { + val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") + rule.get.params map (_.key) should contain theSameElementsAs List("regex", "ignoreRegex", "ignoreOverride", Constants.ClazzParam) + } + + it should "determine correct type of integer parameters" in { + val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") + rule.get.param("maxParameters").`type` shouldEqual RuleParamType.INTEGER + } + + it should "determine correct type of boolean parameters" in { + val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") + rule.get.param("ignoreOverride").`type` shouldEqual RuleParamType.BOOLEAN + } + + it should "determine correct type of regex parameters" in { + val rule = rules.find(_.key == "scalastyle_ClassTypeParameterChecker") + rule.get.param("regex").`type` shouldEqual RuleParamType.STRING + } + + it should "describe the parameter properly" in { + val rule = rules.find(_.key == "scalastyle_ClassTypeParameterChecker") + rule.get.param("regex").description shouldEqual "Standard Scala regular expression syntax" + } + + it should "provide default parameters to scalastyle preferred defaults for rules with a parameter" in { + val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") + rule.get.param("maxParameters").defaultValue.toInt shouldEqual 8 + } + + it should "provide default parameters to scalastyle preferred defaults for rules with multiple parameters" in { + val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") + rule.get.param("regex").defaultValue shouldEqual "^[a-z][A-Za-z0-9]*(_=)?$" + rule.get.param("ignoreRegex").defaultValue shouldEqual "^$" + rule.get.param("ignoreOverride").defaultValue.toBoolean shouldEqual false + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResourcesSpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResourcesSpec.scala new file mode 100644 index 0000000..fabeea5 --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResourcesSpec.scala @@ -0,0 +1,148 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import org.scalatest.{FlatSpec, Inspectors, Matchers, PrivateMethodTester} +import org.sonar.api.PropertyType +import org.sonar.api.server.rule.RuleParamType + +import scala.xml.Elem + +/** + * Tests ScalastyleResources + */ +class ScalastyleResourcesSpec extends FlatSpec with Matchers with Inspectors with PrivateMethodTester { + + it should "get default_config.xml from Scalastyle jar" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") + assert(definitions.isInstanceOf[Elem]) + } + + it should "get scalastyle_definition.xml from Scalastyle jar" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val scalastyleDefinitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") + assert(scalastyleDefinitions.isInstanceOf[Elem]) + } + + it should "get scalastyle_documentation.xml from Scalastyle jar" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val scalastyleDocumentation = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_documentation.xml") + assert(scalastyleDocumentation.isInstanceOf[Elem]) + } + + "the configuration" should "allow access to description in documentation for a checker" in { + ScalastyleResources.description("line.size.limit") shouldEqual + "Lines that are too long can be hard to read and horizontal scrolling is annoying.
" + } + + it should "return all defined checkers" in { + ScalastyleResources.allDefinedRules.size shouldEqual 63 + } + + it should "give rules a description" in { + forAll(ScalastyleResources.allDefinedRules) {r: RepositoryRule => r.description.length should be > 0} + } + + it should "give rules an id" in { + forAll(ScalastyleResources.allDefinedRules) {r: RepositoryRule => r.id should not be empty} + } + + it should "get all parameters of rules with a parameter" in { + val rule = ScalastyleResources.allDefinedRules.find(_.clazz == "org.scalastyle.scalariform.ParameterNumberChecker") + rule.get.params map (_.name) shouldEqual List("maxParameters") + } + + it should "get all parameters of rules with multiple parameters" in { + val rule = ScalastyleResources.allDefinedRules.find(_.clazz == "org.scalastyle.scalariform.MethodNamesChecker") + rule.get.params map (_.name) shouldEqual List("regex", "ignoreRegex", "ignoreOverride") + } + + it should "get labels from configuration" in { + ScalastyleResources.label("disallow.space.after.token") shouldEqual "Space after tokens" + ScalastyleResources.label("no.whitespace.before.left.bracket") shouldEqual "No whitespace before left bracket ''[''" + } + + it should "get description from configuration" in { + ScalastyleResources.description("magic.number") shouldEqual + "Replacing a magic number with a named constant can make code easier to read and understand," + + " and can avoid some subtle bugs.
\n" + + "A simple assignment to a val is not considered to be a magic number, for example:
\n" + + "val foo = 4\n
is not a magic number, but
\n" + + "var foo = 4\n
is considered to be a magic number.
" + + // In case no long description found, return the short description + ScalastyleResources.label("disallow.space.after.token") shouldEqual "Space after tokens" + } + + it should "get parameter key from node" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val nodeToRuleParamKey = PrivateMethod[String]('nodeToRuleParamKey) + + val key = "org.scalastyle.scalariform.ParameterNumberChecker" + val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") + val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" + val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } + + ruleNode match { + case Some(node) => { + val parameter = (node \ "parameters" \ "parameter").head + ScalastyleResources invokePrivate nodeToRuleParamKey(parameter) shouldEqual "maxParameters" + } + case _ => fail("rule with key " + key + "could not found") + } + } + + it should "get property type from node" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val nodeToRuleParamType = PrivateMethod[PropertyType]('nodeToRuleParamType) + + val key = "org.scalastyle.scalariform.ParameterNumberChecker" + val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") + val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" + val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } + + ruleNode match { + case Some(node) => { + val parameter = (node \ "parameters" \ "parameter").head + ScalastyleResources invokePrivate nodeToRuleParamType(parameter) shouldEqual RuleParamType.INTEGER + } + case _ => fail("rule with key " + key + "could not found") + } + } + + it should "get default value from node" in { + val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) + val nodeToDefaultValue = PrivateMethod[String]('nodeToDefaultValue) + + val key = "org.scalastyle.scalariform.ParameterNumberChecker" + val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") + val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" + val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } + + ruleNode match { + case Some(node) => { + val parameter = (node \ "parameters" \ "parameter").head + ScalastyleResources invokePrivate nodeToDefaultValue(parameter) shouldEqual "8" + } + case _ => fail("rule with key " + key + "could not found") + } + } + +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunnerSpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunnerSpec.scala new file mode 100644 index 0000000..4e48190 --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunnerSpec.scala @@ -0,0 +1,116 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import java.io.File +import java.nio.charset.StandardCharsets + +import org.mockito.Mockito._ +import org.scalastyle._ +import org.scalastyle.StyleError +import org.scalatest.mock.MockitoSugar +import org.scalatest.{FlatSpec, Matchers, PrivateMethodTester} +import org.sonar.api.profiles.RulesProfile +import org.sonar.api.rules.{Rule, RulePriority} + +import scala.collection.JavaConversions._ + +/** + * Tests ScalastyleRunner + */ +class ScalastyleRunnerSpec extends FlatSpec with Matchers with MockitoSugar with PrivateMethodTester { + + trait Fixture { + val checker1 = ConfigurationChecker("org.scalastyle.scalariform.MultipleStringLiteralsChecker", ErrorLevel, true, Map(), None, None) + val checker2 = ConfigurationChecker("org.scalastyle.file.HeaderMatchesChecker", ErrorLevel, true, Map("header" -> "// Expected Header Comment"), None, None) + val configuration = ScalastyleConfiguration("sonar", true, List(checker1, checker2)) + val testeeSpy = spy(new ScalastyleRunner(mock[RulesProfile])) + doReturn(configuration).when(testeeSpy).config + val charset = StandardCharsets.UTF_8.name + } + + + "a scalastyle runner" should "report StyleError messages if there are rule violations" in new Fixture { + val files = List(new File("src/test/resources/ScalaFile1.scala")) + + val messages = testeeSpy.run(charset, files).map(_.toString) + + messages should contain ("StyleError key=header.matches args=List() lineNumber=Some(1) column=None customMessage=None") + + } + + it should "not report StyleError messages if there are no violations" in new Fixture { + val files = List(new File("src/test/resources/ScalaFile2.scala")) + + val messages = testeeSpy.run(charset, files) + + messages.length shouldEqual 0 + } + + it should "scan multiple files" in new Fixture { + val files = List(new File("src/test/resources/ScalaFile1.scala"), new File("src/test/resources/ScalaFile2.scala")) + + val messages = testeeSpy.run(charset, files) + + messages.length shouldEqual 1 + } + + it should "convert rules to checker" in { + val ruleToChecker = PrivateMethod[ConfigurationChecker]('ruleToChecker) + val profile = RulesProfile.create(Constants.ProfileName, Constants.ScalaKey) + val testee = new ScalastyleRunner(profile) + val key = "multiple.string.literals" + val className = "org.scalastyle.scalariform.MultipleStringLiteralsChecker" + val rule = Rule.create + rule.setRepositoryKey(Constants.RepositoryKey) + .setKey(className) + .setName(ScalastyleResources.label(key)) + .setDescription(ScalastyleResources.description(key)) + .setConfigKey(key) + .setSeverity(RulePriority.MAJOR) + rule.createParameter + .setKey("allowed") + .setDescription("") + .setType("integer") + .setDefaultValue("1") + rule.createParameter + .setKey("ignoreRegex") + .setDescription("") + .setType("integer") + .setDefaultValue("^""$") + + // add synthetic parameter as reference to the class + rule.createParameter + .setKey(Constants.ClazzParam) + .setDescription("Scalastyle checker that validates the rule.") + .setType("string") + .setDefaultValue("org.scalastyle.scalariform.MultipleStringLiteralsChecker") + + val activeRule = profile.activateRule(rule, rule.getSeverity) + activeRule.setParameter("allowed", "1") + activeRule.setParameter("ignoreRegex", "^""$") + activeRule.setParameter(Constants.ClazzParam, "org.scalastyle.scalariform.MultipleStringLiteralsChecker") + + val checker = testee invokePrivate ruleToChecker(activeRule) + val expectedParameters = Map("allowed" -> "1", "ignoreRegex" -> "^""$", Constants.ClazzParam -> "org.scalastyle.scalariform.MultipleStringLiteralsChecker") + val expectedChecker = ConfigurationChecker(className, ErrorLevel, true, expectedParameters, None, Some(className)) + + checker shouldEqual expectedChecker + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensorSpec.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensorSpec.scala new file mode 100755 index 0000000..e49927e --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensorSpec.scala @@ -0,0 +1,149 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.ncredinburgh.sonar.scalastyle + +import java.io.File +import java.nio.charset.StandardCharsets + +import org.mockito.Matchers._ +import org.mockito.Mockito._ +import org.scalastyle._ +import org.scalastyle.file.FileLengthChecker +import org.scalastyle.scalariform.{ForBraceChecker, IfBraceChecker} +import org.scalatest._ +import org.scalatest.mock.MockitoSugar +import org.sonar.api.batch.SensorContext +import org.sonar.api.batch.fs._ +import org.sonar.api.component.ResourcePerspectives +import org.sonar.api.issue.{Issuable, Issue} +import org.sonar.api.resources.Project +import org.sonar.api.rules.{Rule, RuleFinder, RuleQuery} +import org.sonar.core.issue.DefaultIssueBuilder + +import scala.collection.JavaConversions._ + + +class ScalastyleSensorSpec extends FlatSpec with Matchers with MockitoSugar with PrivateMethodTester { + + trait Fixture { + val fs = mock[FileSystem] + val predicates = mock[FilePredicates] + val project = mock[Project] + val runner = mock[ScalastyleRunner] + val perspective = mock[ResourcePerspectives] + val issuable = mock[Issuable] + val issueBuilder = new DefaultIssueBuilder().componentKey("foo").projectKey("bar") + val rf = mock[RuleFinder] + val aRule = Rule.create("repo", "key") + + val testee = new ScalastyleSensor(perspective, runner, fs, rf) + val context = mock[SensorContext] + + when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List()) + when(fs.encoding).thenReturn(StandardCharsets.UTF_8) + when(fs.predicates()).thenReturn(predicates) + when(perspective.as(any(), any(classOf[InputPath]))).thenReturn(issuable) + when(issuable.newIssueBuilder()).thenReturn(issueBuilder) + when(rf.find(any[RuleQuery])).thenReturn(aRule) + + def mockScalaPredicate(scalaFiles: java.lang.Iterable[File]): Unit= { + val scalaFilesPred = mock[FilePredicate] + val hasTypePred = mock[FilePredicate] + val langPred = mock[FilePredicate] + when(predicates.hasType(InputFile.Type.MAIN)).thenReturn(hasTypePred) + when(predicates.hasLanguage(Constants.ScalaKey)).thenReturn(langPred) + when(predicates.and(hasTypePred, langPred)).thenReturn(scalaFilesPred) + when(fs.files(scalaFilesPred)).thenReturn(scalaFiles) + } + } + + "A Scalastyle Sensor" should "execute when the project have Scala files" in new Fixture { + mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) + + testee.shouldExecuteOnProject(project) shouldBe true + } + + it should "not execute when there isn't any Scala files" in new Fixture { + mockScalaPredicate(List()) + + testee.shouldExecuteOnProject(project) shouldBe false + } + + + + it should "analyse all scala source files in project" in new Fixture { + val scalaFiles = List(new File("foo.scala"), new File("bar.scala")) + mockScalaPredicate(scalaFiles) + + testee.analyse(project, context) + + verify(runner).run(StandardCharsets.UTF_8.name(), scalaFiles) + } + + it should "not create SonarQube issues when there isn't any scalastyle errors" in new Fixture { + mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) + when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List()) + + testee.analyse(project, context) + + verify(issuable, never).addIssue(any[Issue]) + } + + it should "report a scalastyle error as a SonarQube issue" in new Fixture { + mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) + + val error = new StyleError[FileSpec]( + new RealFileSpec("foo.scala", None), + classOf[ForBraceChecker], + "org.scalastyle.scalariform.ForBraceChecker", + WarningLevel, + List(), + None + ) + when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List(error)) + + testee.analyse(project, context) + + verify(issuable, times(1)).addIssue(any[Issue]) + } + + it should "report scalastyle errors as SonarQube issues" in new Fixture { + mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) + + val error1 = new StyleError[FileSpec](new RealFileSpec("foo.scala", None), classOf[FileLengthChecker], + "org.scalastyle.file.FileLengthChecker", WarningLevel, List(), None) + val error2 = new StyleError[FileSpec](new RealFileSpec("bar.scala", None), classOf[IfBraceChecker], + "org.scalastyle.scalariform.IfBraceChecker", WarningLevel, List(), None) + when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List(error1, error2)) + + testee.analyse(project, context) + + verify(issuable, times(2)).addIssue(any[Issue]) + } + + it should "find sonar rule for error" in new Fixture { + val findSonarRuleForError = PrivateMethod[Rule]('findSonarRuleForError) + val error = new StyleError[FileSpec](new RealFileSpec("foo.scala", None), classOf[FileLengthChecker], + "org.scalastyle.file.FileLengthChecker", WarningLevel, List(), None) + + val rule = testee invokePrivate findSonarRuleForError(error) + + rule.getKey shouldEqual "key" + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinder.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinder.scala new file mode 100644 index 0000000..37a0d21 --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinder.scala @@ -0,0 +1,61 @@ +package com.ncredinburgh.sonar.scalastyle.testUtils + +import java.util +import com.ncredinburgh.sonar.scalastyle.{Constants, ScalastyleResources} +import org.sonar.api.rule.RuleKey +import org.sonar.api.rules.{RulePriority, Rule, RuleQuery, RuleFinder} +import scala.collection.JavaConversions._ +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository +import org.sonar.api.server.rule.RuleParamType +import com.ncredinburgh.sonar.scalastyle.RepositoryRule + +object TestRuleFinder extends RuleFinder { + + override def findByKey(repositoryKey: String, key: String): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == repositoryKey && r.getKey == key).orNull + + override def findByKey(key: RuleKey): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == key.repository() && r.getKey == key.rule()).orNull + + override def findById(ruleId: Int): Rule = findAll(RuleQuery.create()).find(r => r.getId == ruleId).orNull + + override def findAll(query: RuleQuery): util.Collection[Rule] = { + ScalastyleResources.allDefinedRules.filterNot(r => isTemplate(r)) map { + defRule => + val rule = Rule.create() + val key = defRule.id + rule.setRepositoryKey(Constants.RepositoryKey) + rule.setLanguage(Constants.ScalaKey) + rule.setKey(ScalastyleRepository.getStandardKey(defRule.clazz)) + rule.setName(ScalastyleResources.label(key)) + rule.setDescription(defRule.description) + rule.setConfigKey(ScalastyleRepository.getStandardKey(defRule.clazz)) + + // currently all rules comes with "warning" default level so we can treat with major severity + rule.setSeverity(RulePriority.MAJOR) + + // add parameters + defRule.params foreach { + param => + rule + .createParameter + .setDefaultValue(param.defaultVal) + .setType(param.`type`.`type`()) + .setKey(param.name) + .setDescription(param.desc) + } + + // add synthetic parameter as reference to the class + rule.createParameter(Constants.ClazzParam) + .setDefaultValue(defRule.clazz) + .setType(RuleParamType.STRING.`type`()) + .setDescription("Scalastyle checker that validates the rule.") + + rule + } + } + + override def find(query: RuleQuery): Rule = ??? + + private def isTemplate(rule: RepositoryRule): Boolean = { + rule.params.size() > 0 + } +} diff --git a/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinderWithTemplates.scala b/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinderWithTemplates.scala new file mode 100644 index 0000000..68c9323 --- /dev/null +++ b/src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinderWithTemplates.scala @@ -0,0 +1,61 @@ +package com.ncredinburgh.sonar.scalastyle.testUtils + +import java.util +import com.ncredinburgh.sonar.scalastyle.{Constants, ScalastyleResources} +import org.sonar.api.rule.RuleKey +import org.sonar.api.rules.{RulePriority, Rule, RuleQuery, RuleFinder} +import scala.collection.JavaConversions._ +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository +import org.sonar.api.server.rule.RuleParamType +import com.ncredinburgh.sonar.scalastyle.RepositoryRule + +object TestRuleFinderWithTemplates extends RuleFinder { + + override def findByKey(repositoryKey: String, key: String): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == repositoryKey && r.getKey == key).orNull + + override def findByKey(key: RuleKey): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == key.repository() && r.getKey == key.rule()).orNull + + override def findById(ruleId: Int): Rule = findAll(RuleQuery.create()).find(r => r.getId == ruleId).orNull + + override def findAll(query: RuleQuery): util.Collection[Rule] = { + ScalastyleResources.allDefinedRules map { + defRule => + val rule = Rule.create() + val key = defRule.id + rule.setRepositoryKey(Constants.RepositoryKey) + rule.setLanguage(Constants.ScalaKey) + rule.setKey(ScalastyleRepository.getStandardKey(defRule.clazz)) + rule.setName(ScalastyleResources.label(key)) + rule.setDescription(defRule.description) + rule.setConfigKey(ScalastyleRepository.getStandardKey(defRule.clazz)) + + // currently all rules comes with "warning" default level so we can treat with major severity + rule.setSeverity(RulePriority.MAJOR) + + // add parameters + defRule.params foreach { + param => + rule + .createParameter + .setDefaultValue(param.defaultVal) + .setType(param.`type`.`type`()) + .setKey(param.name) + .setDescription(param.desc) + } + + // add synthetic parameter as reference to the class + rule.createParameter(Constants.ClazzParam) + .setDefaultValue(defRule.clazz) + .setType(RuleParamType.STRING.`type`()) + .setDescription("Scalastyle checker that validates the rule.") + + rule + } + } + + override def find(query: RuleQuery): Rule = ??? + + private def isTemplate(rule: RepositoryRule): Boolean = { + rule.params.size() > 0 + } +} diff --git a/src/test/scala/com/sagacify/sonar/scala/ScalaPluginSpec.scala b/src/test/scala/com/sagacify/sonar/scala/ScalaPluginSpec.scala new file mode 100755 index 0000000..12960cc --- /dev/null +++ b/src/test/scala/com/sagacify/sonar/scala/ScalaPluginSpec.scala @@ -0,0 +1,52 @@ +/* + * Sonar Scalastyle Plugin + * Copyright (C) 2014 All contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.sagacify.sonar.scala + +import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository +import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile +import org.scalatest.{FlatSpec, Matchers} + +/** + * Tests ScalastylePlugin + */ +class ScalastylePluginSpec extends FlatSpec with Matchers { + + val testee = new ScalaPlugin + + "a scala plugin" should "provide a scala sensor" in { + assert(testee.getExtensions.contains(classOf[ScalaSensor])) + } + + it should "provide a scalastyle sensor" in { + assert(testee.getExtensions.contains(classOf[ScalastyleSensor])) + } + + it should "provide a scalastyle repository" in { + assert(testee.getExtensions.contains(classOf[ScalastyleRepository])) + } + + it should "provide a scala language" in { + assert(testee.getExtensions.contains(classOf[Scala])) + } + + it should "provide a scalastyle quality profile" in { + assert(testee.getExtensions.contains(classOf[ScalastyleQualityProfile])) + } +}