Browse Source

SONAR-10543 Sensor Java API should allow to add external rule engine issues

tags/7.5
Duarte Meneses 6 years ago
parent
commit
7bd31bc52d
36 changed files with 966 additions and 80 deletions
  1. 6
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java
  2. 82
    0
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java
  3. 8
    0
      plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java
  4. 2
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
  5. 6
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
  6. 67
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
  7. 6
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
  8. 9
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
  9. 7
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
  10. 12
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
  11. 3
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
  12. 51
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java
  13. 44
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java
  14. 6
    12
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java
  15. 88
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java
  16. 95
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
  17. 103
    0
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java
  18. 1
    61
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
  19. 1
    0
      sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java
  20. 23
    0
      sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
  21. 126
    0
      sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java
  22. 44
    5
      sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
  23. 15
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java
  24. 8
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
  25. 14
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java
  26. 18
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java
  27. 23
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
  28. 1
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
  29. 14
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
  30. 1
    0
      sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
  31. 8
    0
      sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
  32. 15
    0
      sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java
  33. 24
    0
      sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
  34. 4
    1
      sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java
  35. 11
    0
      sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java
  36. 20
    0
      sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java

+ 6
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java View File

@@ -48,6 +48,7 @@ import org.sonar.xoo.rule.NoSonarSensor;
import org.sonar.xoo.rule.OneBlockerIssuePerFileSensor;
import org.sonar.xoo.rule.OneBugIssuePerLineSensor;
import org.sonar.xoo.rule.OneDayDebtPerFileSensor;
import org.sonar.xoo.rule.OneExternalIssuePerLineSensor;
import org.sonar.xoo.rule.OneIssueOnDirPerFileSensor;
import org.sonar.xoo.rule.OneIssuePerDirectorySensor;
import org.sonar.xoo.rule.OneIssuePerFileSensor;
@@ -164,9 +165,13 @@ public class XooPlugin implements Plugin {
if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(5, 5))) {
context.addExtension(CpdTokenizerSensor.class);
}
if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(6,6))) {
if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(6, 6))) {
context.addExtension(XooBuiltInQualityProfilesDefinition.class);
}
// TODO change to v7.2 once version of SQ was updated
if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 1))) {
context.addExtension(OneExternalIssuePerLineSensor.class);
}
}

}

+ 82
- 0
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java View File

@@ -0,0 +1,82 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.xoo.rule;

import org.sonar.api.batch.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.InputFile.Type;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.xoo.Xoo;
import org.sonar.xoo.Xoo2;

public class OneExternalIssuePerLineSensor implements Sensor {
public static final String RULE_KEY = "OneExternalIssuePerLine";
public static final String ENGINE_KEY = "XooEngine";
public static final String SEVERITY = "MAJOR";
public static final Long EFFORT = 10l;
public static final RuleType type = RuleType.BUG;
public static final String ACTIVATE_EXTERNAL_ISSUES = "sonar.oneExternalIssuePerLine.activate";

@Override
public void describe(SensorDescriptor descriptor) {
descriptor
.name("One External Issue Per Line")
.onlyOnLanguages(Xoo.KEY, Xoo2.KEY)
.onlyWhenConfiguration(c -> c.getBoolean(ACTIVATE_EXTERNAL_ISSUES).orElse(false));
}

@Override
public void execute(SensorContext context) {
analyse(context, Xoo.KEY, XooRulesDefinition.XOO_REPOSITORY);
analyse(context, Xoo2.KEY, XooRulesDefinition.XOO2_REPOSITORY);
}

private void analyse(SensorContext context, String language, String repo) {
FileSystem fs = context.fileSystem();
FilePredicates p = fs.predicates();
for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(language), p.hasType(Type.MAIN)))) {
createIssues(file, context, repo);
}
}

private void createIssues(InputFile file, SensorContext context, String repo) {
RuleKey ruleKey = RuleKey.of(repo, RULE_KEY);
for (int line = 1; line <= file.lines(); line++) {
NewExternalIssue newIssue = context.newExternalIssue();
newIssue
.forRule(ruleKey)
.at(newIssue.newLocation()
.on(file)
.at(file.selectLine(line))
.message("This issue is generated on each line"))
.severity(Severity.valueOf(SEVERITY))
.remediationEffort(EFFORT)
.type(type)
.save();
}
}
}

+ 8
- 0
plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java View File

@@ -55,4 +55,12 @@ public class XooPluginTest {
new XooPlugin().define(context);
assertThat(context.getExtensions()).hasSize(51).contains(CpdTokenizerSensor.class);
}
@Test
public void provide_extensions_for_7_2() {
SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("7.2"), SonarQubeSide.SCANNER);
Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
new XooPlugin().define(context);
assertThat(context.getExtensions()).hasSize(52).contains(CpdTokenizerSensor.class);
}
}

+ 2
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java View File

@@ -39,6 +39,8 @@ public interface BatchReportReader {
ScannerReport.Component readComponent(int componentRef);

CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef);
CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef);

CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef);


+ 6
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java View File

@@ -108,6 +108,12 @@ public class BatchReportReaderImpl implements BatchReportReader {
ensureInitialized();
return delegate.readComponentIssues(componentRef);
}
@Override
public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
ensureInitialized();
return delegate.readComponentExternalIssues(componentRef);
}

@Override
public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) {

+ 67
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java View File

@@ -22,8 +22,11 @@ package org.sonar.server.computation.task.projectanalysis.issue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.issue.Issue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.tracking.Input;
@@ -34,6 +37,7 @@ import org.sonar.db.protobuf.DbCommons;
import org.sonar.db.protobuf.DbIssues;
import org.sonar.scanner.protocol.Constants.Severity;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReader;
import org.sonar.server.computation.task.projectanalysis.component.Component;
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
@@ -111,6 +115,16 @@ public class TrackerRawInputFactory {
}
}

try (CloseableIterator<ScannerReport.ExternalIssue> reportIssues = reportReader.readComponentExternalIssues(component.getReportAttributes().getRef())) {
// optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed
// as late as possible
while (reportIssues.hasNext()) {
ScannerReport.ExternalIssue reportExternalIssue = reportIssues.next();
DefaultIssue issue = toIssue(getLineHashSequence(), reportExternalIssue);
result.add(issue);
}
}

return result;
}

@@ -157,6 +171,59 @@ public class TrackerRawInputFactory {
return issue;
}

private DefaultIssue toIssue(LineHashSequence lineHashSeq, ScannerReport.ExternalIssue reportIssue) {
DefaultIssue issue = new DefaultIssue();
init(issue);
issue.setRuleKey(RuleKey.of(reportIssue.getRuleRepository(), reportIssue.getRuleKey()));
if (reportIssue.hasTextRange()) {
int startLine = reportIssue.getTextRange().getStartLine();
issue.setLine(startLine);
issue.setChecksum(lineHashSeq.getHashForLine(startLine));
} else {
issue.setChecksum("");
}
if (isNotEmpty(reportIssue.getMsg())) {
issue.setMessage(reportIssue.getMsg());
}
if (reportIssue.getSeverity() != Severity.UNSET_SEVERITY) {
issue.setSeverity(reportIssue.getSeverity().name());
}
if (reportIssue.getEffort() != 0) {
issue.setEffort(Duration.create(reportIssue.getEffort()));
}
DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder();
if (reportIssue.hasTextRange()) {
dbLocationsBuilder.setTextRange(convertTextRange(reportIssue.getTextRange()));
}
for (ScannerReport.Flow flow : reportIssue.getFlowList()) {
if (flow.getLocationCount() > 0) {
DbIssues.Flow.Builder dbFlowBuilder = DbIssues.Flow.newBuilder();
for (ScannerReport.IssueLocation location : flow.getLocationList()) {
dbFlowBuilder.addLocation(convertLocation(location));
}
dbLocationsBuilder.addFlow(dbFlowBuilder);
}
}
issue.setLocations(dbLocationsBuilder.build());
issue.setType(toRuleType(reportIssue.getType()));
issue.setDescriptionUrl(StringUtils.stripToNull(reportIssue.getDescriptionUrl()));
return issue;
}

private RuleType toRuleType(IssueType type) {
switch (type) {
case BUG:
return RuleType.BUG;
case CODE_SMELL:
return RuleType.CODE_SMELL;
case VULNERABILITY:
return RuleType.VULNERABILITY;
case UNRECOGNIZED:
default:
throw new IllegalStateException("Invalid issue type: " + type);
}
}

private DefaultIssue init(DefaultIssue issue) {
issue.setResolution(null);
issue.setStatus(Issue.STATUS_OPEN);

+ 6
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java View File

@@ -44,6 +44,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader {
private Map<Integer, ScannerReport.Changesets> changesets = new HashMap<>();
private Map<Integer, ScannerReport.Component> components = new HashMap<>();
private Map<Integer, List<ScannerReport.Issue>> issues = new HashMap<>();
private Map<Integer, List<ScannerReport.ExternalIssue>> externalIssues = new HashMap<>();
private Map<Integer, List<ScannerReport.Duplication>> duplications = new HashMap<>();
private Map<Integer, List<ScannerReport.CpdTextBlock>> duplicationBlocks = new HashMap<>();
private Map<Integer, List<ScannerReport.Symbol>> symbols = new HashMap<>();
@@ -172,6 +173,11 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader {
public CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef) {
return closeableIterator(issues.get(componentRef));
}
@Override
public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
return closeableIterator(externalIssues.get(componentRef));
}

public BatchReportReaderRule putIssues(int componentRef, List<ScannerReport.Issue> issue) {
this.issues.put(componentRef, issue);

+ 9
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java View File

@@ -20,6 +20,7 @@
package org.sonar.api.batch.sensor;

import java.io.Serializable;

import org.sonar.api.SonarRuntime;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
@@ -30,7 +31,9 @@ import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
import org.sonar.api.batch.sensor.error.NewAnalysisError;
import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.measure.Measure;
import org.sonar.api.batch.sensor.measure.NewMeasure;
@@ -112,6 +115,12 @@ public interface SensorContext {
*/
NewIssue newIssue();

/**
* Fluent builder to create a new {@link ExternalIssue}. Don't forget to call {@link NewExternalIssue#save()} once all parameters are provided.
* @since 7.2
*/
NewExternalIssue newExternalIssue();

// ------------ HIGHLIGHTING ------------

/**

+ 7
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java View File

@@ -31,6 +31,7 @@ import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
import org.sonar.api.batch.sensor.error.AnalysisError;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.measure.Measure;
import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable;
@@ -43,6 +44,7 @@ class InMemorySensorStorage implements SensorStorage {
Table<String, String, Measure> measuresByComponentAndMetric = HashBasedTable.create();

Collection<Issue> allIssues = new ArrayList<>();
Collection<ExternalIssue> allExternalIssues = new ArrayList<>();
Collection<AnalysisError> allAnalysisErrors = new ArrayList<>();

Map<String, DefaultHighlighting> highlightingByComponent = new HashMap<>();
@@ -114,4 +116,9 @@ class InMemorySensorStorage implements SensorStorage {
checkArgument(value != null, "Value of context property must not be null");
contextProperties.put(key, value);
}

@Override
public void store(ExternalIssue issue) {
allExternalIssues.add(issue);
}
}

+ 12
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java View File

@@ -58,8 +58,11 @@ import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
import org.sonar.api.batch.sensor.highlighting.TypeOfText;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
import org.sonar.api.batch.sensor.measure.Measure;
import org.sonar.api.batch.sensor.measure.NewMeasure;
@@ -219,6 +222,15 @@ public class SensorContextTester implements SensorContext {
return sensorStorage.allIssues;
}

@Override
public NewExternalIssue newExternalIssue() {
return new DefaultExternalIssue(sensorStorage);
}

public Collection<ExternalIssue> allExternalIssues() {
return sensorStorage.allExternalIssues;
}

public Collection<AnalysisError> allAnalysisErrors() {
return sensorStorage.allAnalysisErrors;
}

+ 3
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java View File

@@ -24,6 +24,7 @@ import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
import org.sonar.api.batch.sensor.error.AnalysisError;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.measure.Measure;
import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable;
@@ -39,6 +40,8 @@ public interface SensorStorage {

void store(Issue issue);

void store(ExternalIssue issue);

void store(DefaultHighlighting highlighting);

/**

+ 51
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue;

import javax.annotation.CheckForNull;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.rules.RuleType;

/**
* Represents an issue imported from an external rule engine by a {@link Sensor}.
* @since 7.2
*/
public interface ExternalIssue extends IIssue {

Severity severity();

/**
* Link to a page describing more details about the rule that triggered this issue.
*/
@CheckForNull
String descriptionUrl();

/**
* Effort to fix the issue, in minutes.
*/
@CheckForNull
Long remediationEffort();

/**
* Type of the issue.
*/
RuleType type();
}

+ 44
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue;

import java.util.List;
import org.sonar.api.batch.sensor.issue.Issue.Flow;
import org.sonar.api.rule.RuleKey;

/**
* @since 7.2
*/
public interface IIssue {
/**
* The {@link RuleKey} of this issue.
*/
RuleKey ruleKey();
/**
* Primary locations for this issue.
*/
IssueLocation primaryLocation();

/**
* List of flows for this issue. Can be empty.
*/
List<Flow> flows();
}

+ 6
- 12
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java View File

@@ -23,27 +23,20 @@ import java.util.List;
import javax.annotation.CheckForNull;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.rule.RuleKey;

/**
* Represents an issue detected by a {@link Sensor}.
*
* @since 5.1
*/
public interface Issue {

public interface Issue extends IIssue {
interface Flow {
/**
* @return Ordered list of locations for the execution flow
*/
List<IssueLocation> locations();
}

/**
* The {@link RuleKey} of this issue.
*/
RuleKey ruleKey();

/**
* Effort to fix the issue. Used by technical debt model.
* @deprecated since 5.5 use {@link #gap()}
@@ -58,23 +51,24 @@ public interface Issue {
*/
@CheckForNull
Double gap();
/**
* Overridden severity.
*/
@CheckForNull
Severity overriddenSeverity();
/**
* Primary locations for this issue.
* @since 5.2
*/
@Override
IssueLocation primaryLocation();

/**
* List of flows for this issue. Can be empty.
* @since 5.2
*/
@Override
List<Flow> flows();

}

+ 88
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java View File

@@ -0,0 +1,88 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue;

import javax.annotation.Nullable;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;

/**
* Builder for an issue imported from an external rule engine by a {@link Sensor}.
* Don't forget to {@link #save()} after setting the fields.
*
* @since 7.2
*/
public interface NewExternalIssue {
/**
* The {@link RuleKey} of the issue.
*/
NewExternalIssue forRule(RuleKey ruleKey);

/**
* Type of issue.
*/
NewExternalIssue type(RuleType type);

/**
* Effort to fix the issue, in minutes.
*/
NewExternalIssue remediationEffort(@Nullable Long effort);

/**
* Set the severity of the issue.
*/
NewExternalIssue severity(Severity severity);

/**
* Primary location for this issue.
*/
NewExternalIssue at(NewIssueLocation primaryLocation);

/**
* Add a secondary location for this issue. Several secondary locations can be registered.
*/
NewExternalIssue addLocation(NewIssueLocation secondaryLocation);

/**
* Add a URL pointing to more information about the rule triggering this issue.
*/
NewExternalIssue descriptionUrl(String url);

/**
* Register a flow for this issue. A flow is an ordered list of issue locations that help to understand the issue.
* It should be a <b>path that backtracks the issue from its primary location to the start of the flow</b>.
* Several flows can be registered.
*/
NewExternalIssue addFlow(Iterable<NewIssueLocation> flowLocations);

/**
* Create a new location for this issue. First registered location is considered as primary location.
*/
NewIssueLocation newLocation();

/**
* Save the issue. If rule key is unknown or rule not enabled in the current quality profile then a warning is logged but no exception
* is thrown.
*/
void save();

}

+ 95
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java View File

@@ -0,0 +1,95 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue.internal;

import com.google.common.base.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.sonar.api.batch.sensor.internal.DefaultStorable;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.IssueLocation;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.batch.sensor.issue.Issue.Flow;
import org.sonar.api.rule.RuleKey;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;

public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue> extends DefaultStorable {
protected RuleKey ruleKey;
protected IssueLocation primaryLocation;
protected List<List<IssueLocation>> flows = new ArrayList<>();
protected AbstractDefaultIssue() {
super(null);
}
public AbstractDefaultIssue(SensorStorage storage) {
super(storage);
}
public T forRule(RuleKey ruleKey) {
this.ruleKey = ruleKey;
return (T) this;
}
public RuleKey ruleKey() {
return this.ruleKey;
}
public IssueLocation primaryLocation() {
return primaryLocation;
}

public List<Flow> flows() {
return this.flows.stream()
.<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l)))
.collect(toList());
}
public NewIssueLocation newLocation() {
return new DefaultIssueLocation();
}

public T at(NewIssueLocation primaryLocation) {
Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null");
checkState(this.primaryLocation == null, "at() already called");
this.primaryLocation = (DefaultIssueLocation) primaryLocation;
Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component");
return (T) this;
}

public T addLocation(NewIssueLocation secondaryLocation) {
flows.add(Arrays.asList((IssueLocation) secondaryLocation));
return (T) this;
}

public T addFlow(Iterable<NewIssueLocation> locations) {
List<IssueLocation> flowAsList = new ArrayList<>();
for (NewIssueLocation issueLocation : locations) {
flowAsList.add((DefaultIssueLocation) issueLocation);
}
flows.add(flowAsList);
return (T) this;
}
}

+ 103
- 0
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java View File

@@ -0,0 +1,103 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue.internal;

import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.rules.RuleType;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIssue> implements ExternalIssue, NewExternalIssue {
private Long effort;
private Severity severity;
private String url;
private RuleType type;

public DefaultExternalIssue() {
super(null);
}

public DefaultExternalIssue(SensorStorage storage) {
super(storage);
}

@Override
public DefaultExternalIssue remediationEffort(@Nullable Long effort) {
Preconditions.checkArgument(effort == null || effort >= 0, format("effort must be greater than or equal 0 (got %s)", effort));
this.effort = effort;
return this;
}

@Override
public DefaultExternalIssue severity(@Nullable Severity severity) {
this.severity = severity;
return this;
}

@Override
public Severity severity() {
return this.severity;
}

@Override
public Long remediationEffort() {
return this.effort;
}

@Override
public void doSave() {
requireNonNull(this.ruleKey, "ruleKey is mandatory on external issue");
checkState(primaryLocation != null, "Primary location is mandatory on every external issue");
checkState(primaryLocation.inputComponent().isFile(), "External issues must be located in files");
checkState(type != null, "Type is mandatory on every external issue");
checkState(severity != null, "Severity is mandatory on every external issue");
checkState(severity != null, "Severity is mandatory on every external issue");
storage.store(this);
}

@Override
public DefaultExternalIssue descriptionUrl(String url) {
this.url = url;
return this;
}

@Override
public String descriptionUrl() {
return url;
}

@Override
public RuleType type() {
return type;
}

@Override
public DefaultExternalIssue type(RuleType type) {
this.type = type;
return this;
}
}

+ 1
- 61
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java View File

@@ -20,32 +20,20 @@
package org.sonar.api.batch.sensor.issue.internal;

import com.google.common.base.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.internal.DefaultStorable;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.IssueLocation;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.rule.RuleKey;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {

private RuleKey ruleKey;
public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements Issue, NewIssue {
private Double gap;
private Severity overriddenSeverity;
private IssueLocation primaryLocation;
private List<List<IssueLocation>> flows = new ArrayList<>();

public DefaultIssue() {
super(null);
@@ -55,12 +43,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
super(storage);
}

@Override
public DefaultIssue forRule(RuleKey ruleKey) {
this.ruleKey = ruleKey;
return this;
}

@Override
public DefaultIssue effortToFix(@Nullable Double effortToFix) {
return gap(effortToFix);
@@ -79,41 +61,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
return this;
}

@Override
public NewIssueLocation newLocation() {
return new DefaultIssueLocation();
}

@Override
public DefaultIssue at(NewIssueLocation primaryLocation) {
Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null");
checkState(this.primaryLocation == null, "at() already called");
this.primaryLocation = (DefaultIssueLocation) primaryLocation;
Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component");
return this;
}

@Override
public NewIssue addLocation(NewIssueLocation secondaryLocation) {
flows.add(Arrays.asList((IssueLocation) secondaryLocation));
return this;
}

@Override
public DefaultIssue addFlow(Iterable<NewIssueLocation> locations) {
List<IssueLocation> flowAsList = new ArrayList<>();
for (NewIssueLocation issueLocation : locations) {
flowAsList.add((DefaultIssueLocation) issueLocation);
}
flows.add(flowAsList);
return this;
}

@Override
public RuleKey ruleKey() {
return this.ruleKey;
}

@Override
public Severity overriddenSeverity() {
return this.overriddenSeverity;
@@ -134,13 +81,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue {
return primaryLocation;
}

@Override
public List<Flow> flows() {
return this.flows.stream()
.<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l)))
.collect(toList());
}

@Override
public void doSave() {
requireNonNull(this.ruleKey, "ruleKey is mandatory on issue");

+ 1
- 0
sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java View File

@@ -39,6 +39,7 @@ public class RuleKey implements Serializable, Comparable<RuleKey> {
*/
@Deprecated
public static final String MANUAL_REPOSITORY_KEY = "manual";
public static final String EXTERNAL_RULE_REPO_PREFIX = "external_";

private final String repository;
private final String rule;

+ 23
- 0
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java View File

@@ -34,16 +34,19 @@ import org.sonar.api.batch.fs.internal.DefaultInputModule;
import org.sonar.api.batch.fs.internal.DefaultTextPointer;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
import org.sonar.api.batch.sensor.error.AnalysisError;
import org.sonar.api.batch.sensor.error.NewAnalysisError;
import org.sonar.api.batch.sensor.highlighting.TypeOfText;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
import org.sonar.api.config.Settings;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.SonarException;

import static org.assertj.core.api.Assertions.assertThat;
@@ -105,6 +108,26 @@ public class SensorContextTesterTest {
assertThat(tester.allIssues()).hasSize(2);
}

@Test
public void testExternalIssues() {
assertThat(tester.allExternalIssues()).isEmpty();
NewExternalIssue newExternalIssue = tester.newExternalIssue();
newExternalIssue
.at(newExternalIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build()))
.forRule(RuleKey.of("repo", "rule"))
.type(RuleType.BUG)
.severity(Severity.BLOCKER)
.save();
newExternalIssue = tester.newExternalIssue();
newExternalIssue
.at(newExternalIssue.newLocation().on(new TestInputFileBuilder("foo", "src/Foo.java").build()))
.type(RuleType.BUG)
.severity(Severity.BLOCKER)
.forRule(RuleKey.of("repo", "rule"))
.save();
assertThat(tester.allExternalIssues()).hasSize(2);
}

@Test
public void testAnalysisErrors() {
assertThat(tester.allAnalysisErrors()).isEmpty();

+ 126
- 0
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java View File

@@ -0,0 +1,126 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* 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.api.batch.sensor.issue.internal;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class DefaultExternalIssueTest {
@Rule
public ExpectedException exception = ExpectedException.none();

private DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.php")
.initMetadata("Foo\nBar\n")
.build();

@Test
public void build_file_issue() {
SensorStorage storage = mock(SensorStorage.class);
DefaultExternalIssue issue = new DefaultExternalIssue(storage)
.at(new DefaultIssueLocation()
.on(inputFile)
.at(inputFile.selectLine(1))
.message("Wrong way!"))
.forRule(RuleKey.of("repo", "rule"))
.remediationEffort(10l)
.descriptionUrl("url")
.type(RuleType.BUG)
.severity(Severity.BLOCKER);

assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputFile);
assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule"));
assertThat(issue.primaryLocation().textRange().start().line()).isEqualTo(1);
assertThat(issue.remediationEffort()).isEqualTo(10l);
assertThat(issue.descriptionUrl()).isEqualTo("url");
assertThat(issue.type()).isEqualTo(RuleType.BUG);
assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!");

issue.save();

verify(storage).store(issue);
}

@Test
public void fail_to_store_if_no_type() {
SensorStorage storage = mock(SensorStorage.class);
DefaultExternalIssue issue = new DefaultExternalIssue(storage)
.at(new DefaultIssueLocation()
.on(inputFile)
.at(inputFile.selectLine(1))
.message("Wrong way!"))
.forRule(RuleKey.of("repo", "rule"))
.remediationEffort(10l)
.descriptionUrl("url")
.severity(Severity.BLOCKER);

exception.expect(IllegalStateException.class);
exception.expectMessage("Type is mandatory");
issue.save();
}

@Test
public void fail_to_store_if_primary_location_is_not_a_file() {
SensorStorage storage = mock(SensorStorage.class);
DefaultExternalIssue issue = new DefaultExternalIssue(storage)
.at(new DefaultIssueLocation()
.on(mock(InputComponent.class))
.message("Wrong way!"))
.forRule(RuleKey.of("repo", "rule"))
.remediationEffort(10l)
.descriptionUrl("url")
.severity(Severity.BLOCKER);

exception.expect(IllegalStateException.class);
exception.expectMessage("External issues must be located in files");
issue.save();
}

@Test
public void fail_to_store_if_no_severity() {
SensorStorage storage = mock(SensorStorage.class);
DefaultExternalIssue issue = new DefaultExternalIssue(storage)
.at(new DefaultIssueLocation()
.on(inputFile)
.at(inputFile.selectLine(1))
.message("Wrong way!"))
.forRule(RuleKey.of("repo", "rule"))
.remediationEffort(10l)
.descriptionUrl("url")
.type(RuleType.BUG);

exception.expect(IllegalStateException.class);
exception.expectMessage("Severity is mandatory");
issue.save();
}

}

+ 44
- 5
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java View File

@@ -20,6 +20,8 @@
package org.sonar.scanner.issue;

import com.google.common.base.Strings;
import java.util.Collection;
import java.util.function.Consumer;
import javax.annotation.concurrent.ThreadSafe;
import org.sonar.api.batch.fs.TextRange;
import org.sonar.api.batch.fs.internal.DefaultInputComponent;
@@ -27,6 +29,7 @@ import org.sonar.api.batch.rule.ActiveRule;
import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.batch.rule.Rule;
import org.sonar.api.batch.rule.Rules;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.Issue.Flow;
import org.sonar.api.rule.RuleKey;
@@ -73,6 +76,12 @@ public class ModuleIssues {
return false;
}

public void initAndAddExternalIssue(ExternalIssue issue) {
DefaultInputComponent inputComponent = (DefaultInputComponent) issue.primaryLocation().inputComponent();
ScannerReport.ExternalIssue rawExternalIssue = createReportExternalIssue(issue, inputComponent.batchId());
write(inputComponent.batchId(), rawExternalIssue);
}

private static ScannerReport.Issue createReportIssue(Issue issue, int componentRef, String ruleName, String activeRuleSeverity) {
String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? ruleName : issue.primaryLocation().message();
org.sonar.api.batch.rule.Severity overriddenSeverity = issue.overriddenSeverity();
@@ -97,14 +106,41 @@ public class ModuleIssues {
if (gap != null) {
builder.setGap(gap);
}
applyFlows(componentRef, builder, locationBuilder, textRangeBuilder, issue);
applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows());
return builder.build();
}

private static void applyFlows(int componentRef, ScannerReport.Issue.Builder builder, ScannerReport.IssueLocation.Builder locationBuilder,
ScannerReport.TextRange.Builder textRangeBuilder, Issue issue) {
private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) {
String primaryMessage = issue.primaryLocation().message();
Severity severity = Severity.valueOf(issue.severity().name());

ScannerReport.ExternalIssue.Builder builder = ScannerReport.ExternalIssue.newBuilder();
ScannerReport.IssueLocation.Builder locationBuilder = IssueLocation.newBuilder();
ScannerReport.TextRange.Builder textRangeBuilder = ScannerReport.TextRange.newBuilder();
// non-null fields
builder.setSeverity(severity);
builder.setRuleRepository(issue.ruleKey().repository());
builder.setRuleKey(issue.ruleKey().rule());
builder.setMsg(primaryMessage);
locationBuilder.setMsg(primaryMessage);

locationBuilder.setComponentRef(componentRef);
TextRange primaryTextRange = issue.primaryLocation().textRange();
if (primaryTextRange != null) {
builder.setTextRange(toProtobufTextRange(textRangeBuilder, primaryTextRange));
}
Long effort = issue.remediationEffort();
if (effort != null) {
builder.setEffort(effort);
}
applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows());
return builder.build();
}

private static void applyFlows(Consumer<ScannerReport.Flow> consumer, ScannerReport.IssueLocation.Builder locationBuilder,
ScannerReport.TextRange.Builder textRangeBuilder, Collection<Flow> flows) {
ScannerReport.Flow.Builder flowBuilder = ScannerReport.Flow.newBuilder();
for (Flow flow : issue.flows()) {
for (Flow flow : flows) {
if (flow.locations().isEmpty()) {
return;
}
@@ -123,7 +159,7 @@ public class ModuleIssues {
}
flowBuilder.addLocation(locationBuilder.build());
}
builder.addFlow(flowBuilder.build());
consumer.accept(flowBuilder.build());
}
}

@@ -152,4 +188,7 @@ public class ModuleIssues {
reportPublisher.getWriter().appendComponentIssue(batchId, rawIssue);
}

public void write(int batchId, ScannerReport.ExternalIssue rawIssue) {
reportPublisher.getWriter().appendComponentExternalIssue(batchId, rawIssue);
}
}

+ 15
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java View File

@@ -123,6 +123,11 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver
int ref = reportComponents.get(inputComponent.key()).getRef();
return issuesFor(ref);
}
public List<ScannerReport.ExternalIssue> externalIssuesFor(InputComponent inputComponent) {
int ref = reportComponents.get(inputComponent.key()).getRef();
return externalIssuesFor(ref);
}

public List<ScannerReport.Issue> issuesFor(Component reportComponent) {
int ref = reportComponent.getRef();
@@ -138,6 +143,16 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver
}
return result;
}
private List<ScannerReport.ExternalIssue> externalIssuesFor(int ref) {
List<ScannerReport.ExternalIssue> result = Lists.newArrayList();
try (CloseableIterator<ScannerReport.ExternalIssue> it = reader.readComponentExternalIssues(ref)) {
while (it.hasNext()) {
result.add(it.next());
}
}
return result;
}

public Collection<InputFile> inputFiles() {
return inputFiles.values();

+ 8
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java View File

@@ -37,7 +37,9 @@ import org.sonar.api.batch.sensor.error.NewAnalysisError;
import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
import org.sonar.api.batch.sensor.measure.NewMeasure;
import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -130,6 +132,11 @@ public class DefaultSensorContext implements SensorContext {
return new DefaultIssue(sensorStorage);
}

@Override
public NewExternalIssue newExternalIssue() {
return new DefaultExternalIssue(sensorStorage);
}

@Override
public NewHighlighting newHighlighting() {
if (analysisMode.isIssues()) {
@@ -182,4 +189,5 @@ public class DefaultSensorContext implements SensorContext {
DefaultInputFile file = (DefaultInputFile) inputFile;
file.setPublished(true);
}

}

+ 14
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java View File

@@ -41,6 +41,7 @@ import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
import org.sonar.api.batch.sensor.error.AnalysisError;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.measure.Measure;
import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -373,6 +374,18 @@ public class DefaultSensorStorage implements SensorStorage {
moduleIssues.initAndAddIssue(issue);
}

/**
* Thread safe assuming that each issues for each file are only written once.
*/
@Override
public void store(ExternalIssue externalIssue) {
if (externalIssue.primaryLocation().inputComponent() instanceof DefaultInputFile) {
DefaultInputFile defaultInputFile = (DefaultInputFile) externalIssue.primaryLocation().inputComponent();
defaultInputFile.setPublished(true);
}
moduleIssues.initAndAddExternalIssue(externalIssue);
}

@Override
public void store(DefaultHighlighting highlighting) {
ScannerReportWriter writer = reportPublisher.getWriter();
@@ -493,4 +506,5 @@ public class DefaultSensorStorage implements SensorStorage {
public void storeProperty(String key, String value) {
contextPropertiesCache.put(key, value);
}

}

+ 18
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java View File

@@ -28,6 +28,7 @@ import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
import org.sonar.api.batch.rule.internal.RulesBuilder;
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
import org.sonar.api.rule.RuleKey;
@@ -146,6 +147,23 @@ public class ModuleIssuesTest {
assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
}

@Test
public void add_external_issue_to_cache() {
ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
initModuleIssues();

DefaultExternalIssue issue = new DefaultExternalIssue()
.at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
.forRule(SQUID_RULE_KEY)
.severity(org.sonar.api.batch.rule.Severity.CRITICAL);

moduleIssues.initAndAddExternalIssue(issue);

ArgumentCaptor<ScannerReport.ExternalIssue> argument = ArgumentCaptor.forClass(ScannerReport.ExternalIssue.class);
verify(reportPublisher.getWriter()).appendComponentExternalIssue(eq(file.batchId()), argument.capture());
assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
}

@Test
public void use_severity_from_active_rule_if_no_severity_on_issue() {
ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);

+ 23
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java View File

@@ -29,8 +29,10 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.scanner.mediumtest.ScannerMediumTester;
import org.sonar.scanner.mediumtest.TaskResult;
import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue;
import org.sonar.scanner.protocol.output.ScannerReport.Issue;
import org.sonar.xoo.XooPlugin;
import org.sonar.xoo.rule.OneExternalIssuePerLineSensor;
import org.sonar.xoo.rule.XooRulesDefinition;

import static org.assertj.core.api.Assertions.assertThat;
@@ -60,10 +62,31 @@ public class IssuesMediumTest {

List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
assertThat(issues).hasSize(8 /* lines */);
List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
assertThat(externalIssues).isEmpty();

Issue issue = issues.get(0);
assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getTextRange().getStartLine());
}
@Test
public void testOneExternalIssuePerLine() throws Exception {
File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
File tmpDir = temp.newFolder();
FileUtils.copyDirectory(projectDir, tmpDir);

TaskResult result = tester
.newScanTask(new File(tmpDir, "sonar-project.properties"))
.property(OneExternalIssuePerLineSensor.ACTIVATE_EXTERNAL_ISSUES, "true")
.execute();

List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
assertThat(externalIssues).hasSize(8 /* lines */);

ExternalIssue externalIssue = externalIssues.get(0);
assertThat(externalIssue.getTextRange().getStartLine()).isEqualTo(externalIssue.getTextRange().getStartLine());
}

@Test
public void findActiveRuleByInternalKey() throws Exception {

+ 1
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java View File

@@ -84,6 +84,7 @@ public class DefaultSensorContextTest {
assertThat(adaptor.runtime()).isEqualTo(runtime);

assertThat(adaptor.newIssue()).isNotNull();
assertThat(adaptor.newExternalIssue()).isNotNull();
assertThat(adaptor.newMeasure()).isNotNull();

assertThat(adaptor.isCancelled()).isFalse();

+ 14
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java View File

@@ -36,7 +36,9 @@ import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.measure.MetricFinder;
import org.sonar.api.batch.sensor.highlighting.TypeOfText;
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
import org.sonar.api.batch.sensor.issue.ExternalIssue;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
@@ -124,6 +126,18 @@ public class DefaultSensorStorageTest {
assertThat(argumentCaptor.getValue()).isEqualTo(issue);
}

@Test
public void should_save_external_issue() {
InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build();

DefaultExternalIssue externalIssue = new DefaultExternalIssue().at(new DefaultIssueLocation().on(file));
underTest.store(externalIssue);

ArgumentCaptor<ExternalIssue> argumentCaptor = ArgumentCaptor.forClass(ExternalIssue.class);
verify(moduleIssues).initAndAddExternalIssue(argumentCaptor.capture());
assertThat(argumentCaptor.getValue()).isEqualTo(externalIssue);
}

@Test
public void should_skip_issue_on_short_branch_when_file_status_is_SAME() {
InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();

+ 1
- 0
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java View File

@@ -31,6 +31,7 @@ public class FileStructure {

public enum Domain {
ISSUES("issues-", Domain.PB),
EXTERNAL_ISSUES("external-issues-", Domain.PB),
COMPONENT("component-", Domain.PB),
MEASURES("measures-", Domain.PB),
DUPLICATIONS("duplications-", Domain.PB),

+ 8
- 0
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java View File

@@ -83,6 +83,14 @@ public class ScannerReportReader {
return emptyCloseableIterator();
}

public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) {
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
if (fileExists(file)) {
return Protobuf.readStream(file, ScannerReport.ExternalIssue.parser());
}
return emptyCloseableIterator();
}

public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) {
File file = fileStructure.fileFor(FileStructure.Domain.DUPLICATIONS, componentRef);
if (fileExists(file)) {

+ 15
- 0
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java View File

@@ -82,6 +82,21 @@ public class ScannerReportWriter {
}
}

public File writeComponentExternalIssues(int componentRef, Iterable<ScannerReport.ExternalIssue> issues) {
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
Protobuf.writeStream(issues, file, false);
return file;
}

public void appendComponentExternalIssue(int componentRef, ScannerReport.ExternalIssue issue) {
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef);
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true))) {
issue.writeDelimitedTo(out);
} catch (Exception e) {
throw ContextException.of("Unable to write external issue", e).addContext("file", file);
}
}

public File writeComponentMeasures(int componentRef, Iterable<ScannerReport.Measure> measures) {
File file = fileStructure.fileFor(FileStructure.Domain.MEASURES, componentRef);
Protobuf.writeStream(measures, file, false);

+ 24
- 0
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto View File

@@ -186,6 +186,30 @@ message Issue {
repeated Flow flow = 7;
}

message ExternalIssue {
string rule_repository = 1;
string rule_key = 2;
// Only when issue component is a file. Can also be empty for a file if this is an issue global to the file.
string msg = 3;
Severity severity = 4;
int64 effort = 5;
// Only when issue component is a file. Can also be empty for a file if this is an issue global to the file.
// Will be identical to the first location of the first flow
TextRange text_range = 6;
repeated Flow flow = 7;
// Can be empty as the field is optional
string descriptionUrl = 8;
IssueType type = 9;
string rule_title = 10;
string rule_name = 11;
}

enum IssueType {
CODE_SMELL = 0;
BUG = 1;
VULNERABILITY = 2;
}

message IssueLocation {
int32 component_ref = 1;
// Only when component is a file. Can be empty for a file if this is an issue global to the file.

+ 4
- 1
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java View File

@@ -61,7 +61,8 @@ public class FileStructureTest {
public void locate_files() throws Exception {
File dir = temp.newFolder();
FileUtils.write(new File(dir, "metadata.pb"), "metadata content");
FileUtils.write(new File(dir, "issues-3.pb"), "issues of component 3");
FileUtils.write(new File(dir, "issues-3.pb"), "external issues of component 3");
FileUtils.write(new File(dir, "external-issues-3.pb"), "issues of component 3");
FileUtils.write(new File(dir, "component-42.pb"), "details of component 42");

FileStructure structure = new FileStructure(dir);
@@ -69,6 +70,8 @@ public class FileStructureTest {
assertThat(structure.fileFor(FileStructure.Domain.COMPONENT, 42)).exists().isFile();
assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 3)).exists().isFile();
assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 42)).doesNotExist();
assertThat(structure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 3)).exists().isFile();
assertThat(structure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 42)).doesNotExist();
}

@Test

+ 11
- 0
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java View File

@@ -104,6 +104,17 @@ public class ScannerReportReaderTest {
assertThat(underTest.readComponentIssues(200)).isEmpty();
}

@Test
public void read_external_issues() {
ScannerReportWriter writer = new ScannerReportWriter(dir);
ScannerReport.ExternalIssue issue = ScannerReport.ExternalIssue.newBuilder()
.build();
writer.writeComponentExternalIssues(1, asList(issue));

assertThat(underTest.readComponentExternalIssues(1)).hasSize(1);
assertThat(underTest.readComponentExternalIssues(200)).isEmpty();
}

@Test
public void empty_list_if_no_issue_found() {
assertThat(underTest.readComponentIssues(UNKNOWN_COMPONENT_REF)).isEmpty();

+ 20
- 0
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java View File

@@ -116,6 +116,26 @@ public class ScannerReportWriterTest {
}
}

@Test
public void write_external_issues() {
// no data yet
assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isFalse();

// write data
ScannerReport.ExternalIssue issue = ScannerReport.ExternalIssue.newBuilder()
.setMsg("the message")
.build();

underTest.writeComponentExternalIssues(1, asList(issue));

assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isTrue();
File file = underTest.getFileStructure().fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 1);
assertThat(file).exists().isFile();
try (CloseableIterator<ScannerReport.ExternalIssue> read = Protobuf.readStream(file, ScannerReport.ExternalIssue.parser())) {
assertThat(Iterators.size(read)).isEqualTo(1);
}
}

@Test
public void write_measures() {
assertThat(underTest.hasComponentData(FileStructure.Domain.MEASURES, 1)).isFalse();

Loading…
Cancel
Save