Skip to content

Commit 1f96976

Browse files
committed
Implement file and function complexity issues based on Lizard report.
1 parent 83b448a commit 1f96976

File tree

5 files changed

+241
-20
lines changed

5 files changed

+241
-20
lines changed

sonar-objective-c-plugin/src/main/java/org/sonar/plugins/objectivec/ObjectiveCPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.sonar.plugins.objectivec.clang.ClangSensor;
3030
import org.sonar.plugins.objectivec.cobertura.CoberturaSensor;
3131
import org.sonar.plugins.objectivec.cpd.ObjectiveCCpdMapping;
32+
import org.sonar.plugins.objectivec.lizard.LizardRulesDefinition;
3233
import org.sonar.plugins.objectivec.lizard.LizardSensor;
3334
import org.sonar.plugins.objectivec.oclint.OCLintProfile;
3435
import org.sonar.plugins.objectivec.oclint.OCLintProfileImporter;
@@ -77,6 +78,7 @@ public List getExtensions() {
7778
.build());
7879

7980
extensions.add(LizardSensor.class);
81+
extensions.add(LizardRulesDefinition.class);
8082
extensions.add(PropertyDefinition.builder(LizardSensor.REPORT_PATH_KEY)
8183
.name("Report path")
8284
.description("Path (absolute or relative) to Lizard XML report file.")

sonar-objective-c-plugin/src/main/java/org/sonar/plugins/objectivec/lizard/LizardReportParser.java

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,20 @@
2121

2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
24+
import org.sonar.api.batch.SensorContext;
25+
import org.sonar.api.batch.fs.FileSystem;
26+
import org.sonar.api.batch.fs.InputFile;
27+
import org.sonar.api.component.ResourcePerspectives;
28+
import org.sonar.api.issue.Issuable;
29+
import org.sonar.api.issue.Issue;
2430
import org.sonar.api.measures.CoreMetrics;
2531
import org.sonar.api.measures.Measure;
2632
import org.sonar.api.measures.PersistenceMode;
2733
import org.sonar.api.measures.RangeDistributionBuilder;
34+
import org.sonar.api.profiles.RulesProfile;
35+
import org.sonar.api.resources.Resource;
36+
import org.sonar.api.rule.RuleKey;
37+
import org.sonar.api.rules.ActiveRule;
2838
import org.w3c.dom.Document;
2939
import org.w3c.dom.Element;
3040
import org.w3c.dom.Node;
@@ -67,23 +77,34 @@ public final class LizardReportParser {
6777
private static final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = {1, 2, 4, 6, 8, 10, 12, 20, 30};
6878
private static final Number[] FILES_DISTRIB_BOTTOM_LIMITS = {0, 5, 10, 20, 30, 60, 90};
6979

70-
private LizardReportParser() {
71-
// Prevent outside instantiation
80+
private final FileSystem fileSystem;
81+
private final ResourcePerspectives resourcePerspectives;
82+
private final RulesProfile rulesProfile;
83+
private final SensorContext sensorContext;
84+
85+
private LizardReportParser(final FileSystem fileSystem, final ResourcePerspectives resourcePerspectives,
86+
final RulesProfile rulesProfile, final SensorContext sensorContext) {
87+
this.fileSystem = fileSystem;
88+
this.resourcePerspectives = resourcePerspectives;
89+
this.rulesProfile = rulesProfile;
90+
this.sensorContext = sensorContext;
7291
}
7392

7493
/**
7594
* @param xmlFile lizard xml report
7695
* @return Map containing as key the name of the file and as value a list containing the measures for that file
7796
*/
7897
@CheckForNull
79-
public static Map<String, List<Measure>> parseReport(final File xmlFile) {
98+
public static Map<String, List<Measure>> parseReport(final FileSystem fileSystem,
99+
final ResourcePerspectives resourcePerspectives, final RulesProfile rulesProfile,
100+
final SensorContext sensorContext, final File xmlFile) {
80101
Map<String, List<Measure>> result = null;
81102
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
82103

83104
try {
84105
DocumentBuilder builder = factory.newDocumentBuilder();
85106
Document document = builder.parse(xmlFile);
86-
result = new LizardReportParser().parseFile(document);
107+
result = new LizardReportParser(fileSystem, resourcePerspectives, rulesProfile, sensorContext).parseFile(document);
87108
} catch (final FileNotFoundException e) {
88109
LOGGER.error("Lizard Report not found {}", xmlFile, e);
89110
} catch (final IOException | ParserConfigurationException | SAXException e) {
@@ -197,6 +218,7 @@ private void addComplexityFunctionMeasures(Map<String, List<Measure>> reportMeas
197218
complexityDistribution.add(func.getCyclomaticComplexity());
198219
count++;
199220
complexityInFunctions += func.getCyclomaticComplexity();
221+
createFunctionComplexityIssue(entry.getKey(), func);
200222
}
201223
}
202224

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

212235
double complexMean = complex / (double) count;
213-
entry.getValue().addAll(buildFuncionMeasuresList(complexMean, complexityInFunctions, complexityDistribution));
236+
entry.getValue().addAll(buildFunctionMeasuresList(complexMean, complexityInFunctions, complexityDistribution));
214237
}
215238
}
216239
}
217240

241+
private void createFileComplexityIssue(String fileName, int complexity) {
242+
ActiveRule activeRule = rulesProfile.getActiveRule(
243+
LizardRulesDefinition.REPOSITORY_KEY,
244+
LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY);
245+
246+
if (activeRule == null) {
247+
// Rule is not active
248+
return;
249+
}
250+
251+
int threshold = Integer.parseInt(activeRule.getParameter(LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY));
252+
253+
if (complexity <= threshold) {
254+
// Complexity is lower or equal to the defined threshold
255+
return;
256+
}
257+
258+
final InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(fileName));
259+
final Resource resource = inputFile == null ? null : sensorContext.getResource(inputFile);
260+
261+
if (resource == null) {
262+
LOGGER.debug("Skipping file (not found in index): {}", fileName);
263+
return;
264+
}
265+
266+
Issuable issuable = resourcePerspectives.as(Issuable.class, resource);
267+
268+
if (issuable != null) {
269+
Issue issue = issuable.newIssueBuilder()
270+
.ruleKey(RuleKey.of(LizardRulesDefinition.REPOSITORY_KEY, LizardRulesDefinition.FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY))
271+
.message(String.format("The Cyclomatic Complexity of this file \"%s\" is %d which is greater than %d authorized.", fileName, complexity, threshold))
272+
.effortToFix((double) (complexity - threshold))
273+
.build();
274+
275+
issuable.addIssue(issue);
276+
}
277+
}
278+
279+
private void createFunctionComplexityIssue(String fileName, ObjCFunction func) {
280+
ActiveRule activeRule = rulesProfile.getActiveRule(
281+
LizardRulesDefinition.REPOSITORY_KEY,
282+
LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY);
283+
284+
if (activeRule == null) {
285+
// Rule is not active
286+
return;
287+
}
288+
289+
int complexity = func.getCyclomaticComplexity();
290+
int threshold = Integer.parseInt(activeRule.getParameter(LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY));
291+
292+
if (complexity <= threshold) {
293+
// Complexity is lower or equal to the defined threshold
294+
return;
295+
}
296+
297+
final InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasPath(fileName));
298+
final Resource resource = inputFile == null ? null : sensorContext.getResource(inputFile);
299+
300+
if (resource == null) {
301+
LOGGER.debug("Skipping file (not found in index): {}", fileName);
302+
return;
303+
}
304+
305+
Issuable issuable = resourcePerspectives.as(Issuable.class, resource);
306+
307+
if (issuable != null) {
308+
String name = func.getName();
309+
310+
int lastColonIndex = name.lastIndexOf(':');
311+
Integer lineNumber = lastColonIndex == -1 ? null : Integer.valueOf(name.substring(lastColonIndex + 1));
312+
313+
int atIndex = name.indexOf(" at ");
314+
String functionName = atIndex == -1 ? name : name.substring(0, atIndex);
315+
316+
Issue issue = issuable.newIssueBuilder()
317+
.ruleKey(RuleKey.of(LizardRulesDefinition.REPOSITORY_KEY, LizardRulesDefinition.FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY))
318+
.message(String.format("The Cyclomatic Complexity of this function \"%s\" is %d which is greater than %d authorized.", functionName, complexity, threshold))
319+
.line(lineNumber)
320+
.effortToFix((double) (complexity - threshold))
321+
.build();
322+
323+
issuable.addIssue(issue);
324+
}
325+
}
326+
218327
/**
219328
* @param complexMean average complexity per function in a file
220329
* @param complexityInFunctions Entire complexity in functions
221330
* @param builder Builder ready to build FUNCTION_COMPLEXITY_DISTRIBUTION
222331
* @return list of Measures containing FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTION_COMPLEXITY and COMPLEXITY_IN_FUNCTIONS
223332
*/
224-
public List<Measure> buildFuncionMeasuresList(double complexMean, int complexityInFunctions,
333+
public List<Measure> buildFunctionMeasuresList(double complexMean, int complexityInFunctions,
225334
RangeDistributionBuilder builder) {
226335
List<Measure> list = new ArrayList<>();
227336
list.add(new Measure(CoreMetrics.FUNCTION_COMPLEXITY, complexMean));
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* SonarQube Objective-C (Community) Plugin
3+
* Copyright (C) 2012-2016 OCTO Technology, Backelite, and contributors
4+
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.plugins.objectivec.lizard;
21+
22+
import org.sonar.api.rule.Severity;
23+
import org.sonar.api.server.rule.RuleParamType;
24+
import org.sonar.api.server.rule.RulesDefinition;
25+
import org.sonar.plugins.objectivec.api.ObjectiveC;
26+
27+
public final class LizardRulesDefinition implements RulesDefinition {
28+
public static final String REPOSITORY_KEY = "lizard";
29+
public static final String REPOSITORY_NAME = "Lizard";
30+
31+
private static final String THRESHOLD = "Threshold";
32+
33+
public static final String FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY = "FileCyclomaticComplexity";
34+
public static final String FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY = THRESHOLD;
35+
36+
public static final String FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY = "FunctionCyclomaticComplexity";
37+
public static final String FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY = THRESHOLD;
38+
39+
@Override
40+
public void define(Context context) {
41+
NewRepository repository = context
42+
.createRepository(REPOSITORY_KEY, ObjectiveC.KEY)
43+
.setName(REPOSITORY_NAME);
44+
45+
NewRule fileCyclomaticComplexityRule = repository
46+
.createRule(FILE_CYCLOMATIC_COMPLEXITY_RULE_KEY)
47+
.setName("Files should not be too complex")
48+
.setHtmlDescription("Most of the time, a very complex file breaks the Single Responsibility " +
49+
"Principle and should be re-factored into several different files.")
50+
.setTags("brain-overload")
51+
.setSeverity(Severity.MAJOR);
52+
fileCyclomaticComplexityRule
53+
.setDebtSubCharacteristic(SubCharacteristics.UNIT_TESTABILITY)
54+
.setDebtRemediationFunction(
55+
fileCyclomaticComplexityRule.debtRemediationFunctions().linearWithOffset("1min", "30min"))
56+
.setEffortToFixDescription("per complexity point above the threshold");
57+
fileCyclomaticComplexityRule
58+
.createParam(FILE_CYCLOMATIC_COMPLEXITY_PARAM_KEY)
59+
.setDefaultValue("80")
60+
.setType(RuleParamType.INTEGER)
61+
.setDescription("Maximum complexity allowed.");
62+
63+
NewRule functionCyclomaticComplexityRule = repository
64+
.createRule(FUNCTION_CYCLOMATIC_COMPLEXITY_RULE_KEY)
65+
.setName("Functions should not be too complex")
66+
.setHtmlDescription("The cyclomatic complexity of functions should not exceed a defined threshold. " +
67+
"Complex code can perform poorly and will in any case be difficult to understand and " +
68+
"therefore to maintain.")
69+
.setTags("brain-overload")
70+
.setSeverity(Severity.MAJOR);
71+
functionCyclomaticComplexityRule
72+
.setDebtSubCharacteristic(SubCharacteristics.UNIT_TESTABILITY)
73+
.setDebtRemediationFunction(
74+
functionCyclomaticComplexityRule.debtRemediationFunctions().linearWithOffset("1min", "10min"))
75+
.setEffortToFixDescription("per complexity point above the threshold");
76+
functionCyclomaticComplexityRule
77+
.createParam(FUNCTION_CYCLOMATIC_COMPLEXITY_PARAM_KEY)
78+
.setDefaultValue("10")
79+
.setType(RuleParamType.INTEGER)
80+
.setDescription("Maximum complexity allowed.");
81+
82+
repository.done();
83+
}
84+
}

sonar-objective-c-plugin/src/main/java/org/sonar/plugins/objectivec/lizard/LizardSensor.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import org.sonar.api.batch.SensorContext;
2727
import org.sonar.api.batch.fs.FileSystem;
2828
import org.sonar.api.batch.fs.InputFile;
29+
import org.sonar.api.component.ResourcePerspectives;
2930
import org.sonar.api.config.Settings;
3031
import org.sonar.api.measures.Measure;
32+
import org.sonar.api.profiles.RulesProfile;
3133
import org.sonar.api.resources.Project;
3234
import org.sonar.api.resources.Resource;
3335
import org.sonar.api.scan.filesystem.PathResolver;
@@ -51,11 +53,16 @@ public final class LizardSensor implements Sensor {
5153

5254
private final FileSystem fileSystem;
5355
private final PathResolver pathResolver;
56+
private final ResourcePerspectives resourcePerspectives;
57+
private final RulesProfile rulesProfile;
5458
private final Settings settings;
5559

56-
public LizardSensor(final FileSystem fileSystem, final PathResolver pathResolver, final Settings settings) {
60+
public LizardSensor(final FileSystem fileSystem, final PathResolver pathResolver,
61+
final ResourcePerspectives resourcePerspectives, final RulesProfile rulesProfile, final Settings settings) {
5762
this.fileSystem = fileSystem;
5863
this.pathResolver = pathResolver;
64+
this.resourcePerspectives = resourcePerspectives;
65+
this.rulesProfile = rulesProfile;
5966
this.settings = settings;
6067
}
6168

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

7885
LOGGER.info("parsing {}", report);
79-
Map<String, List<Measure>> measures = LizardReportParser.parseReport(report);
86+
Map<String, List<Measure>> measures = LizardReportParser.parseReport(fileSystem, resourcePerspectives,
87+
rulesProfile, context, report);
8088

8189
if (measures == null) {
8290
return;

0 commit comments

Comments
 (0)