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 @@ sonar-scala-plugin sonar-plugin - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT Sonar Scala Plugin Enables analysis of Scala projects into Sonar. @@ -111,6 +111,25 @@ ${sonar.version} test + + + org.scalastyle + scalastyle_${scala.major.version} + 0.8.0 + + + org.scala-lang + scala-library + + + + + + org.sonarsource.sonarqube + sonar-core + ${sonar.version} + test + diff --git a/sonar-project.properties b/sonar-project.properties index 379e17b..ab8daf5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ # Required metadata sonar.projectName=Sonar Scala Plugin -sonar.projectVersion=0.0.1 +sonar.projectVersion=0.0.2 # Comma-separated paths to directories with sources (required) sonar.sources=src diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/Constants.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/Constants.scala new file mode 100755 index 0000000..45d68d0 --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/Constants.scala @@ -0,0 +1,29 @@ +/* + * 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 + +object Constants { + val ScalaKey = "scala" + val RepositoryKey = "Scalastyle" + val RepositoryName = "Scalastyle Rules" + val ProfileName = "Scalastyle" + + /** the class of the checker that should be executed by the sonar rule */ + val ClazzParam = "scalastyle-checker" +} diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/RepositoryRule.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/RepositoryRule.scala new file mode 100755 index 0000000..cbe209b --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/RepositoryRule.scala @@ -0,0 +1,29 @@ +/* + * 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.sonar.api.server.rule.RuleParamType + +case class RepositoryRule(clazz : String, id : String, description : String, params : List[Param]) + +case class Param(name: String, `type`: RuleParamType, desc: String, defaultVal: String) + + + + diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleQualityProfile.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleQualityProfile.scala new file mode 100644 index 0000000..ef324b5 --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleQualityProfile.scala @@ -0,0 +1,93 @@ +/* + * 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.slf4j.LoggerFactory +import org.sonar.api.profiles.{ProfileDefinition, RulesProfile} +import org.sonar.api.rules.{RuleFinder, ActiveRule} +import org.sonar.api.utils.ValidationMessages +import org.scalastyle.ScalastyleError +import scala.xml.XML +import collection.JavaConversions._ +import org.sonar.api.rules.RuleQuery +import org.sonar.api.rules.Rule + +/** + * This class creates the default "Scalastyle" quality profile from Scalastyle's default_config.xml + */ +class ScalastyleQualityProfile(ruleFinder: RuleFinder) extends ProfileDefinition { + + private val log = LoggerFactory.getLogger(classOf[ScalastyleQualityProfile]) + private val defaultConfigRules = xmlFromClassPath("/default_config.xml") \\ "scalastyle" \ "check" + + override def createProfile(validation: ValidationMessages): RulesProfile = { + val profile = RulesProfile.create(Constants.ProfileName, Constants.ScalaKey) + val enabledRules = defaultConfigRules filter (x => (x \ "@enabled").text.equals("true")) + val defaultRuleClasses = enabledRules map (x => (x \ "@class").text) + + // currently findAll is buggy (sonar 4.5-5.1 https://jira.sonarsource.com/browse/SONAR-6390) + // will still work but won't add all possible rule to the default profile + val query = RuleQuery.create().withRepositoryKey(Constants.RepositoryKey) + val repoRules = ruleFinder.findAll(query) + + for {clazz <- defaultRuleClasses} { + val ruleOption = repoRules.find(clazzMatch(_, clazz)) + + ruleOption match { + case None => validation.addWarningText(s"Rule for $clazz not found in ${Constants.RepositoryKey} repository! Rule won't be activated.") + case Some(rule) => { + if (!rule.isTemplate()) { + val activated = profile.activateRule(rule, rule.getSeverity) + setParameters(activated, clazz) + } + } + } + } + + profile + } + + def setParameters(activeRule: ActiveRule, clazz: String) { + // set parameters + defaultConfigRules.find(x => (x \ "@class").text.equals(clazz)) match { + case Some(rule) => { + val params = (rule \ "parameters" \ "parameter").map(n => ((n \ "@name").text, n.text)).toMap + params foreach { case (key, value) => activeRule.setParameter(key, value) } + } + case _ => log.warn("Default rule with key " + activeRule.getRuleKey + " could not found in default_config.xml") + } + + // set synthetic parameter + activeRule.setParameter(Constants.ClazzParam, clazz) + } + + private def clazzMatch(rule: Rule, clazz: String): Boolean = { + Option(rule.getParam(Constants.ClazzParam)) match { + case Some(param) => { + param.getDefaultValue.equals(clazz) + } + case None => { + log.warn(s"Could not find required parameter ${Constants.ClazzParam}, rule for $clazz cannot be activated") + false + } + } + } + + private def xmlFromClassPath(s: String) = XML.load(classOf[ScalastyleError].getResourceAsStream(s)) +} diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepository.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepository.scala new file mode 100755 index 0000000..59df9a7 --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepository.scala @@ -0,0 +1,106 @@ +/* + * 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.sonar.api.rule.Severity +import org.sonar.api.server.rule.RulesDefinition +import org.sonar.api.server.rule.RuleParamType +import org.slf4j.LoggerFactory +import org.sonar.api.server.rule.RulesDefinition.NewRepository +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository.getStandardKey +import scala.annotation.tailrec + +object ScalastyleRepository { + + def getStandardKey(clazz: String) = { + val simpleClazz = clazz.reverse.takeWhile(_ != '.').reverse + s"scalastyle_${simpleClazz}" + } +} + +/** + * Scalastyle rules repository - creates a rule for each checker shipped with Scalastyle based + * on the scalastyle_definition.xml file that ships with the Scalastyle jar. + */ +class ScalastyleRepository extends RulesDefinition { + + override def define(context: RulesDefinition.Context): Unit = { + val repository = context + .createRepository(Constants.RepositoryKey, Constants.ScalaKey) + .setName(Constants.RepositoryName) + + ScalastyleResources.allDefinedRules foreach { + repoRule => + { + val ruleKey = determineFreeRuleKey(repoRule.clazz, repository) + + // define the rule + val rule = repository.createRule(ruleKey) + rule.setName(ScalastyleResources.label(repoRule.id)) + rule.setHtmlDescription(repoRule.description) + + // currently all rules comes with "warning" default level so we can treat with major severity + rule.setSeverity(Severity.MAJOR) + + // add parameters + repoRule.params foreach { + param => + { + rule + .createParam(param.name) + .setDefaultValue(param.defaultVal) + .setType(param.`type`) + .setDescription(param.desc) + } + } + + // add synthetic parameter as reference to the class + rule.createParam(Constants.ClazzParam) + .setDefaultValue(repoRule.clazz) + .setType(RuleParamType.STRING) + .setDescription("Scalastyle checker that validates the rule.") + + // if a rule has at least one real parameter make it a template + rule.setTemplate(repoRule.params.size > 0) + + } + } + + repository.done() + } + + /** + * determines a free rule key in the repo, in case the key scalastyle- is already + * in use the name scalastyle__ is tried i = 1, 2, .... + */ + private def determineFreeRuleKey(clazz: String, repo: NewRepository): String = { + @tailrec + def getFreeRuleKey(key: String, count: Int, repo: NewRepository): String = { + val testee = if (count == 0) key else "$key_$count" + if (repo.rule(testee) == null) { + testee + } else { + getFreeRuleKey(key, (count + 1), repo) + } + } + + getFreeRuleKey(getStandardKey(clazz), 0, repo) + } + +} diff --git a/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResources.scala b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResources.scala new file mode 100755 index 0000000..23d69b5 --- /dev/null +++ b/src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResources.scala @@ -0,0 +1,156 @@ +/* + * 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.InputStream +import com.typesafe.config.ConfigFactory +import org.scalastyle.ScalastyleError +import org.sonar.api.server.rule.RuleParamType +import scala.io.Source +import scala.xml.{Elem, XML, Node} + +/** + * Provides access to the various .property and XML files that Scalastyle provides + * to describe its checkers. + */ +object ScalastyleResources { + + // accessing scalastyles definition and documentation.xml files + private val definitions = xmlFromClassPath("/scalastyle_definition.xml") + private val documentation = xmlFromClassPath("/scalastyle_documentation.xml") + + // accessing scalastyles reference.conf (includes additional data such as key.label) + private val cfg = ConfigFactory.load(this.getClass.getClassLoader) + + def allDefinedRules: Seq[RepositoryRule] = for { + checker <- definitions \\ "checker" + clazz = (checker \ "@class").text + id = (checker \ "@id").text + desc = description(id) + params = nodeToParams(checker, id) + } yield RepositoryRule(clazz, id, desc, params) + + def nodeToParams(checker: Node, id: String): List[Param] = for { + parameter <- (checker \\ "parameter").toList + ruleParamKey = nodeToRuleParamKey(parameter) + ruleParamType = nodeToRuleParamType(parameter) + description = nodeToPropertyDescription(parameter, id) + defaultValue = nodeToDefaultValue(parameter) + } yield Param(ruleParamKey, ruleParamType, description, defaultValue) + + def description(key: String): String = descriptionFromDocumentation(key) getOrElse cfg.getConfig(key).getString("description") + + def label(key: String): String = cfg.getConfig(key).getString("label") + + private def descriptionFromDocumentation(key: String): Option[String] = { + documentation \\ "scalastyle-documentation" \ "check" find { _ \\ "@id" exists (_.text == key) } match { + case Some(node) => + val justification = { + val text = (node \ "justification").text + if (text.trim != "") Some(ScalastyleDocFormatter.format(text)) else None + } + val extraDescription = { + val text = (node \ "extra-description").text + if (text.trim != "") Some(ScalastyleDocFormatter.format(text)) else None + } + (justification, extraDescription) match { + case (Some(j), Some(ed)) => Some(s"$j\n$ed") + case (Some(j), None) => Some(j) + case _ => None + } + case None => None + } + } + + private def nodeToRuleParamKey(n: Node): String = (n \ "@name").text.trim + + private def nodeToRuleParamType(n: Node): RuleParamType = (n \ "@type").text.trim match { + case "string" => if ((n \ "@name").text == "regex") { + RuleParamType.STRING + } else if ((n \ "@name").text == "header") { + RuleParamType.TEXT + } else { + RuleParamType.STRING + } + case "integer" => RuleParamType.INTEGER + case "boolean" => RuleParamType.BOOLEAN + case _ => RuleParamType.STRING + } + + private def nodeToPropertyDescription(node: Node, id: String): String = { + val key = nodeToRuleParamKey(node) + description(s"$id.$key") + } + + private def nodeToDefaultValue(n: Node): String = (n \ "@default").text.trim + + private def xmlFromClassPath(s: String): Elem = XML.load(fromClassPath(s)) + + private def fromClassPath(s: String): InputStream = classOf[ScalastyleError].getResourceAsStream(s) +} + +object ScalastyleDocFormatter { + + private case class Out(pre: Boolean, appended: Boolean, text: String) + private case class LineWithLeadingSpaces(spaceCount: Int, empty: Boolean, line: String) + private case class DocLine(pre: Boolean, empty: Boolean, line: String) + + private def empty(line: String) = line.trim == "" + private def countLeadingSpaces(line: String) = { + val count = line.takeWhile(_ == ' ').length + LineWithLeadingSpaces(count, empty(line), line) + } + private val margin = 2 + + def format(in: String): String = { + val linesWithLeadingSpaces = Source.fromString(in).getLines().map(countLeadingSpaces).toList + val docLines = linesWithLeadingSpaces.map(l => DocLine(l.spaceCount > margin, l.empty, l.line)) + + docLines.foldLeft(Out(pre = false, appended = false, "")) { + case (out @ Out(false, false, text), line) => + if (line.empty) out + else if (line.pre) Out(pre = true, appended = true, text + s"

${line.line}\n")
+        else Out(pre = false, appended = true, text + s"

${line.line.trim}\n") + + case (out @ Out(false, true, text), line) => + if (line.empty) out.copy(appended = false, text = text.trim + "

\n") + else if (line.pre) Out(pre = true, appended = true, text + s"

\n

${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])) + } +}