Skip to content

Commit

Permalink
Implement file and function complexity issues based on Lizard report.
Browse files Browse the repository at this point in the history
  • Loading branch information
mjdetullio committed Mar 16, 2016
1 parent 8b4cd85 commit 23b5ec0
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.sonar.plugins.objectivec.clang.ClangSensor;
import org.sonar.plugins.objectivec.cobertura.CoberturaSensor;
import org.sonar.plugins.objectivec.cpd.ObjectiveCCpdMapping;
import org.sonar.plugins.objectivec.lizard.LizardRulesDefinition;
import org.sonar.plugins.objectivec.lizard.LizardSensor;
import org.sonar.plugins.objectivec.oclint.OCLintProfile;
import org.sonar.plugins.objectivec.oclint.OCLintProfileImporter;
Expand Down Expand Up @@ -77,6 +78,7 @@ public List getExtensions() {
.build());

extensions.add(LizardSensor.class);
extensions.add(LizardRulesDefinition.class);
extensions.add(PropertyDefinition.builder(LizardSensor.REPORT_PATH_KEY)
.name("Report path")
.description("Path (absolute or relative) to Lizard XML report file.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,20 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.component.ResourcePerspectives;
import org.sonar.api.issue.Issuable;
import org.sonar.api.issue.Issue;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.PersistenceMode;
import org.sonar.api.measures.RangeDistributionBuilder;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Resource;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.ActiveRule;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
Expand Down Expand Up @@ -67,23 +77,34 @@ public final class LizardReportParser {
private static final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = {1, 2, 4, 6, 8, 10, 12, 20, 30};
private static final Number[] FILES_DISTRIB_BOTTOM_LIMITS = {0, 5, 10, 20, 30, 60, 90};

private LizardReportParser() {
// Prevent outside instantiation
private final FileSystem fileSystem;
private final ResourcePerspectives resourcePerspectives;
private final RulesProfile rulesProfile;
private final SensorContext sensorContext;

private LizardReportParser(final FileSystem fileSystem, final ResourcePerspectives resourcePerspectives,
final RulesProfile rulesProfile, final SensorContext sensorContext) {
this.fileSystem = fileSystem;
this.resourcePerspectives = resourcePerspectives;
this.rulesProfile = rulesProfile;
this.sensorContext = sensorContext;
}

/**
* @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
*/
@CheckForNull
public static Map<String, List<Measure>> parseReport(final File xmlFile) {
public static Map<String, List<Measure>> parseReport(final FileSystem fileSystem,
final ResourcePerspectives resourcePerspectives, final RulesProfile rulesProfile,
final SensorContext sensorContext, final File xmlFile) {
Map<String, List<Measure>> result = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(xmlFile);
result = new LizardReportParser().parseFile(document);
result = new LizardReportParser(fileSystem, resourcePerspectives, rulesProfile, sensorContext).parseFile(document);
} catch (final FileNotFoundException e) {
LOGGER.error("Lizard Report not found {}", xmlFile, e);
} catch (final IOException | ParserConfigurationException | SAXException e) {
Expand Down Expand Up @@ -197,6 +218,7 @@ private void addComplexityFunctionMeasures(Map<String, List<Measure>> reportMeas
complexityDistribution.add(func.getCyclomaticComplexity());
count++;
complexityInFunctions += func.getCyclomaticComplexity();
createFunctionComplexityIssue(entry.getKey(), func);
}
}

Expand All @@ -205,23 +227,110 @@ private void addComplexityFunctionMeasures(Map<String, List<Measure>> reportMeas
for (Measure m : entry.getValue()) {
if (m.getMetric().getKey().equalsIgnoreCase(CoreMetrics.FILE_COMPLEXITY.getKey())) {
complex = m.getValue();
createFileComplexityIssue(entry.getKey(), (int) complex);
break;
}
}

double complexMean = complex / (double) count;
entry.getValue().addAll(buildFuncionMeasuresList(complexMean, complexityInFunctions, complexityDistribution));
entry.getValue().addAll(buildFunctionMeasuresList(complexMean, complexityInFunctions, complexityDistribution));
}
}
}

private void createFileComplexityIssue(String fileName, int complexity) {
ActiveRule activeRule = rulesProfile.getActiveRule(
LizardRulesDefinition.REPOSITORY_KEY,
LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY);

if (activeRule == null) {
// Rule is not active
return;
}

int threshold = Integer.parseInt(activeRule.getParameter(LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY));

if (complexity <= threshold) {
// Complexity is lower or equal to the defined threshold
return;
}

final InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(fileName));
final Resource resource = inputFile == null ? null : sensorContext.getResource(inputFile);

if (resource == null) {
LOGGER.debug("Skipping file (not found in index): {}", fileName);
return;
}

Issuable issuable = resourcePerspectives.as(Issuable.class, resource);

if (issuable != null) {
Issue issue = issuable.newIssueBuilder()
.ruleKey(RuleKey.of(LizardRulesDefinition.REPOSITORY_KEY, LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY))
.message(String.format("The Cyclomatic Complexity of this file \"%s\" is %d which is greater than %d authorized.", fileName, complexity, threshold))
.effortToFix((double) (complexity - threshold))
.build();

issuable.addIssue(issue);
}
}

private void createFunctionComplexityIssue(String fileName, ObjCFunction func) {
ActiveRule activeRule = rulesProfile.getActiveRule(
LizardRulesDefinition.REPOSITORY_KEY,
LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY);

if (activeRule == null) {
// Rule is not active
return;
}

int complexity = func.getCyclomaticComplexity();
int threshold = Integer.parseInt(activeRule.getParameter(LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY));

if (complexity <= threshold) {
// Complexity is lower or equal to the defined threshold
return;
}

final InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(fileName));
final Resource resource = inputFile == null ? null : sensorContext.getResource(inputFile);

if (resource == null) {
LOGGER.debug("Skipping file (not found in index): {}", fileName);
return;
}

Issuable issuable = resourcePerspectives.as(Issuable.class, resource);

if (issuable != null) {
String name = func.getName();

int lastColonIndex = name.lastIndexOf(':');
Integer lineNumber = lastColonIndex == -1 ? null : Integer.valueOf(name.substring(lastColonIndex + 1));

int atIndex = name.indexOf(" at ");
String functionName = atIndex == -1 ? name : name.substring(0, atIndex);

Issue issue = issuable.newIssueBuilder()
.ruleKey(RuleKey.of(LizardRulesDefinition.REPOSITORY_KEY, LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY))
.message(String.format("The Cyclomatic Complexity of this function \"%s\" is %d which is greater than %d authorized.", functionName, complexity, threshold))
.line(lineNumber)
.effortToFix((double) (complexity - threshold))
.build();

issuable.addIssue(issue);
}
}

/**
* @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<Measure> buildFuncionMeasuresList(double complexMean, int complexityInFunctions,
public List<Measure> buildFunctionMeasuresList(double complexMean, int complexityInFunctions,
RangeDistributionBuilder builder) {
List<Measure> list = new ArrayList<>();
list.add(new Measure(CoreMetrics.FUNCTION_COMPLEXITY, complexMean));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* SonarQube Objective-C (Community) Plugin
* Copyright (C) 2012-2016 OCTO Technology, Backelite, and contributors
* mailto:[email protected]
*
* 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 02110-1301, USA.
*/
package org.sonar.plugins.objectivec.lizard;

import org.sonar.api.rule.Severity;
import org.sonar.api.server.rule.RuleParamType;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.plugins.objectivec.api.ObjectiveC;

public final class LizardRulesDefinition implements RulesDefinition {
public static final String REPOSITORY_KEY = "lizard";
public static final String REPOSITORY_NAME = "Lizard";

private static final String THRESHOLD = "Threshold";

public static final String FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY = "FileCyclomaticComplexity";
public static final String FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY = THRESHOLD;

public static final String FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY = "FunctionCyclomaticComplexity";
public static final String FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY = THRESHOLD;

@Override
public void define(Context context) {
NewRepository repository = context
.createRepository(REPOSITORY_KEY, ObjectiveC.KEY)
.setName(REPOSITORY_NAME);

NewRule fileCyclomaticComplexityRule = repository
.createRule(FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY)
.setName("Files should not be too complex")
.setHtmlDescription("Most of the time, a very complex file breaks the Single Responsibility " +
"Principle and should be re-factored into several different files.")
.setTags("brain-overload")
.setSeverity(Severity.MAJOR);
fileCyclomaticComplexityRule
.setDebtSubCharacteristic(SubCharacteristics.UNIT_TESTABILITY)
.setDebtRemediationFunction(
fileCyclomaticComplexityRule.debtRemediationFunctions().linearWithOffset("1min", "30min"))
.setEffortToFixDescription("per complexity point above the threshold");
fileCyclomaticComplexityRule
.createParam(FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY)
.setDefaultValue("80")
.setType(RuleParamType.INTEGER)
.setDescription("Maximum complexity allowed.");

NewRule functionCyclomaticComplexityRule = repository
.createRule(FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY)
.setName("Functions should not be too complex")
.setHtmlDescription("The cyclomatic complexity of functions should not exceed a defined threshold. " +
"Complex code can perform poorly and will in any case be difficult to understand and " +
"therefore to maintain.")
.setTags("brain-overload")
.setSeverity(Severity.MAJOR);
functionCyclomaticComplexityRule
.setDebtSubCharacteristic(SubCharacteristics.UNIT_TESTABILITY)
.setDebtRemediationFunction(
functionCyclomaticComplexityRule.debtRemediationFunctions().linearWithOffset("1min", "10min"))
.setEffortToFixDescription("per complexity point above the threshold");
functionCyclomaticComplexityRule
.createParam(FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY)
.setDefaultValue("10")
.setType(RuleParamType.INTEGER)
.setDescription("Maximum complexity allowed.");

repository.done();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import org.sonar.api.batch.SensorContext;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.component.ResourcePerspectives;
import org.sonar.api.config.Settings;
import org.sonar.api.measures.Measure;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
import org.sonar.api.scan.filesystem.PathResolver;
Expand All @@ -51,11 +53,16 @@ public final class LizardSensor implements Sensor {

private final FileSystem fileSystem;
private final PathResolver pathResolver;
private final ResourcePerspectives resourcePerspectives;
private final RulesProfile rulesProfile;
private final Settings settings;

public LizardSensor(final FileSystem fileSystem, final PathResolver pathResolver, final Settings settings) {
public LizardSensor(final FileSystem fileSystem, final PathResolver pathResolver,
final ResourcePerspectives resourcePerspectives, final RulesProfile rulesProfile, final Settings settings) {
this.fileSystem = fileSystem;
this.pathResolver = pathResolver;
this.resourcePerspectives = resourcePerspectives;
this.rulesProfile = rulesProfile;
this.settings = settings;
}

Expand All @@ -76,7 +83,8 @@ public void analyse(Project project, SensorContext context) {
}

LOGGER.info("parsing {}", report);
Map<String, List<Measure>> measures = LizardReportParser.parseReport(report);
Map<String, List<Measure>> measures = LizardReportParser.parseReport(fileSystem, resourcePerspectives,
rulesProfile, context, report);

if (measures == null) {
return;
Expand Down
Loading

0 comments on commit 23b5ec0

Please sign in to comment.