diff --git a/README.md b/README.md
index b2dde7f1..821c0257 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,9 @@ Find below an example of an iOS SonarQube dashboard:
-###Features
+### Features
-- [ ] Complexity
+- [x] Complexity
- [ ] Design
- [x] Documentation
- [x] Duplications
@@ -24,12 +24,12 @@ Find below an example of an iOS SonarQube dashboard:
For more details, see the list of [SonarQube metrics](https://github.com/octo-technology/sonar-objective-c/wiki/Features) implemented or pending.
-###Compatibility
+### Compatibility
- Use 0.3.x releases for SonarQube < 4.3
- Use 0.4.x releases for SonarQube >= 4.3 (4.x and 5.x)
-###Download
+### Download
The latest version is the 0.4.0 and it's available [here](http://bit.ly/18A7OkE).
The latest SonarQube 3.x release is the 0.3.1, and it's available [here](http://bit.ly/1fSwd5I).
@@ -38,36 +38,37 @@ You can also download the latest build of the plugin from [Cloudbees](https://rf
In the worst case, the Maven repository with all snapshots and releases is available here: http://repository-rfelden.forge.cloudbees.com/
-###Prerequisites
+### Prerequisites
- a Mac with Xcode...
- [SonarQube](http://docs.codehaus.org/display/SONAR/Setup+and+Upgrade) and [SonarQube Runner](http://docs.codehaus.org/display/SONAR/Installing+and+Configuring+SonarQube+Runner) installed ([HomeBrew](http://brew.sh) installed and ```brew install sonar-runner```)
- [xctool](https://github.com/facebook/xctool) ([HomeBrew](http://brew.sh) installed and ```brew install xctool```). If you are using Xcode 6, make sure to update xctool (```brew upgrade xctool```) to a version > 0.2.2.
- [OCLint](http://docs.oclint.org/en/dev/intro/installation.html) installed. Version 0.8.1 recommended ([HomeBrew](http://brew.sh) installed and ```brew install https://gist.githubusercontent.com/TonyAnhTran/e1522b93853c5a456b74/raw/157549c7a77261e906fb88bc5606afd8bd727a73/oclint.rb```).
- [gcovr](http://gcovr.com) installed
+- [lizard](https://github.com/terryyin/lizard) installed
-###Installation (once for all your Objective-C projects)
+### Installation (once for all your Objective-C projects)
- Install [the plugin](http://bit.ly/18A7OkE) through the Update Center (of SonarQube) or download it into the $SONARQUBE_HOME/extensions/plugins directory
- Copy [run-sonar.sh](https://rawgithub.com/octo-technology/sonar-objective-c/master/src/main/shell/run-sonar.sh) somewhere in your PATH
- Restart the SonarQube server.
-###Configuration (once per project)
+### Configuration (once per project)
- Copy [sonar-project.properties](https://rawgithub.com/octo-technology/sonar-objective-c/master/sample/sonar-project.properties) in your Xcode project root folder (along your .xcodeproj file)
- Edit the *sonar-project.properties* file to match your Xcode iOS/MacOS project
**The good news is that you don't have to modify your Xcode project to enable SonarQube!**. Ok, there might be one needed modification if you don't have a specific scheme for your test target, but that's all.
-###Analysis
+### Analysis
- Run the script ```run-sonar.sh``` in your Xcode project root folder
- Enjoy or file an issue!
-###Update (once per plugin update)
+### Update (once per plugin update)
- Install the [latest plugin](http://bit.ly/18A7OkE) version
- Copy [run-sonar.sh](https://rawgithub.com/octo-technology/sonar-objective-c/master/src/main/shell/run-sonar.sh) somewhere in your PATH
If you still have *run-sonar.sh* file in each of your project (not recommended), you will need to update all those files.
-###Credits
+### Credits
* **Cyril Picat**
* **Gilles Grousset**
* **Denis Bregeon**
@@ -75,14 +76,15 @@ If you still have *run-sonar.sh* file in each of your project (not recommended),
* **Romain Felden**
* **Mete Balci**
-###History
+### History
+- v0.4.1 (2015/05): added support for Lizard to implement complexity metrics.
- v0.4.0 (2015/01): support for SonarQube >= 4.3 (4.x & 5.x)
- v0.3.1 (2013/10): fix release
- v0.3 (2013/10): added support for OCUnit tests and test coverage
- v0.2 (2013/10): added OCLint checks as SonarQube violations
- v0.0.1 (2012/09): v0 with basic metrics such as nb lines of code, nb lines of comment, nb of files, duplications
-###License
+### License
SonarQube Plugin for Objective C is released under the GNU LGPL 3 license:
http://www.gnu.org/licenses/lgpl.txt
diff --git a/pom.xml b/pom.xml
index 8cf7bd1e..3df5fc60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -182,6 +182,11 @@
sonar-surefire-plugin2.7
+
+ xml-apis
+ xml-apis
+ 1.0.b2
+
diff --git a/sample/sonar-project.properties b/sample/sonar-project.properties
index 41ed559f..58797447 100644
--- a/sample/sonar-project.properties
+++ b/sample/sonar-project.properties
@@ -48,6 +48,10 @@ sonar.sourceEncoding=UTF-8
# Change it only if you generate the file on your own
# sonar.objectivec.oclint.report=sonar-reports/oclint.xml
+# Lizard report generated by run-sonar.sh is stored in sonar-reports/lizard-report.xml
+# Change it only if you generate the file on your own
+# sonar.objectivec.lizard.report=sonar-reports/lizard-report.xml
+
# Paths to exclude from coverage report (tests, 3rd party libraries etc.)
# sonar.objectivec.excludedPathsFromCoverage=pattern1,pattern2
sonar.objectivec.excludedPathsFromCoverage=.*Tests.*
diff --git a/src/main/java/org/sonar/plugins/objectivec/ObjectiveCPlugin.java b/src/main/java/org/sonar/plugins/objectivec/ObjectiveCPlugin.java
index 8fa3b673..3debe84a 100644
--- a/src/main/java/org/sonar/plugins/objectivec/ObjectiveCPlugin.java
+++ b/src/main/java/org/sonar/plugins/objectivec/ObjectiveCPlugin.java
@@ -25,6 +25,7 @@
import org.sonar.api.Properties;
import org.sonar.api.Property;
import org.sonar.api.SonarPlugin;
+import org.sonar.plugins.objectivec.complexity.LizardSensor;
import org.sonar.plugins.objectivec.coverage.CoberturaSensor;
import org.sonar.plugins.objectivec.colorizer.ObjectiveCColorizerFormat;
import org.sonar.plugins.objectivec.core.ObjectiveC;
@@ -53,11 +54,17 @@ public List> getExtensions() {
ObjectiveCSquidSensor.class,
ObjectiveCProfile.class,
+
SurefireSensor.class,
+
CoberturaSensor.class,
+
OCLintRuleRepository.class,
- OCLintSensor.class, OCLintProfile.class,
- OCLintProfileImporter.class
+ OCLintSensor.class,
+ OCLintProfile.class,
+ OCLintProfileImporter.class,
+
+ LizardSensor.class
);
}
diff --git a/src/main/java/org/sonar/plugins/objectivec/ObjectiveCSquidSensor.java b/src/main/java/org/sonar/plugins/objectivec/ObjectiveCSquidSensor.java
index 43bbb5e9..a901ba9b 100644
--- a/src/main/java/org/sonar/plugins/objectivec/ObjectiveCSquidSensor.java
+++ b/src/main/java/org/sonar/plugins/objectivec/ObjectiveCSquidSensor.java
@@ -52,9 +52,6 @@
public class ObjectiveCSquidSensor implements Sensor {
- private final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = {1, 2, 4, 6, 8, 10, 12, 20, 30};
- private final Number[] FILES_DISTRIB_BOTTOM_LIMITS = {0, 5, 10, 20, 30, 60, 90};
-
private final AnnotationCheckFactory annotationCheckFactory;
private Project project;
@@ -91,8 +88,12 @@ private void save(Collection squidSourceFiles) {
File sonarFile = File.fromIOFile(new java.io.File(squidFile.getKey()), project);
- saveFilesComplexityDistribution(sonarFile, squidFile);
- saveFunctionsComplexityDistribution(sonarFile, squidFile);
+ /*
+ * Distribution is saved in the Lizard sensor and therefore it is not possible to save the complexity
+ * distribution here. The functionality has been moved to LizardParser.
+ */
+ //saveFilesComplexityDistribution(sonarFile, squidFile);
+ //saveFunctionsComplexityDistribution(sonarFile, squidFile);
saveMeasures(sonarFile, squidFile);
saveViolations(sonarFile, squidFile);
}
@@ -102,27 +103,18 @@ private void saveMeasures(File sonarFile, SourceFile squidFile) {
context.saveMeasure(sonarFile, CoreMetrics.FILES, squidFile.getDouble(ObjectiveCMetric.FILES));
context.saveMeasure(sonarFile, CoreMetrics.LINES, squidFile.getDouble(ObjectiveCMetric.LINES));
context.saveMeasure(sonarFile, CoreMetrics.NCLOC, squidFile.getDouble(ObjectiveCMetric.LINES_OF_CODE));
- context.saveMeasure(sonarFile, CoreMetrics.FUNCTIONS, squidFile.getDouble(ObjectiveCMetric.FUNCTIONS));
+ /**
+ * Saving the same measure more than once per file throws exception. That is why
+ * CoreMetrics.FUNCTIONS and CoreMetrics.COMPLEXITY are not allowed to be saved here. In order for the
+ * LizardSensor to be able to to its job and save the values for those metrics the functionality has been
+ * moved to Lizard classes.
+ */
+ //context.saveMeasure(sonarFile, CoreMetrics.FUNCTIONS, squidFile.getDouble(ObjectiveCMetric.FUNCTIONS));
context.saveMeasure(sonarFile, CoreMetrics.STATEMENTS, squidFile.getDouble(ObjectiveCMetric.STATEMENTS));
- context.saveMeasure(sonarFile, CoreMetrics.COMPLEXITY, squidFile.getDouble(ObjectiveCMetric.COMPLEXITY));
+ //context.saveMeasure(sonarFile, CoreMetrics.COMPLEXITY, squidFile.getDouble(ObjectiveCMetric.COMPLEXITY));
context.saveMeasure(sonarFile, CoreMetrics.COMMENT_LINES, squidFile.getDouble(ObjectiveCMetric.COMMENT_LINES));
}
- private void saveFunctionsComplexityDistribution(File sonarFile, SourceFile squidFile) {
- Collection squidFunctionsInFile = scanner.getIndex().search(new QueryByParent(squidFile), new QueryByType(SourceFunction.class));
- RangeDistributionBuilder complexityDistribution = new RangeDistributionBuilder(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTIONS_DISTRIB_BOTTOM_LIMITS);
- for (SourceCode squidFunction : squidFunctionsInFile) {
- complexityDistribution.add(squidFunction.getDouble(ObjectiveCMetric.COMPLEXITY));
- }
- context.saveMeasure(sonarFile, complexityDistribution.build().setPersistenceMode(PersistenceMode.MEMORY));
- }
-
- private void saveFilesComplexityDistribution(File sonarFile, SourceFile squidFile) {
- RangeDistributionBuilder complexityDistribution = new RangeDistributionBuilder(CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION, FILES_DISTRIB_BOTTOM_LIMITS);
- complexityDistribution.add(squidFile.getDouble(ObjectiveCMetric.COMPLEXITY));
- context.saveMeasure(sonarFile, complexityDistribution.build().setPersistenceMode(PersistenceMode.MEMORY));
- }
-
private void saveViolations(File sonarFile, SourceFile squidFile) {
Collection messages = squidFile.getCheckMessages();
if (messages != null) {
diff --git a/src/main/java/org/sonar/plugins/objectivec/complexity/LizardMeasurePersistor.java b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardMeasurePersistor.java
new file mode 100644
index 00000000..eeee8279
--- /dev/null
+++ b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardMeasurePersistor.java
@@ -0,0 +1,82 @@
+/*
+ * Sonar Objective-C Plugin
+ * Copyright (C) 2012 OCTO Technology
+ * dev@sonar.codehaus.org
+ *
+ * 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 org.sonar.plugins.objectivec.complexity;
+
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is used to save the measures created by the lizardReportParser in the sonar database
+ *
+ * @author Andres Gil Herrera
+ * @since 28/05/15.
+ */
+public class LizardMeasurePersistor {
+
+ private Project project;
+ private SensorContext sensorContext;
+
+ public LizardMeasurePersistor(final Project p, final SensorContext c) {
+ this.project = p;
+ this.sensorContext = c;
+ }
+
+ /**
+ *
+ * @param measures Map containing as key the name of the file and as value a list containing the measures for that file
+ */
+ public void saveMeasures(final Map> measures) {
+
+ if (measures == null) {
+ return;
+ }
+
+ for (Map.Entry> entry : measures.entrySet()) {
+ final org.sonar.api.resources.File objcfile = org.sonar.api.resources.File.fromIOFile(new File(project.getFileSystem().getBasedir(), entry.getKey()), project);
+ if (fileExists(sensorContext, objcfile)) {
+ for (Measure measure : entry.getValue()) {
+ try {
+ LoggerFactory.getLogger(getClass()).debug("Save measure {} for file {}", measure.getMetric().getName(), objcfile);
+ sensorContext.saveMeasure(objcfile, measure);
+ } catch (Exception e) {
+ LoggerFactory.getLogger(getClass()).error(" Exception -> {} -> {}", entry.getKey(), measure.getMetric().getName());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * @param context context of the sensor
+ * @param file file to prove for
+ * @return true if the resource is indexed and false if not
+ */
+ private boolean fileExists(final SensorContext context,
+ final org.sonar.api.resources.File file) {
+ return context.getResource(file) != null;
+ }
+}
diff --git a/src/main/java/org/sonar/plugins/objectivec/complexity/LizardReportParser.java b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardReportParser.java
new file mode 100644
index 00000000..3a4daf5a
--- /dev/null
+++ b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardReportParser.java
@@ -0,0 +1,251 @@
+/*
+ * Sonar Objective-C Plugin
+ * Copyright (C) 2012 OCTO Technology
+ * dev@sonar.codehaus.org
+ *
+ * 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 org.sonar.plugins.objectivec.complexity;
+
+import org.slf4j.LoggerFactory;
+import org.sonar.api.measures.*;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class parses xml Reports form the tool Lizard in order to extract this measures:
+ * COMPLEXITY, FUNCTIONS, FUNCTION_COMPLEXITY, FUNCTION_COMPLEXITY_DISTRIBUTION,
+ * FILE_COMPLEXITY, FUNCTION_COMPLEXITY_DISTRIBUTION, COMPLEXITY_IN_FUNCTIONS
+ *
+ * @author Andres Gil Herrera
+ * @since 28/05/15
+ */
+public class LizardReportParser {
+
+ private final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = {1, 2, 4, 6, 8, 10, 12, 20, 30};
+ private final Number[] FILES_DISTRIB_BOTTOM_LIMITS = {0, 5, 10, 20, 30, 60, 90};
+
+ private static final String MEASURE = "measure";
+ private static final String MEASURE_TYPE = "type";
+ private static final String MEASURE_ITEM = "item";
+ private static final String FILE_MEASURE = "file";
+ private static final String FUNCTION_MEASURE = "Function";
+ private static final String NAME = "name";
+ private static final String VALUE = "value";
+ private static final int CYCLOMATIC_COMPLEXITY_INDEX = 2;
+ private static final int FUNCTIONS_INDEX = 3;
+
+ /**
+ *
+ * @param xmlFile lizard xml report
+ * @return Map containing as key the name of the file and as value a list containing the measures for that file
+ */
+ public Map> parseReport(final File xmlFile) {
+ Map> result = null;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+
+ try {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document document = builder.parse(xmlFile);
+ result = parseFile(document);
+ } catch (final FileNotFoundException e){
+ LoggerFactory.getLogger(getClass()).error("Lizard Report not found {}", xmlFile, e);
+ } catch (final IOException e) {
+ LoggerFactory.getLogger(getClass()).error("Error processing file named {}", xmlFile, e);
+ } catch (final ParserConfigurationException e) {
+ LoggerFactory.getLogger(getClass()).error("Error parsing file named {}", xmlFile, e);
+ } catch (final SAXException e) {
+ LoggerFactory.getLogger(getClass()).error("Error processing file named {}", xmlFile, e);
+ }
+
+ return result;
+ }
+
+ /**
+ *
+ * @param document Document object representing the lizard report
+ * @return Map containing as key the name of the file and as value a list containing the measures for that file
+ */
+ private Map> parseFile(Document document) {
+ final Map> reportMeasures = new HashMap>();
+ final List functions = new ArrayList();
+
+ NodeList nodeList = document.getElementsByTagName(MEASURE);
+
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ Node node = nodeList.item(i);
+
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ if (element.getAttribute(MEASURE_TYPE).equalsIgnoreCase(FILE_MEASURE)) {
+ NodeList itemList = element.getElementsByTagName(MEASURE_ITEM);
+ addComplexityFileMeasures(itemList, reportMeasures);
+ } else if(element.getAttribute(MEASURE_TYPE).equalsIgnoreCase(FUNCTION_MEASURE)) {
+ NodeList itemList = element.getElementsByTagName(MEASURE_ITEM);
+ collectFunctions(itemList, functions);
+ }
+ }
+ }
+
+ addComplexityFunctionMeasures(reportMeasures, functions);
+
+ return reportMeasures;
+ }
+
+ /**
+ * This method extracts the values for COMPLEXITY, FUNCTIONS, FILE_COMPLEXITY
+ *
+ * @param itemList list of all items from a
+ * @param reportMeasures map to save the measures for each file
+ */
+ private void addComplexityFileMeasures(NodeList itemList, Map> reportMeasures){
+ for (int i = 0; i < itemList.getLength(); i++) {
+ Node item = itemList.item(i);
+
+ if (item.getNodeType() == Node.ELEMENT_NODE) {
+ Element itemElement = (Element) item;
+ String fileName = itemElement.getAttribute(NAME);
+ NodeList values = itemElement.getElementsByTagName(VALUE);
+ int complexity = Integer.parseInt(values.item(CYCLOMATIC_COMPLEXITY_INDEX).getTextContent());
+ double fileComplexity = Double.parseDouble(values.item(CYCLOMATIC_COMPLEXITY_INDEX).getTextContent());
+ int numberOfFunctions = Integer.parseInt(values.item(FUNCTIONS_INDEX).getTextContent());
+
+ reportMeasures.put(fileName, buildMeasureList(complexity, fileComplexity, numberOfFunctions));
+ }
+ }
+ }
+
+ /**
+ *
+ * @param complexity overall complexity of the file
+ * @param fileComplexity file complexity
+ * @param numberOfFunctions number of functions in the file
+ * @return returns a list of tree measures COMPLEXITY, FUNCTIONS, FILE_COMPLEXITY with the values specified
+ */
+ private List buildMeasureList(int complexity, double fileComplexity, int numberOfFunctions){
+ List list = new ArrayList();
+ list.add(new Measure(CoreMetrics.COMPLEXITY).setIntValue(complexity));
+ list.add(new Measure(CoreMetrics.FUNCTIONS).setIntValue(numberOfFunctions));
+ list.add(new Measure(CoreMetrics.FILE_COMPLEXITY, fileComplexity));
+ RangeDistributionBuilder complexityDistribution = new RangeDistributionBuilder(CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION, FILES_DISTRIB_BOTTOM_LIMITS);
+ complexityDistribution.add(fileComplexity);
+ list.add(complexityDistribution.build().setPersistenceMode(PersistenceMode.MEMORY));
+ return list;
+ }
+
+ /**
+ *
+ * @param itemList NodeList of all items in a tag
+ * @param functions list to save the functions in the NodeList as ObjCFunction objects.
+ */
+ private void collectFunctions(NodeList itemList, List functions) {
+ for (int i = 0; i < itemList.getLength(); i++) {
+ Node item = itemList.item(i);
+ if (item.getNodeType() == Node.ELEMENT_NODE) {
+ Element itemElement = (Element) item;
+ String name = itemElement.getAttribute(NAME);
+ String measure = itemElement.getElementsByTagName(VALUE).item(CYCLOMATIC_COMPLEXITY_INDEX).getTextContent();
+ functions.add(new ObjCFunction(name, Integer.parseInt(measure)));
+ }
+ }
+ }
+
+ /**
+ *
+ * @param reportMeasures map to save the measures for the different files
+ * @param functions list of ObjCFunction to extract the information needed to create
+ * FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTION_COMPLEXITY, COMPLEXITY_IN_FUNCTIONS
+ */
+ private void addComplexityFunctionMeasures(Map> reportMeasures, List functions){
+ for (Map.Entry> entry : reportMeasures.entrySet()) {
+
+ RangeDistributionBuilder complexityDistribution = new RangeDistributionBuilder(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTIONS_DISTRIB_BOTTOM_LIMITS);
+ int count = 0;
+ int complexityInFunctions = 0;
+
+ for (ObjCFunction func : functions) {
+ if (func.getName().contains(entry.getKey())) {
+ complexityDistribution.add(func.getCyclomaticComplexity());
+ count++;
+ complexityInFunctions += func.getCyclomaticComplexity();
+ }
+ }
+
+ if (count != 0) {
+ double complex = 0;
+ for (Measure m : entry.getValue()){
+ if (m.getMetric().getKey().equalsIgnoreCase(CoreMetrics.FILE_COMPLEXITY.getKey())){
+ complex = m.getValue();
+ break;
+ }
+ }
+
+ double complexMean = complex/(double)count;
+ entry.getValue().addAll(buildFuncionMeasuresList(complexMean, complexityInFunctions, complexityDistribution));
+ }
+ }
+ }
+
+ /**
+ *
+ * @param complexMean average complexity per function in a file
+ * @param complexityInFunctions Entire complexity in functions
+ * @param builder Builder ready to build FUNCTION_COMPLEXITY_DISTRIBUTION
+ * @return list of Measures containing FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTION_COMPLEXITY and COMPLEXITY_IN_FUNCTIONS
+ */
+ public List buildFuncionMeasuresList(double complexMean, int complexityInFunctions, RangeDistributionBuilder builder){
+ List list = new ArrayList();
+ list.add(new Measure(CoreMetrics.FUNCTION_COMPLEXITY, complexMean));
+ list.add(new Measure(CoreMetrics.COMPLEXITY_IN_FUNCTIONS).setIntValue(complexityInFunctions));
+ list.add(builder.build().setPersistenceMode(PersistenceMode.MEMORY));
+ return list;
+ }
+
+ /**
+ * helper class to process the information the functions contained in a Lizard report
+ */
+ private class ObjCFunction {
+ private String name;
+ private int cyclomaticComplexity;
+
+ public ObjCFunction(String name, int cyclomaticComplexity) {
+ this.name = name;
+ this.cyclomaticComplexity = cyclomaticComplexity;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getCyclomaticComplexity() {
+ return cyclomaticComplexity;
+ }
+
+ }
+}
diff --git a/src/main/java/org/sonar/plugins/objectivec/complexity/LizardSensor.java b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardSensor.java
new file mode 100644
index 00000000..7e1a03aa
--- /dev/null
+++ b/src/main/java/org/sonar/plugins/objectivec/complexity/LizardSensor.java
@@ -0,0 +1,106 @@
+/*
+ * Sonar Objective-C Plugin
+ * Copyright (C) 2012 OCTO Technology
+ * dev@sonar.codehaus.org
+ *
+ * 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 org.sonar.plugins.objectivec.complexity;
+
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+import org.sonar.plugins.objectivec.ObjectiveCPlugin;
+import org.sonar.plugins.objectivec.core.ObjectiveC;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This sensor searches for the report generated from the tool Lizard
+ * in order to save complexity metrics.
+ *
+ * @author Andres Gil Herrera
+ * @since 28/05/15
+ */
+@SuppressWarnings("deprecation")
+public class LizardSensor implements Sensor {
+
+ public static final String REPORT_PATH_KEY = ObjectiveCPlugin.PROPERTY_PREFIX
+ + ".lizard.report";
+ public static final String DEFAULT_REPORT_PATH = "sonar-reports/lizard-report.xml";
+
+ private final Settings conf;
+ private final FileSystem fileSystem;
+
+ public LizardSensor(final FileSystem moduleFileSystem, final Settings config) {
+ this.conf = config;
+ this.fileSystem = moduleFileSystem;
+ }
+
+ /**
+ *
+ * @param project
+ * @return true if the project is root the root project and uses Objective-C
+ */
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return project.isRoot() && fileSystem.languages().contains(ObjectiveC.KEY);
+ }
+
+ /**
+ *
+ * @param project
+ * @param sensorContext
+ */
+ @Override
+ public void analyse(Project project, SensorContext sensorContext) {
+ final String projectBaseDir = project.getFileSystem().getBasedir().getPath();
+ Map> measures = parseReportsIn(projectBaseDir, new LizardReportParser());
+ LoggerFactory.getLogger(getClass()).info("Saving results of complexity analysis");
+ new LizardMeasurePersistor(project, sensorContext).saveMeasures(measures);
+ }
+
+ /**
+ *
+ * @param baseDir base directory of the project to search the report
+ * @param parser LizardReportParser to parse the report
+ * @return Map containing as key the name of the file and as value a list containing the measures for that file
+ */
+ private Map> parseReportsIn(final String baseDir, LizardReportParser parser) {
+ final StringBuilder reportFileName = new StringBuilder(baseDir);
+ reportFileName.append("/").append(reportPath());
+ LoggerFactory.getLogger(getClass()).info("Processing complexity report ");
+ return parser.parseReport(new File(reportFileName.toString()));
+ }
+
+ /**
+ *
+ * @return the default report path or the one specified in the sonar-project.properties
+ */
+ private String reportPath() {
+ String reportPath = conf.getString(REPORT_PATH_KEY);
+ if (reportPath == null) {
+ reportPath = DEFAULT_REPORT_PATH;
+ }
+ return reportPath;
+ }
+}
diff --git a/src/main/shell/run-sonar.sh b/src/main/shell/run-sonar.sh
index f82d33ef..cd7f8ee6 100755
--- a/src/main/shell/run-sonar.sh
+++ b/src/main/shell/run-sonar.sh
@@ -117,6 +117,7 @@ function runCommand() {
vflag=""
nflag=""
oclint="on"
+lizard="on"
while [ $# -gt 0 ]
do
case "$1" in
@@ -293,6 +294,21 @@ else
echo 'Skipping OCLint (test purposes only!)'
fi
+if [ "$lizard" = "on" ]; then
+ if hash lizard 2>/dev/null; then
+ # Lizard
+ echo -n 'Running Lizard...'
+
+ # Run Lizard with xml output option and write the output in sonar-reports/lizard-report.xml
+ lizard --xml "$srcDirs" > sonar-reports/lizard-report.xml
+ else
+ echo 'Skipping Lizard (not installed!)'
+ fi
+
+else
+ echo 'Skipping Lizard (test purposes only!)'
+fi
+
# SonarQube
echo -n 'Running SonarQube using SonarQube Runner'
runCommand /dev/stdout sonar-runner
diff --git a/src/test/java/org/sonar/plugins/objectivec/complexity/LizardReportParserTest.java b/src/test/java/org/sonar/plugins/objectivec/complexity/LizardReportParserTest.java
new file mode 100644
index 00000000..43bc8518
--- /dev/null
+++ b/src/test/java/org/sonar/plugins/objectivec/complexity/LizardReportParserTest.java
@@ -0,0 +1,207 @@
+/*
+ * Sonar Objective-C Plugin
+ * Copyright (C) 2012 OCTO Technology
+ * dev@sonar.codehaus.org
+ *
+ * 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 org.sonar.plugins.objectivec.complexity;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Andres Gil Herrera
+ * @since 03/06/15.
+ */
+public class LizardReportParserTest {
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ private File correctFile;
+ private File incorrectFile;
+
+ @Before
+ public void setup() throws IOException {
+ correctFile = createCorrectFile();
+ incorrectFile = createIncorrectFile();
+ }
+
+ /**
+ *
+ * @return dummy lizard xml report to test the parser
+ * @throws IOException
+ */
+ public File createCorrectFile() throws IOException {
+ File xmlFile = folder.newFile("correctFile.xml");
+ BufferedWriter out = new BufferedWriter(new FileWriter(xmlFile));
+ //header
+ out.write("");
+ out.write("");
+ //root object and measure
+ out.write("");
+ //items for function
+ out.write("");
+ out.write("2151");
+ out.write("");
+ out.write("3205");
+ //average and close funciton measure
+ out.write("");
+ out.write("");
+ out.write("");
+ //open file measure and add the labels
+ out.write("");
+ //items for file
+ out.write("");
+ out.write("1200");
+ out.write("");
+ out.write("286862");
+ //add averages
+ out.write("");
+ //add sum
+ out.write("");
+ //close measures and root object
+ out.write("");
+
+ out.close();
+
+ return xmlFile;
+ }
+
+ /**
+ *
+ * @return corrupted dummy lizard report to test the parser
+ * @throws IOException
+ */
+ public File createIncorrectFile() throws IOException {
+ File xmlFile = folder.newFile("incorrectFile.xml");
+ BufferedWriter out = new BufferedWriter(new FileWriter(xmlFile));
+ //header
+ out.write("");
+ out.write("");
+ //root object and measure
+ out.write("");
+ //items for function
+ out.write("");
+ out.write("2151");
+ out.write("");
+ out.write("3205");
+ //average and close funciton measure
+ out.write("");
+ out.write("");
+ out.write("");
+ //open file measure and add the labels
+ out.write("");
+ //items for file 3th value tag has no closing tag
+ out.write("");
+ out.write("1200");
+ out.write("");
+ out.write("286862");
+ //add averages
+ out.write("");
+ //add sum
+ out.write("");
+ //close measures and root object no close tag for measure
+ out.write("");
+
+ out.close();
+
+ return xmlFile;
+ }
+
+ /**
+ * this test case test that the parser extract all measures right
+ */
+ @Test
+ public void parseReportShouldReturnMapWhenXMLFileIsCorrect() {
+ LizardReportParser parser = new LizardReportParser();
+
+ assertNotNull("correct file is null", correctFile);
+
+ Map> report = parser.parseReport(correctFile);
+
+ assertNotNull("report is null", report);
+
+ assertTrue("Key is not there", report.containsKey("App/Controller/Accelerate/AccelerationViewController.h"));
+ List list1 = report.get("App/Controller/Accelerate/AccelerationViewController.h");
+ assertEquals(4, list1.size());
+
+ for (Measure measure : list1) {
+ String s = measure.getMetric().getKey();
+
+ if (s.equals(CoreMetrics.FUNCTIONS_KEY)) {
+ assertEquals("Header Functions has a wrong value", 0, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.COMPLEXITY_KEY)) {
+ assertEquals("Header Complexity has a wrong value", 0, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.FILE_COMPLEXITY_KEY)) {
+ assertEquals("Header File Complexity has a wrong value", 0.0d, measure.getValue().doubleValue(), 0.0d);
+ } else if (s.equals(CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY)) {
+ assertEquals("Header Complexity in Functions has a wrong value", 0, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.FUNCTION_COMPLEXITY_KEY)) {
+ assertEquals("Header Functions Complexity has a wrong value", 0.0d, measure.getValue().doubleValue(), 0.0d);
+ }
+ }
+
+ assertTrue("Key is not there", report.containsKey("App/Controller/Accelerate/AccelerationViewController.m"));
+
+ List list2 = report.get("App/Controller/Accelerate/AccelerationViewController.m");
+ assertEquals(7, list2.size());
+ for (Measure measure : list2) {
+ String s = measure.getMetric().getKey();
+
+ if (s.equals(CoreMetrics.FUNCTIONS_KEY)) {
+ assertEquals("MFile Functions has a wrong value", 2, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.COMPLEXITY_KEY)) {
+ assertEquals("MFile Complexity has a wrong value", 6, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.FILE_COMPLEXITY_KEY)) {
+ assertEquals("MFile File Complexity has a wrong value", 6.0d, measure.getValue().doubleValue(), 0.0d);
+ } else if (s.equals(CoreMetrics.COMPLEXITY_IN_FUNCTIONS_KEY)) {
+ assertEquals("MFile Complexity in Functions has a wrong value", 6, measure.getIntValue().intValue());
+ } else if (s.equals(CoreMetrics.FUNCTION_COMPLEXITY_KEY)) {
+ assertEquals("MFile Functions Complexity has a wrong value", 3.0d, measure.getValue().doubleValue(), 0.0d);
+ }
+ }
+ }
+
+ /**
+ * this method test that the parser shoud not return anything if the xml report is corrupted
+ */
+ @Test
+ public void parseReportShouldReturnNullWhenXMLFileIsIncorrect() {
+ LizardReportParser parser = new LizardReportParser();
+
+ assertNotNull("correct file is null", incorrectFile);
+
+ Map> report = parser.parseReport(incorrectFile);
+ assertNull("report is not null", report);
+
+ }
+
+}
diff --git a/src/test/java/org/sonar/plugins/objectivec/complexity/LizardSensorTest.java b/src/test/java/org/sonar/plugins/objectivec/complexity/LizardSensorTest.java
new file mode 100644
index 00000000..55748d17
--- /dev/null
+++ b/src/test/java/org/sonar/plugins/objectivec/complexity/LizardSensorTest.java
@@ -0,0 +1,85 @@
+/*
+ * Sonar Objective-C Plugin
+ * Copyright (C) 2012 OCTO Technology
+ * dev@sonar.codehaus.org
+ *
+ * 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 org.sonar.plugins.objectivec.complexity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.plugins.objectivec.core.ObjectiveC;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Andres Gil Herrera
+ * @since 03/06/15.
+ */
+public class LizardSensorTest {
+
+ private Settings settings;
+
+ @Before
+ public void setUp() {
+ settings = new Settings();
+ }
+
+ /**
+ * this method tests that the sensor should be executed when a project is a root project and uses objective c
+ */
+ @Test
+ public void shouldExecuteOnProjectShouldBeTrueWhenProjectIsObjc() {
+ final Project project = new Project("Test");
+
+ FileSystem fileSystem = mock(FileSystem.class);
+ SortedSet languages = new TreeSet();
+ languages.add(ObjectiveC.KEY);
+ when(fileSystem.languages()).thenReturn(languages);
+
+ final LizardSensor testedSensor = new LizardSensor(fileSystem, settings);
+
+ assertTrue(testedSensor.shouldExecuteOnProject(project));
+ }
+
+ /**
+ * this method tests that the sensor does not get executed when a project dont uses objective c
+ */
+ @Test
+ public void shouldExecuteOnProjectShouldBeFalseWhenProjectIsSomethingElse() {
+ final Project project = new Project("Test");
+
+ FileSystem fileSystem = mock(FileSystem.class);
+ SortedSet languages = new TreeSet();
+ languages.add("Test");
+ when(fileSystem.languages()).thenReturn(languages);
+
+ final LizardSensor testedSensor = new LizardSensor(fileSystem, settings);
+
+ assertFalse(testedSensor.shouldExecuteOnProject(project));
+ }
+
+}