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: Example 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-plugin 2.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)); + } + +}